View Javadoc
1   package fr.ifremer.quadrige2.ui.swing.common;
2   
3   /*-
4    * #%L
5    * Quadrige2 Core :: Quadrige2 UI Common
6    * $Id:$
7    * $HeadURL:$
8    * %%
9    * Copyright (C) 2017 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.base.Preconditions;
27  import com.google.common.collect.Lists;
28  import com.google.common.collect.Sets;
29  import fr.ifremer.quadrige2.core.config.Quadrige2CoreConfiguration;
30  import fr.ifremer.quadrige2.core.exception.Quadrige2BusinessException;
31  import fr.ifremer.quadrige2.core.exception.Quadrige2TechnicalException;
32  import fr.ifremer.quadrige2.core.security.AuthenticationInfo;
33  import fr.ifremer.quadrige2.synchro.vo.SynchroProgressionStatus;
34  import fr.ifremer.quadrige2.ui.swing.common.action.ActionFactory;
35  import fr.ifremer.quadrige2.ui.swing.common.action.ActionUI;
36  import fr.ifremer.quadrige2.ui.swing.common.content.MainUI;
37  import fr.ifremer.quadrige2.ui.swing.common.desktop.Desktop;
38  import fr.ifremer.quadrige2.ui.swing.common.desktop.DesktopPower;
39  import fr.ifremer.quadrige2.ui.swing.common.model.ProgressionModel;
40  import fr.ifremer.quadrige2.ui.swing.common.synchro.SynchroUI;
41  import fr.ifremer.quadrige2.ui.swing.common.synchro.SynchroUIContext;
42  import fr.ifremer.quadrige2.ui.swing.common.synchro.SynchroUIHandler;
43  import org.apache.commons.io.FileUtils;
44  import org.apache.commons.logging.Log;
45  import org.apache.commons.logging.LogFactory;
46  import org.jdesktop.beans.AbstractBean;
47  import org.nuiton.converter.ConverterUtil;
48  import org.nuiton.i18n.I18n;
49  import org.nuiton.i18n.init.DefaultI18nInitializer;
50  import org.nuiton.i18n.init.UserI18nInitializer;
51  import org.nuiton.jaxx.application.ApplicationBusinessException;
52  import org.nuiton.jaxx.application.ApplicationIOUtil;
53  import org.nuiton.jaxx.application.bean.JavaBeanObjectUtil;
54  import org.nuiton.jaxx.application.swing.action.ApplicationActionEngine;
55  import org.nuiton.jaxx.application.swing.util.ApplicationErrorHelper;
56  
57  import javax.swing.JComponent;
58  import javax.swing.JFrame;
59  import java.awt.Component;
60  import java.beans.PropertyChangeEvent;
61  import java.beans.PropertyChangeListener;
62  import java.beans.PropertyChangeListenerProxy;
63  import java.io.Closeable;
64  import java.io.File;
65  import java.io.IOException;
66  import java.io.Writer;
67  import java.util.Date;
68  import java.util.LinkedList;
69  import java.util.Locale;
70  import java.util.Set;
71  import java.util.concurrent.ThreadPoolExecutor;
72  
73  import static org.nuiton.i18n.I18n.n;
74  import static org.nuiton.i18n.I18n.t;
75  
76  /**
77   * @author peck7 on 28/06/2017.
78   */
79  public abstract class ApplicationUIContext extends AbstractBean implements org.nuiton.jaxx.application.swing.ApplicationUIContext, UIMessageNotifier, Closeable {
80  
81      private static final Log LOG = LogFactory.getLog(ApplicationUIContext.class);
82  
83      /**
84       * Application context (only one for all the application).
85       */
86      private static ApplicationUIContext instance;
87  
88      public static final String PROPERTY_LOCALE = "locale";
89      public static final String PROPERTY_SCREEN = "screen";
90      public static final String PROPERTY_BUSY = "busy";
91      public static final String PROPERTY_HIDE_BODY = "hideBody";
92      public static final String PROPERTY_AUTHENTICATED = "authenticated";
93      public static final String PROPERTY_SYNCHRO_RUNNING = "synchroRunning";
94      public static final String PROPERTY_DB_EXIST = "dbExist";
95      public static final String PROPERTY_DB_LOADED = "dbLoaded";
96  
97      public static final Set<String> PROPERTIES_TO_SAVE = Sets.newHashSet(
98              PROPERTY_LOCALE
99      );
100 
101     /**
102      * Application global configuration.
103      */
104     private Quadrige2CoreConfiguration configuration;
105     /**
106      * session used to save ui states.
107      */
108     private SwingSession swingSession;
109     protected ThreadPoolExecutor saveComponentInSwingSessionExecutor;
110 
111     /**
112      * Main UI
113      */
114     private MainUI mainUI;
115     private ActionUI actionUI;
116     /**
117      * Action factory
118      */
119     private ActionFactory actionFactory;
120     private ApplicationActionEngine actionEngine;
121     /**
122      * Dialog helper
123      */
124     private DialogHelper dialogHelper;
125     /**
126      * Current screen displayed in ui.
127      */
128     private Screen screen;
129     private LinkedList<Screen> screenBreadcrumb;
130     /**
131      * Current locale used in application.
132      */
133     private Locale locale;
134     /**
135      * Flag to know if there is an existing db.
136      */
137     private boolean dbExist;
138     /**
139      * Flag to know if the database has been imported or installed
140      */
141     private boolean dbJustInstalled;
142     /**
143      * Flag to know if the database has been imported from file
144      */
145     private boolean dbJustImportedFromFile;
146     /**
147      * Flag to know if there is a loaded db.
148      */
149     private boolean persistenceLoaded;
150     /**
151      * Busy state ({@code true} when a blocking action is running).
152      */
153     private boolean busy;
154     /**
155      * Flag to hide (or not) the body of application.
156      */
157     private boolean hideBody;
158     /**
159      * Flag to know if context was already closed. <br/>
160      * Can happen when you close nicely the application, the shutdown hook must then not close a second time this context.
161      */
162     private boolean closed;
163     /**
164      * A file lock to prevent multiple instance. the lock is create in the {@link #init(String)} method and remove in the {@link #close()} method. <br/>
165      * Now use a Writer to keep handle during application life.
166      */
167     private Writer lock;
168 
169     /*
170      * Synchronization Context
171      */
172     private SynchroUIContext synchroUIContext;
173     private SynchroUIHandler synchroUIHandler;
174     private boolean synchroRunning;
175     private final PropertyChangeListener synchroRunningListener = new PropertyChangeListener() {
176         @Override
177         public void propertyChange(PropertyChangeEvent evt) {
178             if (SynchroUIContext.PROPERTY_PROGRESSION_STATUS.equals(evt.getPropertyName())) {
179                 ApplicationUIContext.this.setSynchroRunning(!SynchroProgressionStatus.NOT_STARTED.equals(evt.getNewValue()));
180             }
181         }
182     };
183 
184     /**
185      * To keep last authentication credential in memory.
186      */
187     private AuthenticationInfo authenticationInfo;
188     /**
189      * To provide authentication information in status bar
190      */
191     private boolean authenticated;
192 
193     protected ApplicationUIContext(Quadrige2CoreConfiguration configuration) {
194         this.configuration = configuration;
195         instance = this;
196     }
197 
198     public static ApplicationUIContext getInstance() {
199         return instance;
200     }
201 
202     public static void setInstance(ApplicationUIContext applicationUIContext) {
203         instance = applicationUIContext;
204     }
205 
206     @Override
207     public Quadrige2CoreConfiguration getConfiguration() {
208         return configuration;
209     }
210 
211     @Override
212     public MainUI getMainUI() {
213         return mainUI;
214     }
215 
216     public void setMainUI(MainUI mainUI) {
217         this.mainUI = mainUI;
218     }
219 
220     public void installActionUI(JFrame frame) {
221         setActionUI(new ActionUI(frame, this));
222     }
223 
224     @Override
225     public ActionFactory getActionFactory() {
226         if (actionFactory == null) {
227             actionFactory = new ActionFactory();
228         }
229         return actionFactory;
230     }
231 
232     @Override
233     public ApplicationActionEngine getActionEngine() {
234         if (actionEngine == null) {
235             actionEngine = new ApplicationActionEngine(getActionFactory());
236         }
237         return actionEngine;
238     }
239 
240     public DialogHelper getDialogHelper() {
241         if (dialogHelper == null) {
242             dialogHelper = new DialogHelper(this);
243         }
244         return dialogHelper;
245     }
246 
247     @Override
248     public ApplicationErrorHelper getErrorHelper() {
249         return getDialogHelper();
250     }
251 
252     // DB methods
253 
254     public boolean isDbExist() {
255         return dbExist;
256     }
257 
258     public void setDbExist(boolean dbExist) {
259         this.dbExist = dbExist;
260         firePropertyChange(PROPERTY_DB_EXIST, null, dbExist);
261     }
262 
263     public boolean isDbJustInstalled() {
264         return dbJustInstalled;
265     }
266 
267     public void setDbJustInstalled(boolean dbJustInstalled) {
268         this.dbJustInstalled = dbJustInstalled;
269     }
270 
271     public boolean isDbJustImportedFromFile() {
272         return dbJustImportedFromFile;
273     }
274 
275     public void setDbJustImportedFromFile(boolean dbJustImportedFromFile) {
276         this.dbJustImportedFromFile = dbJustImportedFromFile;
277     }
278 
279     public boolean isPersistenceLoaded() {
280         return persistenceLoaded;
281     }
282 
283     public void setPersistenceLoaded(boolean persistenceLoaded) {
284         this.persistenceLoaded = persistenceLoaded;
285         firePropertyChange(PROPERTY_DB_LOADED, null, persistenceLoaded);
286     }
287 
288     // UI methods
289 
290     @Override
291     public ActionUI getActionUI() {
292         return actionUI;
293     }
294 
295     public void setActionUI(ActionUI actionUI) {
296         this.actionUI = actionUI;
297     }
298 
299     public Screen getScreen() {
300         return screen;
301     }
302 
303     public void setScreen(Screen screen) {
304         Screen oldScreen = getScreen();
305         this.screen = screen;
306         firePropertyChange(PROPERTY_SCREEN, oldScreen, screen);
307     }
308 
309     public LinkedList<Screen> getScreenBreadcrumb() {
310         if (screenBreadcrumb == null) {
311             screenBreadcrumb = Lists.newLinkedList();
312         }
313         return screenBreadcrumb;
314     }
315 
316     public void setFallBackScreen() {
317         if (isPersistenceLoaded()) {
318             setScreen(Screen.HOME);
319         } else {
320             setScreen(Screen.MANAGE_DB);
321         }
322     }
323 
324     public Locale getLocale() {
325         return locale;
326     }
327 
328     public void setLocale(Locale locale) {
329         this.locale = locale;
330 
331         // change i18n locale
332         configuration.setI18nLocale(locale);
333         I18n.setDefaultLocale(locale);
334         Locale.setDefault(locale);
335         JComponent.setDefaultLocale(locale);
336 
337         firePropertyChange(PROPERTY_LOCALE, null, locale);
338     }
339 
340     public boolean acceptLocale(String expected) {
341         return getLocale() != null && getLocale().toString().equals(expected);
342     }
343 
344     @Override
345     public boolean isBusy() {
346         return busy;
347     }
348 
349     @Override
350     public void setBusy(boolean busy) {
351         this.busy = busy;
352         firePropertyChange(PROPERTY_BUSY, null, busy);
353     }
354 
355     @Override
356     public boolean isHideBody() {
357         return hideBody;
358     }
359 
360     @Override
361     public void setHideBody(boolean hideBody) {
362         this.hideBody = hideBody;
363         firePropertyChange(PROPERTY_HIDE_BODY, null, hideBody);
364     }
365 
366     public final AuthenticationInfo getAuthenticationInfo() {
367         return authenticationInfo;
368     }
369 
370     public void setAuthenticationInfo(AuthenticationInfo authenticationInfo) {
371         this.authenticationInfo = authenticationInfo;
372     }
373 
374     public boolean isAuthenticated() {
375         return authenticated;
376     }
377 
378     public void setAuthenticated(boolean authenticated) {
379         boolean oldValue = isAuthenticated();
380         this.authenticated = authenticated;
381         firePropertyChange(PROPERTY_AUTHENTICATED, oldValue, authenticated);
382     }
383 
384     /*
385      * Synchronization
386      */
387     public boolean isSynchroEnabled() {
388         return isAuthenticated() && isPersistenceLoaded() && getConfiguration().isSynchronizationEnabled();
389     }
390 
391     public SynchroUIContext getSynchroContext() {
392         return synchroUIContext;
393     }
394 
395     public SynchroUIHandler getSynchroHandler() {
396         return synchroUIHandler;
397     }
398 
399     public void setSynchroUI(SynchroUI synchroUI) {
400         this.synchroUIHandler = synchroUI.getHandler();
401         this.synchroUIContext = synchroUI.getModel();
402         if (synchroUIContext != null) {
403             synchroUIContext.addPropertyChangeListener(synchroRunningListener);
404         }
405     }
406 
407     public boolean isSynchroRunning() {
408         return synchroRunning;
409     }
410 
411     public void setSynchroRunning(boolean synchroRunning) {
412         this.synchroRunning = synchroRunning;
413         firePropertyChange(PROPERTY_SYNCHRO_RUNNING, null, synchroRunning);
414     }
415 
416     public void setSwingSession(SwingSession swingSession) {
417         this.swingSession = swingSession;
418     }
419 
420     public void initSwingSession(MainUI ui) {
421         getSwingSession().add(ui);
422         // Workaround to avoid component duplication in xml file
423         getSwingSession().addUnsavedRootComponentByName(ui.getName() + "/JRootPane");
424         getSwingSession().addUnsavedRootComponentByName(ui.getName() + "/JXLayer");
425         saveSwingSession(null);
426     }
427 
428     public SwingSession getSwingSession() {
429         Preconditions.checkNotNull(swingSession, "SwingSession has not been initialized !");
430         return swingSession;
431     }
432 
433     public void saveSwingSession(Component componentToRemove) {
434         // Save actual registered components into file
435         try {
436             getSwingSession().setPartialSave(false);
437             getSwingSession().save();
438             if (LOG.isDebugEnabled()) LOG.debug("swing session saved");
439         } catch (IOException ex) {
440             LOG.error(ex.getLocalizedMessage());
441         }
442 
443         // Remove this component from registered components
444         if (componentToRemove != null) {
445             getSwingSession().remove(componentToRemove);
446         }
447     }
448 
449     public void saveComponentInSwingSession(Component componentToSave, String stateContext) {
450 
451         saveComponentInSwingSessionExecutor.execute(new Runnable() {
452             @Override
453             public void run() {
454                 // update specified component with specified state context (can be null)
455                 ApplicationUIContext.this.getSwingSession().updateState(componentToSave, stateContext);
456 
457                 // set partial save to avoid update states on all component
458                 ApplicationUIContext.this.getSwingSession().setPartialSave(true);
459 
460                 // Save actual registered components into file
461                 try {
462                     ApplicationUIContext.this.getSwingSession().save();
463                     if (LOG.isDebugEnabled()) LOG.debug("swing session saved (by worker)");
464                 } catch (IOException ex) {
465                     LOG.error(ex.getLocalizedMessage());
466                 }
467 
468                 ApplicationUIContext.this.getSwingSession().setPartialSave(false);
469             }
470         });
471     }
472 
473     public void restoreComponentFromSwingSession(Component component) {
474         getSwingSession().restoreState(component);
475     }
476 
477     public abstract ApplicationUI<?, ?> getApplicationUI(Screen screen);
478 
479     public abstract String getSelectedScreenTitle();
480 
481     public abstract boolean isAuthenticatedAsLocalUser();
482 
483     public abstract void reloadDbCache(ProgressionModel progressionModel);
484 
485     public abstract String getAuthenticationLabel();
486 
487     public abstract String getAuthenticationToolTipText();
488 
489     public abstract void openPersistenceService(boolean isAfterImportDb);
490 
491     public abstract void closePersistenceService();
492 
493     public abstract void closePersistenceService(boolean compact, boolean keepAuthentication);
494 
495     public abstract void checkDbContext(ProgressionModel progressionModel);
496 
497     public boolean checkUpdateReachable(String url, boolean showErrorInPopup) {
498         boolean result = true;
499 
500         try {
501             ApplicationUIUtil.tryToConnectToUpdateUrl(
502                     url,
503                     n("quadrige2.error.update.bad.url.syntax"),
504                     n("quadrige2.error.update.could.not.reach.url"),
505                     n("quadrige2.error.update.could.not.find.url")
506             );
507         } catch (ApplicationBusinessException e) {
508             if (showErrorInPopup) {
509                 getErrorHelper().showWarningDialog(e.getMessage());
510             } else {
511                 showInformationMessage(e.getMessage());
512             }
513             result = false;
514         }
515         return result;
516     }
517 
518     public abstract void clearDbContext();
519 
520     /**
521      * Initialisation method
522      * @param i18nBundleName
523      */
524     public void init(String i18nBundleName) {
525 
526         // converters are stored in current classloader, we need then to rescan them
527         // each time we change current classloader
528         ConverterUtil.deregister();
529         ConverterUtil.initConverters();
530 
531         // Add an OS shutdown hook
532         addShutdownHook();
533 
534         // --------------------------------------------------------------------//
535         // init i18n
536         // --------------------------------------------------------------------//
537         File i18nDirectory = getConfiguration().getI18nDirectory();
538         if (!getConfiguration().isFullLaunchMode()) {
539 
540             i18nDirectory = new File(getConfiguration().getDataDirectory(), "i18n");
541 
542             if (i18nDirectory.exists()) {
543                 // clean i18n cache
544                 ApplicationIOUtil.cleanDirectory(i18nDirectory, t("quadrige2.i18n.deleteCache.error", i18nDirectory));
545             }
546         }
547 
548         ApplicationIOUtil.forceMkdir(i18nDirectory, t("quadrige2.i18n.mkDir.error", i18nDirectory));
549 
550         if (LOG.isDebugEnabled()) {
551             LOG.debug("I18N directory: " + i18nDirectory);
552         }
553 
554         locale = getConfiguration().getI18nLocale();
555         Locale.setDefault(locale);
556         JComponent.setDefaultLocale(locale);
557 
558         if (LOG.isInfoEnabled()) {
559             LOG.info(String.format("Starts i18n with locale [%s] at [%s]", locale, i18nDirectory));
560         }
561         I18n.init(new UserI18nInitializer(i18nDirectory, new DefaultI18nInitializer(i18nBundleName + "-i18n")), locale);
562 
563         //--------------------------------------------------------------------//
564         // init lock
565         //--------------------------------------------------------------------//
566         File lockFile = getConfiguration().getLockFile();
567 
568         if (lockFile.exists()) {
569             // try to delete it
570             try {
571                 FileUtils.forceDelete(lockFile);
572             } catch (IOException e) {
573                 throw new Quadrige2BusinessException(t("quadrige2.error.application.already.started", lockFile));
574             }
575         }
576 
577         lock = ApplicationIOUtil.newWriter(lockFile, "error");
578         try {
579             lock.write(new Date().toString());
580         } catch (IOException e) {
581             throw new Quadrige2TechnicalException("Could not create lock file", e);
582         }
583 
584         if (LOG.isDebugEnabled()) {
585             LOG.debug("Create lock file: " + lockFile);
586         }
587 
588         installActionUI(null);
589 
590         saveComponentInSwingSessionExecutor = ActionFactory.createThreadExecutor(true);
591 
592         // save back to config
593         save();
594 
595         // list when a property to save change to save the configuration
596         addPropertyChangeListener(new PropertyChangeListener() {
597             @Override
598             public void propertyChange(PropertyChangeEvent evt) {
599                 if (PROPERTIES_TO_SAVE.contains(evt.getPropertyName())) {
600                     ApplicationUIContext.this.save();
601                 }
602             }
603         });
604 
605     }
606 
607     @Override
608     public void close() {
609 
610         // Don't try to close if already closed
611         if (isClosed()) {
612             return;
613         }
614         if (LOG.isInfoEnabled()) {
615             LOG.info("Closing application ...");
616         }
617 
618         try {
619             // Clear data references
620 //            messageNotifiers.clear();
621 
622             // Close data context
623             closePersistenceService();
624 
625             setScreen(null);
626 
627             // remove listeners
628             PropertyChangeListener[] listeners = getPropertyChangeListeners();
629             for (PropertyChangeListener listener : listeners) {
630                 if (listener instanceof PropertyChangeListenerProxy) {
631                     PropertyChangeListenerProxy proxy = (PropertyChangeListenerProxy) listener;
632                     listener = proxy.getListener();
633                 }
634                 if (LOG.isDebugEnabled()) {
635                     LOG.debug("Remove listener: " + listener);
636                 }
637                 removePropertyChangeListener(listener);
638             }
639             setMainUI(null);
640             if (actionUI != null) {
641 
642                 // close action ui
643                 actionUI.getModel().clear();
644             }
645             setActionUI(null);
646 
647         } finally {
648             closed = true;
649 
650             if (lock != null) {
651 
652                 // close lock stream
653                 ApplicationIOUtil.close(lock, "Unable to close lock stream");
654                 FileUtils.deleteQuietly(getConfiguration().getLockFile());
655                 if (LOG.isDebugEnabled()) {
656                     LOG.debug("Lock file released");
657                 }
658 
659             }
660 
661         }
662 
663         if (LOG.isInfoEnabled()) {
664             LOG.info("Application closed.");
665         }
666 
667     }
668 
669     public boolean isClosed() {
670         return closed;
671     }
672 
673     /**
674      * Add an OS shutdown hook, to close application on shutdown
675      */
676     private void addShutdownHook() {
677 
678         // Use shutdownHook to close context on System.exit
679         Runtime.getRuntime().addShutdownHook(new Thread(new Runnable() {
680             @Override
681             public void run() {
682                 close();
683             }
684         }));
685 
686         // Add DesktopPower to hook computer shutdown
687         DesktopPower desktopPower = Desktop.getDesktopPower();
688         if (desktopPower != null) {
689 
690             desktopPower.addListener(new DesktopPower.Listener() {
691                 @Override
692                 public void quit() {
693                     close();
694                 }
695             });
696         }
697 
698     }
699 
700     /**
701      * Save all context properties.<p/>
702      * Will typically call config.save()
703      */
704     protected void save() {
705         if (LOG.isDebugEnabled()) {
706             StringBuilder info = new StringBuilder("Save config (");
707             for (String property : PROPERTIES_TO_SAVE) {
708                 info.append(property).append(": ").append(JavaBeanObjectUtil.getProperty(this, property)).append(", ");
709             }
710             info = new StringBuilder(info.substring(0, info.lastIndexOf(", ")));
711             info.append(")");
712             LOG.debug(info.toString());
713         }
714         getConfiguration().save();
715     }
716 
717 }