View Javadoc
1   package fr.ifremer.dali.ui.swing.action;
2   
3   /*
4    * #%L
5    * Dali :: UI
6    * $Id:$
7    * $HeadURL:$
8    * %%
9    * Copyright (C) 2014 - 2015 Ifremer
10   * %%
11   * This program is free software: you can redistribute it and/or modify
12   * it under the terms of the GNU Affero General Public License as published by
13   * the Free Software Foundation, either version 3 of the License, or
14   * (at your option) any later version.
15   *
16   * This program is distributed in the hope that it will be useful,
17   * but WITHOUT ANY WARRANTY; without even the implied warranty of
18   * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
19   * GNU General Public License for more details.
20   *
21   * You should have received a copy of the GNU Affero General Public License
22   * along with this program.  If not, see <http://www.gnu.org/licenses/>.
23   * #L%
24   */
25  
26  import com.google.common.collect.Multimap;
27  import fr.ifremer.common.synchro.service.SynchroResult;
28  import fr.ifremer.dali.dto.DaliBeans;
29  import fr.ifremer.dali.dto.configuration.programStrategy.ProgramDTO;
30  import fr.ifremer.dali.service.DaliServiceLocator;
31  import fr.ifremer.dali.service.StatusFilter;
32  import fr.ifremer.dali.ui.swing.DaliUIContext;
33  import fr.ifremer.dali.ui.swing.content.DaliMainUIHandler;
34  import fr.ifremer.dali.ui.swing.content.synchro.program.ProgramSelectUI;
35  import fr.ifremer.dali.ui.swing.util.DaliUIs;
36  import fr.ifremer.quadrige3.core.dao.technical.ZipUtils;
37  import fr.ifremer.quadrige3.core.exception.QuadrigeTechnicalException;
38  import fr.ifremer.quadrige3.core.security.SecurityContextHelper;
39  import fr.ifremer.quadrige3.core.service.http.HttpNotFoundException;
40  import fr.ifremer.quadrige3.synchro.meta.data.DataSynchroTables;
41  import fr.ifremer.quadrige3.synchro.service.client.SynchroClientService;
42  import fr.ifremer.quadrige3.synchro.service.client.SynchroRejectedRowResolver;
43  import fr.ifremer.quadrige3.synchro.service.client.SynchroRestClientService;
44  import fr.ifremer.quadrige3.synchro.service.client.vo.SynchroClientExportResult;
45  import fr.ifremer.quadrige3.synchro.service.data.DataSynchroContext;
46  import fr.ifremer.quadrige3.synchro.vo.SynchroProgressionStatus;
47  import fr.ifremer.quadrige3.ui.swing.ApplicationUI;
48  import fr.ifremer.quadrige3.ui.swing.action.AbstractReloadCurrentScreenAction;
49  import fr.ifremer.quadrige3.ui.swing.model.ProgressionUIModel;
50  import fr.ifremer.quadrige3.ui.swing.synchro.SynchroDirection;
51  import fr.ifremer.quadrige3.ui.swing.synchro.SynchroUIContext;
52  import fr.ifremer.quadrige3.ui.swing.synchro.SynchroUIHandler;
53  import fr.ifremer.quadrige3.ui.swing.synchro.action.*;
54  import fr.ifremer.quadrige3.ui.swing.synchro.resolver.SynchroRejectedRowUIResolver;
55  import org.apache.commons.collections4.CollectionUtils;
56  import org.apache.commons.logging.Log;
57  import org.apache.commons.logging.LogFactory;
58  import org.nuiton.jaxx.application.ApplicationIOUtil;
59  
60  import javax.swing.JOptionPane;
61  import java.awt.Dimension;
62  import java.io.File;
63  import java.util.LinkedHashSet;
64  import java.util.List;
65  import java.util.Set;
66  import java.util.stream.Collectors;
67  
68  import static org.nuiton.i18n.I18n.t;
69  
70  /**
71   * <p>ExportSynchroAction class.</p>
72   *
73   * @author ludovic.pecquot@e-is.pro
74   */
75  public class ExportSynchroAction extends AbstractReloadCurrentScreenAction {
76  
77      private static final Log log = LogFactory.getLog(ExportSynchroAction.class);
78  
79      private Set<String> programCodes;
80      private int userId;
81      private File dbDirToExport;
82      private boolean serverJobRunning = false;
83      private boolean serverFailed = false;
84      private boolean hasData = false;
85  
86      /**
87       * <p>Constructor for ExportSynchroAction.</p>
88       *
89       * @param handler a {@link DaliMainUIHandler} object.
90       */
91      public ExportSynchroAction(DaliMainUIHandler handler) {
92          super(handler, true);
93          setActionDescription(t("dali.action.synchro.export.title"));
94      }
95  
96      @Override
97      public DaliUIContext getContext() {
98          return (DaliUIContext) super.getContext();
99      }
100 
101     private SynchroUIContext getSynchroUIContext() {
102         return getContext().getSynchroContext();
103     }
104 
105     private SynchroUIHandler getSynchroHandler() {
106         return getContext().getSynchroHandler();
107     }
108 
109     /**
110      * {@inheritDoc}
111      */
112     @Override
113     public boolean prepareAction() throws Exception {
114         super.prepareAction();
115 
116         if (!getContext().isSynchroEnabled() && getConfig().isSynchronizationUsingServer()) {
117             getContext().getDialogHelper().showWarningDialog(t("dali.synchro.unavailable"), t("dali.action.synchro.export.title"));
118             return false;
119         }
120 
121         // Check if user has access right (not local user, or has some programs)
122         if (getContext().isAuthenticatedAsLocalUser()
123                 || !getContext().getProgramStrategyService().hasRemoteAccessRightOnProgram(getContext().getAuthenticationInfo())) {
124             getContext().getDialogHelper().showWarningDialog(t("dali.synchro.export.accessDenied"), t("dali.action.synchro.export.title"));
125             return false;
126         }
127 
128         // load actual export context
129         getSynchroUIContext().setDirection(SynchroDirection.EXPORT);
130         getSynchroUIContext().loadExportContext();
131         userId = SecurityContextHelper.getQuadrigeUserId();
132 
133         // Ask user to select program to import
134         programCodes = getSynchroUIContext().getExportDataProgramCodes();
135 
136         // Hide photo option (Mantis #48536)
137         ProgramSelectUI programSelectUI = new ProgramSelectUI((ApplicationUI) getUI(), StatusFilter.ACTIVE, programCodes, false, false);
138 
139         // Force photo option to true (Mantis #48536)
140         programSelectUI.getModel().setEnablePhoto(true);
141 
142         handler.openDialog(programSelectUI, t("dali.action.synchro.export.dataProgramCodes.title"), new Dimension(800, 400));
143 
144         List<ProgramDTO> programs = programSelectUI.getModel().getSelectedPrograms();
145 
146         // If no programs selected (or user cancelled): exit
147         if (CollectionUtils.isEmpty(programs)) {
148             return false;
149         }
150 
151         // Get selected programs as code list
152         programCodes = new LinkedHashSet<>(DaliBeans.collectProperties(programs, ProgramDTO.PROPERTY_CODE));
153 
154         // get photo option and set it in context
155         getSynchroUIContext().setExportPhoto(programSelectUI.getModel().isEnablePhoto());
156 
157         return true;
158     }
159 
160     /**
161      * {@inheritDoc}
162      */
163     @Override
164     public void doActionBeforeReload() throws Exception {
165         dbDirToExport = null;
166         serverJobRunning = false;
167         serverFailed = false;
168         hasData = false;
169 
170         SynchroClientService synchroService = DaliServiceLocator.instance().getSynchroClientService();
171 
172         getSynchroUIContext().setExportDataProgramCodes(programCodes);
173         getSynchroUIContext().saveExportContext();
174 
175         createProgressionUIModel(100);
176         getSynchroUIContext().setStatus(SynchroProgressionStatus.RUNNING);
177         getSynchroUIContext().saveExportContext();
178 
179         // build temp database and export local to temp
180         getProgressionUIModel().setMessage(t("quadrige3.synchro.progress.export"));
181 
182         if (!getConfig().isSynchronizationUsingServer()) {
183 
184             SynchroRejectedRowResolver rejectResolver = new SynchroRejectedRowUIResolver(getContext().getDialogHelper(), false /*export*/);
185 
186             // do it in one go with direct synchronization
187             SynchroClientExportResult exportResult = synchroService.exportToServerDatabase(userId,
188                     programCodes,
189                     rejectResolver,
190                     getProgressionUIModel(),
191                     100);
192             hasData = exportResult.getDataResult().getTotalTreated() > 0;
193             if (!hasData) {
194                 showNoDataMessage();
195             }
196 
197         } else {
198 
199             // Do a referential import if need (see mantis #25665)
200             doImportReferential();
201 
202             // After potential user privilege updates, check write access (Mantis #39506)
203             if (!checkWriteAccess()) return;
204 
205             // export directory is set by the synchronization service
206             SynchroClientExportResult exportResult = synchroService.exportDataToTempDb(userId,
207                     programCodes,
208                     getSynchroUIContext().isExportPhoto(),
209                     getProgressionUIModel(),
210                     100);
211             DataSynchroContext context = exportResult.getDataContext();
212 
213             // If no data to export, stop here
214             hasData = context.getResult().getTotalTreated() > 0;
215             if (!hasData) {
216                 showNoDataMessage();
217                 return;
218             }
219 
220             // check photo volume
221             int nbPhoto = context.getResult().getNbRows(DataSynchroTables.PHOTO.name());
222             int threshold = getConfig().getSynchroPhotoMaxNumberThreshold();
223             if (threshold > 0 && nbPhoto > threshold) {
224                 if (getContext().getDialogHelper().showConfirmDialog(
225                         t("quadrige3.synchro.export.photo.overThreshold.message", nbPhoto),
226                         t("dali.action.synchro.export.title"),
227                         JOptionPane.YES_NO_OPTION) == JOptionPane.NO_OPTION)
228                     return;
229             }
230 
231             // compress the database
232             dbDirToExport = exportResult.getTempDbExportDirectory();
233             getProgressionUIModel().setMessage(t("quadrige3.synchro.progress.compress"));
234             String filename = dbDirToExport.getName() + ".zip";
235             File zipFile = new File(dbDirToExport.getParent(), filename);
236             ZipUtils.compressFilesInPath(dbDirToExport.toPath(), zipFile.toPath(), getProgressionUIModel(), false, true);
237 
238             getSynchroUIContext().setExportFileName(zipFile.getName());
239             getSynchroUIContext().saveExportContext();
240 
241             // delegate progression model from SynchroUIContext
242             getProgressionUIModel().setTotal(100);
243             setProgressionUIModel(getSynchroUIContext().getProgressionModel());
244 
245             // send it to synchro server
246             ExportSynchroUploadAction uploadAction = getContext().getActionFactory().createNonBlockingUIAction(getContext().getSynchroHandler(),
247                     ExportSynchroUploadAction.class);
248             uploadAction.executeAndWait();
249 
250             boolean needDownloadResultAndFinish = false;
251             int retryCounter = 0;
252             while (true) {
253                 try {
254                     // Start export job on server
255                     if (!serverJobRunning) {
256                         getProgressionUIModel().clear();
257                         ExportSynchroStartAction startAction = getContext().getActionFactory().createNonBlockingUIAction(getContext().getSynchroHandler(),
258                                 ExportSynchroStartAction.class);
259                         startAction.executeAndWait();
260                         serverJobRunning = true;
261 
262                         Thread.sleep(getConfig().getSynchronizationRefreshTimeout()); // wait
263                     }
264 
265                     // Get job status  on server (if job still alive)
266                     if (serverJobRunning) {
267                         if (getSynchroUIContext().isRunningStatus()) {
268                             try {
269                                 ExportSynchroGetStatusAction getStatusAction = getContext().getActionFactory().createNonBlockingUIAction(getContext().getSynchroHandler(),
270                                         ExportSynchroGetStatusAction.class);
271                                 getStatusAction.executeAndWait();
272                                 needDownloadResultAndFinish = true;
273                             } catch (HttpNotFoundException e) {
274                                 // Job may has finished (we don't really known): continue as a successful status
275                                 getSynchroUIContext().setStatus(SynchroProgressionStatus.SUCCESS);
276                                 needDownloadResultAndFinish = true;
277                             }
278 
279                             // if SynchroException, do not try to reconnect - fix mantis #39388
280                             catch (SynchroException se) {
281                                 serverJobRunning = false;
282                                 serverFailed = true;
283                                 throw se; // TODO Dont throw here because the result file has to be downloaded from server
284                                 // needDownloadResultAndFinish = true;
285                             }
286                         } else {
287                             needDownloadResultAndFinish = true;
288                         }
289                     }
290 
291                     // Finish export
292                     if (needDownloadResultAndFinish) {
293                         try {
294                             finishExportThenCleanFiles(exportResult, getProgressionUIModel());
295                             break; // stop loop
296                         }
297 
298                         // Server delete the result file = failed
299                         catch (HttpNotFoundException e) {
300                             getSynchroUIContext().setStatus(SynchroProgressionStatus.FAILED);
301                             serverFailed = true;
302                             throw new SynchroException(t("dali.action.synchro.export.failed.serverJobStarted.log", e.getMessage()), e); // stop
303                         }
304                     }
305                 } catch (QuadrigeTechnicalException e) {
306                     retryCounter++;
307                     log.debug(String.format("Error during export: %s", e.getMessage()), e);
308                     getProgressionUIModel().setMessage(t("dali.action.synchro.export.retry.progression", retryCounter, getConfig().getSynchronizationMaxRetryCount()));
309 
310                     // Retry many times, or ask user to retry (mantis #35441)
311                     if (retryCounter < getConfig().getSynchronizationMaxRetryCount()) {
312                         Thread.sleep(getConfig().getSynchronizationRetryTimeout()); // Wait then loop (= retry)
313                     } else {
314                         int result = getContext().getDialogHelper().showOptionDialog(null,
315                                 serverJobRunning
316                                         ? t("dali.action.synchro.export.retry.ask.serverJobStarted",
317                                         getConfig().getAdminEmail())
318                                         : t("dali.action.synchro.export.retry.ask"),
319                                 t("dali.action.synchro.export.retry.title"), JOptionPane.ERROR_MESSAGE, JOptionPane.YES_NO_OPTION);
320 
321                         // No = stop
322                         if (result == JOptionPane.NO_OPTION) throw e;
323 
324                         // Or try again
325                         retryCounter = 0; // reset the counter
326                         needDownloadResultAndFinish = true; // loop (= retry)
327                     }
328                 }
329             }
330 
331         }
332 
333         // Skip screen reloading if No updates
334         setSkipScreenReload(!hasData);
335     }
336 
337     /**
338      * {@inheritDoc}
339      */
340     @Override
341     public void postSuccessAction() {
342         // If server failed (but no technical error during action)
343         if (serverFailed) {
344             postFailedAction(null);
345             return;
346         }
347 
348         super.postSuccessAction();
349         if (log.isInfoEnabled()) {
350             log.info("Synchronization export success");
351         }
352 
353         getSynchroUIContext().resetExportContext();
354         getSynchroUIContext().saveExportContext();
355 
356         // do not display if no data found
357         if (!hasData) {
358             getContext().getSynchroHandler().report(t("dali.action.synchro.export.noData"), false);
359         } else {
360             getContext().getSynchroHandler().report(t("dali.action.synchro.export.success"));
361         }
362 
363     }
364 
365     /**
366      * {@inheritDoc}
367      */
368     @Override
369     public void postFailedAction(Throwable error) {
370         super.postFailedAction(error);
371 
372         if (error != null) {
373             log.error(t("dali.action.synchro.export.failed.log", error.getMessage()));
374         } else if (getSynchroUIContext().getProgressionModel() != null
375                 && getSynchroUIContext().getProgressionModel().getMessage() != null) {
376 
377             String serverMessage = getSynchroUIContext().getProgressionModel().getMessage();
378             log.error(t("dali.action.synchro.export.failed.server.log", serverMessage));
379         } else {
380             log.error(t("dali.action.synchro.export.failed"));
381         }
382 
383         getSynchroUIContext().resetExportContext();
384         getSynchroUIContext().saveExportContext();
385 
386         if (serverJobRunning) {
387             // warn user
388             getContext().getSynchroHandler().report(t("dali.action.synchro.export.failed.serverJobStarted",
389                     getConfig().getAdminEmail()));
390 
391             // Log to file
392             DaliServiceLocator.instance().getSynchroHistoryService().saveExportError(
393                     userId,
394                     getSynchroUIContext().getSynchroExportContext(),
395                     t("dali.action.synchro.export.failed.serverJobStarted.history",
396                             error != null ? error.getMessage() : ""));
397         } else {
398             getContext().getSynchroHandler().report(t("dali.action.synchro.export.failed"));
399         }
400     }
401 
402     /* -- Internal methods -- */
403 
404     private void finishExportThenCleanFiles(SynchroClientExportResult exportResult,
405                                             ProgressionUIModel mainProgressionModel) throws Exception {
406 
407         // Save server status and message
408         SynchroProgressionStatus serverStatus = getSynchroUIContext().getStatus();
409         serverFailed = serverStatus != SynchroProgressionStatus.SUCCESS; // server hasn't committed exported rows
410         String serverErrorMessage = serverFailed ? getSynchroUIContext().getProgressionModel().getMessage() : null;
411 
412         // download the result file
413         boolean hasRejectMessages = false;
414         SynchroRestClientService restService = DaliServiceLocator.instance().getSynchroRestClientService();
415         SynchroResult serverResult = restService.downloadExportResult(
416                 getContext().getAuthenticationInfo(),
417                 getModel().getSynchroContext().getSynchroExportContext(),
418                 getProgressionUIModel());
419         boolean resultFileExists = serverResult != null;
420 
421         // restore action progression model
422         setProgressionUIModel(mainProgressionModel);
423 
424         // exploit the result file: finish export
425         if (resultFileExists) {
426             exportResult.setServerResult(serverResult);
427 
428             mainProgressionModel.setMessage(t("quadrige3.synchro.progress.finishExport"));
429 
430             SynchroRejectedRowResolver rejectResolver = new SynchroRejectedRowUIResolver(getContext().getDialogHelper(), false /*export*/);
431             SynchroClientService synchroService = DaliServiceLocator.instance().getSynchroClientService();
432 
433             try {
434                 hasRejectMessages = synchroService.finishExportData(userId, exportResult, rejectResolver, serverFailed, false /* do not revert PKs inside this method */);
435             } catch (QuadrigeTechnicalException e) {
436                 // Conversion to SynchroException is need, to avoid a retry
437                 throw new SynchroException(t("quadrige3.error.synchro.export.finish"), e);
438             }
439 
440             // get Pks to revert, then apply revert
441             DataSynchroContext context = exportResult.getDataContext();
442             Multimap<String, String> pksToRevert = context.getResult().getSourceMissingReverts();
443             revertPks(pksToRevert);
444         }
445 
446         // cleanFiles files (local and server)
447         cleanFiles();
448 
449         // Server failed BEFORE finish: restore server status and message
450         if (serverFailed) {
451 
452             // Restore the server status and message (could have been overridden by download action)
453             getSynchroUIContext().getProgressionModel().setMessage(serverErrorMessage);
454             getSynchroUIContext().setStatus(serverStatus);
455 
456             if (!resultFileExists || !hasRejectMessages) {
457                 // If nothing was displayed yet, throw the exception (will display an error dialog)
458                 throw new SynchroException(t("quadrige3.error.synchro.status", serverErrorMessage));
459             }
460         }
461     }
462 
463     private void revertPks(Multimap<String, String> pksToRevert) throws Exception {
464         if (pksToRevert == null || pksToRevert.isEmpty()) {
465             return;
466         }
467         getSynchroUIContext().resetImportContext();
468         getSynchroUIContext().setImportData(true);
469         getSynchroUIContext().setImportReferential(false);
470         // always enable photo on this import (Mantis #48536 & #48479)
471         getSynchroUIContext().setImportPhoto(true);
472         getSynchroUIContext().setImportDataPkIncludes(pksToRevert);
473         getSynchroUIContext().setDirection(SynchroDirection.IMPORT);
474         getSynchroUIContext().setImportDataForceEditedRowOverride(true);
475 
476         // send it to synchro server
477         ImportSynchroStartAction reimportAction = getContext().getActionFactory().createNonBlockingUIAction(getContext().getSynchroHandler(),
478                 ImportSynchroStartAction.class);
479 
480         // execute and wait for it
481         reimportAction.executeAndWait();
482 
483         // download
484         ImportSynchroDownloadAction action = getContext().getActionFactory().createNonBlockingUIAction(getContext().getSynchroHandler(),
485                 ImportSynchroDownloadAction.class);
486 
487         // execute and wait for it
488         action.executeAndWait();
489 
490         // Import Temp DB
491         ImportSynchroApplyAction applyImportAction = getContext().getActionFactory().createLogicAction(getHandler(), ImportSynchroApplyAction.class);
492         applyImportAction.prepareAction();
493         applyImportAction.setSilent(true);
494         getContext().getActionEngine().runInternalAction(applyImportAction);
495     }
496 
497     private void cleanFiles() throws Exception {
498 
499         // clean local files
500         ApplicationIOUtil.forceDeleteOnExit(dbDirToExport, t("quadrige3.error.delete.directory", dbDirToExport.getAbsolutePath()));
501         File fileToDelete = new File(dbDirToExport.getParent(), dbDirToExport.getName() + ".zip");
502         ApplicationIOUtil.deleteFile(fileToDelete, t("quadrige3.error.delete.directory", fileToDelete.getAbsolutePath()));
503 
504         fileToDelete = new File(dbDirToExport.getParent(), dbDirToExport.getName() + ".json");
505         if (fileToDelete.exists()) {
506             ApplicationIOUtil.deleteFile(fileToDelete, t("quadrige3.error.delete.directory", fileToDelete.getAbsolutePath()));
507         }
508 
509         // send acknowledge
510         ExportSynchroAckAction ackAction = getContext().getActionFactory().createNonBlockingUIAction(getSynchroHandler(),
511                 ExportSynchroAckAction.class);
512         ackAction.executeAndWait();
513     }
514 
515     private void showNoDataMessage() {
516         getContext().getDialogHelper().showMessageDialog(t("dali.action.synchro.export.noData"),
517                 t("dali.action.synchro.export.result.title"));
518     }
519 
520     /**
521      * Do a import of referential data
522      */
523     private void doImportReferential() {
524 
525         ImportReferentialSynchroAtOnceAction importAction = getContext().getActionFactory().createLogicAction(getHandler(), ImportReferentialSynchroAtOnceAction.class);
526         getContext().getActionEngine().runInternalAction(importAction);
527 
528         // Restore previous running status (fix mantis #39390)
529         getSynchroUIContext().setStatus(SynchroProgressionStatus.RUNNING);
530     }
531 
532     private boolean checkWriteAccess() {
533 
534         List<ProgramDTO> programs = getContext().getProgramStrategyService().getRemoteProgramsByUser(getContext().getAuthenticationInfo());
535         Set<String> allowedProgramCodes = programs.stream().map(ProgramDTO::getCode).collect(Collectors.toSet());
536         if (CollectionUtils.isEmpty(programs) || !allowedProgramCodes.containsAll(programCodes)) {
537             getContext().getDialogHelper().showWarningDialog(
538                     t("dali.synchro.export.accessDenied.program.topMessage"),
539                     DaliUIs.getHtmlString(CollectionUtils.removeAll(programCodes, allowedProgramCodes)),
540                     t("dali.synchro.export.accessDenied.program.bottomMessage"),
541                     t("dali.action.synchro.export.title")
542             );
543             return false;
544         }
545         return true;
546     }
547 }