View Javadoc
1   package fr.ifremer.quadrige3.ui.swing;
2   
3   /*-
4    * #%L
5    * Quadrige3 Core :: Quadrige3 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.collect.Lists;
27  import com.google.common.collect.Sets;
28  import fr.ifremer.quadrige3.core.config.QuadrigeCoreConfiguration;
29  import fr.ifremer.quadrige3.core.dao.technical.Assert;
30  import fr.ifremer.quadrige3.core.exception.QuadrigeBusinessException;
31  import fr.ifremer.quadrige3.core.exception.QuadrigeTechnicalException;
32  import fr.ifremer.quadrige3.core.security.AuthenticationInfo;
33  import fr.ifremer.quadrige3.core.security.SecurityContextHelper;
34  import fr.ifremer.quadrige3.core.service.ClientServiceLocator;
35  import fr.ifremer.quadrige3.core.service.persistence.PersistenceService;
36  import fr.ifremer.quadrige3.synchro.vo.SynchroProgressionStatus;
37  import fr.ifremer.quadrige3.ui.swing.action.*;
38  import fr.ifremer.quadrige3.ui.swing.content.AbstractMainUIHandler;
39  import fr.ifremer.quadrige3.ui.swing.content.MainUI;
40  import fr.ifremer.quadrige3.ui.swing.desktop.Desktop;
41  import fr.ifremer.quadrige3.ui.swing.desktop.DesktopPower;
42  import fr.ifremer.quadrige3.ui.swing.model.ProgressionUIModel;
43  import fr.ifremer.quadrige3.ui.swing.synchro.SynchroUI;
44  import fr.ifremer.quadrige3.ui.swing.synchro.SynchroUIContext;
45  import fr.ifremer.quadrige3.ui.swing.synchro.SynchroUIHandler;
46  import org.apache.commons.io.FileUtils;
47  import org.apache.commons.logging.Log;
48  import org.apache.commons.logging.LogFactory;
49  import org.jdesktop.beans.AbstractBean;
50  import org.nuiton.converter.ConverterUtil;
51  import org.nuiton.i18n.I18n;
52  import org.nuiton.i18n.init.DefaultI18nInitializer;
53  import org.nuiton.i18n.init.UserI18nInitializer;
54  import org.nuiton.jaxx.application.ApplicationBusinessException;
55  import org.nuiton.jaxx.application.ApplicationIOUtil;
56  import org.nuiton.jaxx.application.bean.JavaBeanObjectUtil;
57  import org.nuiton.jaxx.application.swing.action.ApplicationActionEngine;
58  import org.nuiton.jaxx.application.swing.action.ApplicationUIAction;
59  import org.nuiton.jaxx.application.swing.util.ApplicationErrorHelper;
60  
61  import javax.swing.JComponent;
62  import javax.swing.JFrame;
63  import java.awt.Component;
64  import java.beans.PropertyChangeListener;
65  import java.beans.PropertyChangeListenerProxy;
66  import java.io.Closeable;
67  import java.io.File;
68  import java.io.IOException;
69  import java.io.Writer;
70  import java.util.*;
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 final QuadrigeCoreConfiguration configuration;
105     /**
106      * session used to save ui states.
107      */
108     private SwingSession swingSession;
109     private 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     private final Map<Class<? extends AbstractAction<?, ?, ?>>, Boolean> actionInProgress = new LinkedHashMap<>();
122 
123     /**
124      * Dialog helper
125      */
126     private DialogHelper dialogHelper;
127     /**
128      * Current screen displayed in ui.
129      */
130     private Screen screen;
131     private LinkedList<Screen> screenBreadcrumb;
132     /**
133      * Current locale used in application.
134      */
135     private Locale locale;
136     /**
137      * Flag to know if there is an existing db.
138      */
139     private boolean dbExist;
140     /**
141      * Flag to know if the database has been imported or installed
142      */
143     private boolean dbJustInstalled;
144     /**
145      * Flag to know if the database has been imported from file
146      */
147     private boolean dbJustImportedFromFile;
148     /**
149      * Flag to know if there is a loaded db.
150      */
151     private boolean persistenceLoaded;
152     /**
153      * Busy state ({@code true} when a blocking action is running).
154      */
155     private boolean busy;
156     /**
157      * Flag to hide (or not) the body of application.
158      */
159     private boolean hideBody;
160     /**
161      * Flag to know if context was already closed. <br/>
162      * Can happen when you close nicely the application, the shutdown hook must then not close a second time this context.
163      */
164     private boolean closed;
165     /**
166      * 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/>
167      * Now use a Writer to keep handle during application life.
168      */
169     private Writer lock;
170 
171     /*
172      * Synchronization Context
173      */
174     private SynchroUIContext synchroUIContext;
175     private SynchroUIHandler synchroUIHandler;
176     private boolean synchroRunning;
177 
178     /**
179      * To keep last authentication credential in memory.
180      */
181     private AuthenticationInfo authenticationInfo;
182     /**
183      * To provide authentication information in status bar
184      */
185     private boolean authenticated;
186 
187     protected ApplicationUIContext(QuadrigeCoreConfiguration configuration) {
188         this.configuration = configuration;
189         instance = this;
190     }
191 
192     public static ApplicationUIContext getInstance() {
193         return instance;
194     }
195 
196     public static void setInstance(ApplicationUIContext applicationUIContext) {
197         instance = applicationUIContext;
198     }
199 
200     @Override
201     public QuadrigeCoreConfiguration getConfiguration() {
202         return configuration;
203     }
204 
205     @Override
206     public MainUI getMainUI() {
207         return mainUI;
208     }
209 
210     public void setMainUI(MainUI mainUI) {
211         this.mainUI = mainUI;
212     }
213 
214     public void installActionUI(JFrame frame) {
215         setActionUI(new ActionUI(frame, this));
216     }
217 
218     @Override
219     public ActionFactory getActionFactory() {
220         if (actionFactory == null) {
221             actionFactory = new ActionFactory();
222         }
223         return actionFactory;
224     }
225 
226     @Override
227     public ApplicationActionEngine getActionEngine() {
228         if (actionEngine == null) {
229             actionEngine = new ApplicationActionEngine(getActionFactory());
230         }
231         return actionEngine;
232     }
233 
234     @Override
235     public final boolean isActionInProgress(ApplicationUIAction applicationUIAction) {
236         return false; // nothing here
237     }
238 
239     @Override
240     public final void setActionInProgress(ApplicationUIAction applicationUIAction, boolean b) {
241         // nothing here
242     }
243 
244     public boolean isActionInProgress(Class<? extends AbstractAction<?, ?, ?>> actionClass) {
245         return actionInProgress.getOrDefault(actionClass, false);
246     }
247 
248     public void setActionInProgress(Class<? extends AbstractAction<?, ?, ?>> actionClass, boolean progress) {
249         if (progress && isActionInProgress(actionClass)) {
250             LOG.warn(String.format("Action '%s' is already marked as running", actionClass.getName()));
251         }
252         actionInProgress.put(actionClass, progress);
253     }
254 
255     public DialogHelper getDialogHelper() {
256         if (dialogHelper == null) {
257             dialogHelper = new DialogHelper(this);
258         }
259         return dialogHelper;
260     }
261 
262     @Override
263     public ApplicationErrorHelper getErrorHelper() {
264         return getDialogHelper();
265     }
266 
267     // DB methods
268 
269     public boolean isDbExist() {
270         return dbExist;
271     }
272 
273     public void setDbExist(boolean dbExist) {
274         this.dbExist = dbExist;
275         firePropertyChange(PROPERTY_DB_EXIST, null, dbExist);
276     }
277 
278     public boolean isDbJustInstalled() {
279         return dbJustInstalled;
280     }
281 
282     public void setDbJustInstalled(boolean dbJustInstalled) {
283         this.dbJustInstalled = dbJustInstalled;
284     }
285 
286     public boolean isDbJustImportedFromFile() {
287         return dbJustImportedFromFile;
288     }
289 
290     public void setDbJustImportedFromFile(boolean dbJustImportedFromFile) {
291         this.dbJustImportedFromFile = dbJustImportedFromFile;
292     }
293 
294     public boolean isPersistenceLoaded() {
295         return persistenceLoaded;
296     }
297 
298     public void setPersistenceLoaded(boolean persistenceLoaded) {
299         this.persistenceLoaded = persistenceLoaded;
300         firePropertyChange(PROPERTY_DB_LOADED, null, persistenceLoaded);
301     }
302 
303     // UI methods
304 
305     @Override
306     public ActionUI getActionUI() {
307         return actionUI;
308     }
309 
310     public void setActionUI(ActionUI actionUI) {
311         this.actionUI = actionUI;
312     }
313 
314     public Screen getScreen() {
315         return screen;
316     }
317 
318     public void setScreen(Screen screen) {
319         this.screen = screen;
320         firePropertyChange(PROPERTY_SCREEN, null, screen);
321     }
322 
323     public LinkedList<Screen> getScreenBreadcrumb() {
324         if (screenBreadcrumb == null) {
325             screenBreadcrumb = Lists.newLinkedList();
326         }
327         return screenBreadcrumb;
328     }
329 
330     public void setFallBackScreen() {
331         if (isPersistenceLoaded()) {
332             setScreen(Screen.HOME);
333         } else {
334             setScreen(Screen.MANAGE_DB);
335         }
336     }
337 
338     public Locale getLocale() {
339         return locale;
340     }
341 
342     public void setLocale(Locale locale) {
343         this.locale = locale;
344 
345         // change i18n locale
346         configuration.setI18nLocale(locale);
347         I18n.setDefaultLocale(locale);
348         Locale.setDefault(locale);
349         JComponent.setDefaultLocale(locale);
350 
351         firePropertyChange(PROPERTY_LOCALE, null, locale);
352     }
353 
354     public boolean acceptLocale(String expected) {
355         return getLocale() != null && getLocale().toString().equals(expected);
356     }
357 
358     @Override
359     public boolean isBusy() {
360         return busy;
361     }
362 
363     @Override
364     public void setBusy(boolean busy) {
365         this.busy = busy;
366         firePropertyChange(PROPERTY_BUSY, null, busy);
367     }
368 
369     @Override
370     public boolean isHideBody() {
371         return hideBody;
372     }
373 
374     @Override
375     public void setHideBody(boolean hideBody) {
376         this.hideBody = hideBody;
377         firePropertyChange(PROPERTY_HIDE_BODY, null, hideBody);
378     }
379 
380     public final AuthenticationInfo getAuthenticationInfo() {
381         return authenticationInfo;
382     }
383 
384     public void setAuthenticationInfo(AuthenticationInfo authenticationInfo) {
385         this.authenticationInfo = authenticationInfo;
386     }
387 
388     public boolean isAuthenticated() {
389         return authenticated;
390     }
391 
392     public void setAuthenticated(boolean authenticated) {
393         this.authenticated = authenticated;
394         firePropertyChange(PROPERTY_AUTHENTICATED, null, authenticated);
395     }
396 
397     /**
398      * Is authentication properties was filled, try to authenticate against the DB.<p/>
399      * Then update the authentication status
400      *
401      * @return a boolean.
402      */
403     public boolean tryReAuthenticate() {
404 
405         boolean result = false;
406 
407         // check authentication
408         if (getAuthenticationInfo() != null) {
409             result = SecurityContextHelper.authenticate(getAuthenticationInfo().getLogin(), getAuthenticationInfo().getPassword());
410 
411             // Set authenticated property
412             setAuthenticated(result);
413         }
414 
415         return result;
416     }
417 
418     /*
419      * Synchronization
420      */
421     public boolean isSynchroEnabled() {
422         return isAuthenticated() && isPersistenceLoaded() && getConfiguration().isSynchronizationEnabled();
423     }
424 
425     public SynchroUIContext getSynchroContext() {
426         return synchroUIContext;
427     }
428 
429     public SynchroUIHandler getSynchroHandler() {
430         return synchroUIHandler;
431     }
432 
433     public void setSynchroUI(SynchroUI synchroUI) {
434         this.synchroUIHandler = synchroUI.getHandler();
435         this.synchroUIContext = synchroUI.getModel();
436         if (synchroUIContext != null) {
437             synchroUIContext.addPropertyChangeListener(evt -> {
438                 if (SynchroUIContext.PROPERTY_PROGRESSION_STATUS.equals(evt.getPropertyName())) {
439                     ApplicationUIContext.this.setSynchroRunning(!SynchroProgressionStatus.NOT_STARTED.equals(evt.getNewValue()));
440                 }
441             });
442         }
443     }
444 
445     public boolean isSynchroRunning() {
446         return synchroRunning;
447     }
448 
449     public void setSynchroRunning(boolean synchroRunning) {
450         this.synchroRunning = synchroRunning;
451         firePropertyChange(PROPERTY_SYNCHRO_RUNNING, null, synchroRunning);
452     }
453 
454     public void setSwingSession(SwingSession swingSession) {
455         this.swingSession = swingSession;
456     }
457 
458     public void initSwingSession(MainUI ui) {
459         getSwingSession().add(ui);
460         // Workaround to avoid component duplication in xml file
461         getSwingSession().addUnsavedRootComponentByName(ui.getName() + "/JRootPane");
462         getSwingSession().addUnsavedRootComponentByName(ui.getName() + "/JXLayer");
463         saveSwingSession(null);
464     }
465 
466     public SwingSession getSwingSession() {
467         Assert.notNull(swingSession, "SwingSession has not been initialized !");
468         return swingSession;
469     }
470 
471     public void saveSwingSession(Component componentToRemove) {
472         // Save actual registered components into file
473         try {
474             getSwingSession().setPartialSave(false);
475             getSwingSession().save();
476             if (LOG.isDebugEnabled()) LOG.debug("swing session saved");
477         } catch (IOException ex) {
478             LOG.error(ex.getLocalizedMessage());
479         }
480 
481         // Remove this component from registered components
482         if (componentToRemove != null) {
483             getSwingSession().remove(componentToRemove);
484         }
485     }
486 
487     public void saveComponentInSwingSession(Component componentToSave, String stateContext) {
488 
489         saveComponentInSwingSessionExecutor.execute(() -> {
490 
491             // update specified component with specified state context (can be null)
492             ApplicationUIContext.this.getSwingSession().updateState(componentToSave, stateContext);
493 
494             // set partial save to avoid update states on all component
495             ApplicationUIContext.this.getSwingSession().setPartialSave(true);
496 
497             // Save actual registered components into file
498             try {
499                 ApplicationUIContext.this.getSwingSession().save();
500                 if (LOG.isDebugEnabled()) LOG.debug("swing session saved (by worker)");
501             } catch (IOException ex) {
502                 LOG.error(ex.getLocalizedMessage());
503             }
504 
505             ApplicationUIContext.this.getSwingSession().setPartialSave(false);
506         });
507     }
508 
509     public void restoreComponentFromSwingSession(Component component) {
510         getSwingSession().restoreState(component);
511     }
512 
513     public abstract ApplicationUI<?, ?> getApplicationUI(Screen screen);
514 
515     public abstract String getSelectedScreenTitle();
516 
517     public abstract boolean isAuthenticatedAsLocalUser();
518 
519     public abstract String getAuthenticationLabel();
520 
521     public abstract String getAuthenticationToolTipText();
522 
523     public abstract void openPersistenceService(boolean isAfterImportDb);
524 
525     public abstract void closePersistenceService();
526 
527     public abstract void closePersistenceService(boolean compact, boolean keepAuthentication);
528 
529     public abstract void checkDbContext(ProgressionUIModel progressionModel);
530 
531     public boolean checkUpdateReachable(String url, boolean showErrorInPopup) {
532         boolean result = true;
533 
534         try {
535             ApplicationUIUtil.tryToConnectToUpdateUrl(
536                     url,
537                     n("quadrige3.error.update.bad.url.syntax"),
538                     n("quadrige3.error.update.could.not.reach.url"),
539                     n("quadrige3.error.update.could.not.find.url")
540             );
541         } catch (ApplicationBusinessException e) {
542             if (showErrorInPopup) {
543                 getErrorHelper().showWarningDialog(e.getMessage());
544             } else {
545                 showInformationMessage(e.getMessage());
546             }
547             result = false;
548         }
549         return result;
550     }
551 
552     /**
553      * Execute update actions and return true if restart is needed
554      *
555      * @return true if restart needed
556      */
557     protected boolean doUpdates() {
558 
559         boolean needRestart = false;
560 
561         if (checkUpdateReachable(getConfiguration().getUpdateApplicationUrl(), true)) {
562 
563             UpdateApplicationAction action = getActionFactory().createLogicAction(new DummyMainUIHandler(this), UpdateApplicationAction.class);
564             action.setSilent(true);
565             getActionEngine().runActionAndWait(action);
566 
567             needRestart = action.isReload();
568         }
569 
570         if (checkUpdateReachable(getConfiguration().getUpdateDataUrl(), true)) {
571 
572             UpdateDataAction action = getActionFactory().createLogicAction(new DummyMainUIHandler(this), UpdateDataAction.class);
573             action.setSilent(true);
574             getActionEngine().runActionAndWait(action);
575 
576             needRestart |= action.isReload();
577         }
578 
579         return needRestart;
580     }
581 
582     public abstract void clearDbContext();
583 
584     /**
585      * Initialisation method
586      * @param i18nBundleName i18n bundle name
587      */
588     public void init(String i18nBundleName) {
589 
590         // converters are stored in current classloader, we need then to rescan them
591         // each time we change current classloader
592         ConverterUtil.deregister();
593         ConverterUtil.initConverters();
594 
595         // Add an OS shutdown hook
596         addShutdownHook();
597 
598         // --------------------------------------------------------------------//
599         // init i18n
600         // --------------------------------------------------------------------//
601         File i18nDirectory = getConfiguration().getI18nDirectory();
602         if (!getConfiguration().isFullLaunchMode()) {
603 
604             i18nDirectory = new File(getConfiguration().getDataDirectory(), "i18n");
605 
606             if (i18nDirectory.exists()) {
607                 // clean i18n cache
608                 ApplicationIOUtil.cleanDirectory(i18nDirectory, String.format("Failed to delete translation cache at %s", i18nDirectory));
609             }
610         }
611 
612         ApplicationIOUtil.forceMkdir(i18nDirectory, String.format("Failed to create translation directory at %s", i18nDirectory));
613 
614         if (LOG.isDebugEnabled()) {
615             LOG.debug("I18N directory: " + i18nDirectory);
616         }
617 
618         locale = getConfiguration().getI18nLocale();
619         Locale.setDefault(locale);
620         JComponent.setDefaultLocale(locale);
621 
622         if (LOG.isInfoEnabled()) {
623             LOG.info(String.format("Starts i18n with locale [%s] at [%s]", locale, i18nDirectory));
624         }
625         I18n.init(new UserI18nInitializer(i18nDirectory, new DefaultI18nInitializer(i18nBundleName + "-i18n")), locale);
626 
627         //--------------------------------------------------------------------//
628         // init lock
629         //--------------------------------------------------------------------//
630         File lockFile = getConfiguration().getLockFile();
631 
632         if (lockFile.exists()) {
633             // try to delete it
634             try {
635                 FileUtils.forceDelete(lockFile);
636             } catch (IOException e) {
637                 throw new QuadrigeBusinessException(t("quadrige3.error.application.already.started", lockFile));
638             }
639         }
640 
641         lock = ApplicationIOUtil.newWriter(lockFile, "error");
642         try {
643             lock.write(new Date().toString());
644         } catch (IOException e) {
645             throw new QuadrigeTechnicalException("Could not create lock file", e);
646         }
647 
648         if (LOG.isDebugEnabled()) {
649             LOG.debug("Create lock file: " + lockFile);
650         }
651 
652         installActionUI(null);
653 
654         saveComponentInSwingSessionExecutor = ActionFactory.createSingleThreadExecutor(ActionFactory.ExecutionMode.CUMULATIVE);
655 
656         // save back to config
657         save();
658 
659         // list when a property to save change to save the configuration
660         addPropertyChangeListener(evt -> {
661             if (PROPERTIES_TO_SAVE.contains(evt.getPropertyName())) {
662                 ApplicationUIContext.this.save();
663             }
664         });
665 
666     }
667 
668     public void reloadDbCache(ProgressionUIModel progressionModel) {
669         if (isPersistenceLoaded() && !isDbJustInstalled()) {
670 
671             // save previous progression model state
672             int total = progressionModel.getTotal();
673             int current = progressionModel.getCurrent();
674             String message = progressionModel.getMessage();
675 
676             // Clear first
677             clearCaches();
678 
679             // reload all needed caches
680             PersistenceService persistenceService = ClientServiceLocator.instance().getPersistenceService();
681             persistenceService.loadDefaultCaches(progressionModel);
682 
683             // restore progression model
684             progressionModel.setTotal(total);
685             progressionModel.setCurrent(current);
686             progressionModel.setMessage(message);
687         }
688     }
689 
690     public void clearCaches() {
691         if (isPersistenceLoaded()) {
692             PersistenceService persistenceService = ClientServiceLocator.instance().getPersistenceService();
693             persistenceService.clearAllCaches();
694         }
695     }
696 
697     @Override
698     public void close() {
699 
700         // Don't try to close if already closed
701         if (isClosed()) {
702             return;
703         }
704         if (LOG.isInfoEnabled()) {
705             LOG.info("Closing application ...");
706         }
707 
708         try {
709             // Clear data references
710 //            messageNotifiers.clear();
711 
712             // Close data context
713             closePersistenceService();
714 
715             setScreen(null);
716 
717             // remove listeners
718             PropertyChangeListener[] listeners = getPropertyChangeListeners();
719             for (PropertyChangeListener listener : listeners) {
720                 if (listener instanceof PropertyChangeListenerProxy) {
721                     PropertyChangeListenerProxy proxy = (PropertyChangeListenerProxy) listener;
722                     listener = proxy.getListener();
723                 }
724                 if (LOG.isDebugEnabled()) {
725                     LOG.debug("Remove listener: " + listener);
726                 }
727                 removePropertyChangeListener(listener);
728             }
729             setMainUI(null);
730             // Code removed because it can blocks the closing process (Mantis #42313)
731 //            if (actionUI != null) {
732 //
733 //                // close action ui
734 //                actionUI.getModel().clear();
735 //            }
736             setActionUI(null);
737 
738         } finally {
739             closed = true;
740 
741             if (lock != null) {
742 
743                 // close lock stream
744                 ApplicationIOUtil.close(lock, "Unable to close lock stream");
745                 FileUtils.deleteQuietly(getConfiguration().getLockFile());
746                 if (LOG.isDebugEnabled()) {
747                     LOG.debug("Lock file released");
748                 }
749 
750             }
751 
752         }
753 
754         if (LOG.isInfoEnabled()) {
755             LOG.info("Application closed.");
756         }
757 
758     }
759 
760     public boolean isClosed() {
761         return closed;
762     }
763 
764     /**
765      * Add an OS shutdown hook, to close application on shutdown
766      */
767     private void addShutdownHook() {
768 
769         // Use shutdownHook to close context on System.exit
770         Runtime.getRuntime().addShutdownHook(new Thread(this::close));
771 
772         // Add DesktopPower to hook computer shutdown
773         DesktopPower desktopPower = Desktop.getDesktopPower();
774         if (desktopPower != null) {
775 
776             desktopPower.addListener(this::close);
777         }
778 
779     }
780 
781     /**
782      * Save all context properties.<p/>
783      * Will typically call config.save()
784      */
785     protected void save() {
786         if (LOG.isDebugEnabled()) {
787             StringBuilder info = new StringBuilder("Save config (");
788             for (String property : PROPERTIES_TO_SAVE) {
789                 info.append(property).append(": ").append(JavaBeanObjectUtil.getProperty(this, property)).append(", ");
790             }
791             info = new StringBuilder(info.substring(0, info.lastIndexOf(", ")));
792             info.append(")");
793             LOG.debug(info.toString());
794         }
795         getConfiguration().save();
796     }
797 
798     private class DummyMainUIHandler extends AbstractMainUIHandler {
799 
800         private final ApplicationUIContext context;
801 
802         DummyMainUIHandler(ApplicationUIContext context) {
803             this.context = context;
804         }
805 
806         @Override
807         public ApplicationUIContext getContext() {
808             return context;
809         }
810 
811     }
812 }