AbstractJavaFXGriffonApplication.java
001 /*
002  * SPDX-License-Identifier: Apache-2.0
003  *
004  * Copyright 2008-2017 the original author or authors.
005  *
006  * Licensed under the Apache License, Version 2.0 (the "License");
007  * you may not use this file except in compliance with the License.
008  * You may obtain a copy of the License at
009  *
010  *     http://www.apache.org/licenses/LICENSE-2.0
011  *
012  * Unless required by applicable law or agreed to in writing, software
013  * distributed under the License is distributed on an "AS IS" BASIS,
014  * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
015  * See the License for the specific language governing permissions and
016  * limitations under the License.
017  */
018 package griffon.javafx;
019 
020 import griffon.core.ApplicationClassLoader;
021 import griffon.core.ApplicationConfigurer;
022 import griffon.core.ApplicationEvent;
023 import griffon.core.Configuration;
024 import griffon.core.Context;
025 import griffon.core.ExecutorServiceManager;
026 import griffon.core.GriffonApplication;
027 import griffon.core.ShutdownHandler;
028 import griffon.core.addon.AddonManager;
029 import griffon.core.addon.GriffonAddon;
030 import griffon.core.artifact.ArtifactManager;
031 import griffon.core.configuration.ConfigurationManager;
032 import griffon.core.controller.ActionManager;
033 import griffon.core.env.ApplicationPhase;
034 import griffon.core.env.Lifecycle;
035 import griffon.core.event.EventRouter;
036 import griffon.core.i18n.MessageSource;
037 import griffon.core.injection.Injector;
038 import griffon.core.mvc.MVCGroupManager;
039 import griffon.core.resources.ResourceHandler;
040 import griffon.core.resources.ResourceInjector;
041 import griffon.core.resources.ResourceResolver;
042 import griffon.core.threading.UIThreadManager;
043 import griffon.core.view.WindowManager;
044 import javafx.application.Application;
045 import javafx.application.Platform;
046 import javafx.stage.Stage;
047 import org.codehaus.griffon.runtime.core.MVCGroupExceptionHandler;
048 import org.slf4j.Logger;
049 import org.slf4j.LoggerFactory;
050 
051 import javax.annotation.Nonnull;
052 import javax.annotation.Nullable;
053 import java.beans.PropertyChangeEvent;
054 import java.beans.PropertyChangeListener;
055 import java.beans.PropertyChangeSupport;
056 import java.util.ArrayList;
057 import java.util.Arrays;
058 import java.util.List;
059 import java.util.Locale;
060 import java.util.Map;
061 import java.util.concurrent.CountDownLatch;
062 
063 import static griffon.util.AnnotationUtils.named;
064 import static griffon.util.GriffonApplicationUtils.parseLocale;
065 import static griffon.util.GriffonNameUtils.requireNonBlank;
066 import static java.util.Arrays.asList;
067 import static java.util.Objects.requireNonNull;
068 
069 /**
070  * Base implementation of {@code GriffonApplication} that runs in applet mode.
071  *
072  @author Andres Almiray
073  @since 2.0.0
074  */
075 public abstract class AbstractJavaFXGriffonApplication extends Application implements GriffonApplication {
076     public static final String[] EMPTY_ARGS = new String[0];
077     private static final String ERROR_SHUTDOWN_HANDLER_NULL = "Argument 'shutdownHandler' must not be null";
078     protected final Object[] lock = new Object[0];
079     protected final PropertyChangeSupport pcs;
080     private final List<ShutdownHandler> shutdownHandlers = new ArrayList<>();
081     private final Object shutdownLock = new Object();
082     private final Logger log;
083     private Locale locale = Locale.getDefault();
084     private ApplicationPhase phase = ApplicationPhase.INITIALIZE;
085     private String[] startupArgs;
086     private Injector<?> injector;
087 
088     public AbstractJavaFXGriffonApplication() {
089         this(EMPTY_ARGS);
090     }
091 
092     public AbstractJavaFXGriffonApplication(@Nonnull String[] args) {
093         requireNonNull(args, "Argument 'args' must not be null");
094         pcs = new PropertyChangeSupport(this);
095         startupArgs = Arrays.copyOf(args, args.length);
096         log = LoggerFactory.getLogger(getClass());
097     }
098 
099     // ------------------------------------------------------
100 
101     @Override
102     public void start(Stage stagethrows Exception {
103         stage.setOnHidden(t -> shutdown());
104     }
105 
106     @Override
107     public void stop() throws Exception {
108         shutdown();
109     }
110 
111     public void addPropertyChangeListener(@Nullable PropertyChangeListener listener) {
112         pcs.addPropertyChangeListener(listener);
113     }
114 
115     public void addPropertyChangeListener(@Nullable String propertyName, @Nullable PropertyChangeListener listener) {
116         pcs.addPropertyChangeListener(propertyName, listener);
117     }
118 
119     public void removePropertyChangeListener(@Nullable PropertyChangeListener listener) {
120         pcs.removePropertyChangeListener(listener);
121     }
122 
123     public void removePropertyChangeListener(@Nullable String propertyName, @Nullable PropertyChangeListener listener) {
124         pcs.removePropertyChangeListener(propertyName, listener);
125     }
126 
127     @Nonnull
128     public PropertyChangeListener[] getPropertyChangeListeners() {
129         return pcs.getPropertyChangeListeners();
130     }
131 
132     @Nonnull
133     public PropertyChangeListener[] getPropertyChangeListeners(@Nullable String propertyName) {
134         return pcs.getPropertyChangeListeners(propertyName);
135     }
136 
137     protected void firePropertyChange(@Nonnull PropertyChangeEvent event) {
138         pcs.firePropertyChange(requireNonNull(event, "Argument 'event' must not be null"));
139     }
140 
141     protected void firePropertyChange(@Nonnull String propertyName, @Nullable Object oldValue, @Nullable Object newValue) {
142         pcs.firePropertyChange(requireNonBlank(propertyName, "Argument 'propertyName' must not be blank"), oldValue, newValue);
143     }
144 
145     // ------------------------------------------------------
146 
147     @Nonnull
148     public Locale getLocale() {
149         return locale;
150     }
151 
152     public void setLocale(@Nonnull Locale locale) {
153         Locale oldValue = this.locale;
154         this.locale = locale;
155         Locale.setDefault(locale);
156         firePropertyChange(PROPERTY_LOCALE, oldValue, locale);
157     }
158 
159     @Nonnull
160     public String[] getStartupArgs() {
161         return startupArgs;
162     }
163 
164     @Nonnull
165     public Logger getLog() {
166         return log;
167     }
168 
169     public void setLocaleAsString(@Nullable String locale) {
170         setLocale(parseLocale(locale));
171     }
172 
173     public void addShutdownHandler(@Nonnull ShutdownHandler handler) {
174         requireNonNull(handler, ERROR_SHUTDOWN_HANDLER_NULL);
175         if (!shutdownHandlers.contains(handler)) shutdownHandlers.add(handler);
176     }
177 
178     public void removeShutdownHandler(@Nonnull ShutdownHandler handler) {
179         requireNonNull(handler, ERROR_SHUTDOWN_HANDLER_NULL);
180         shutdownHandlers.remove(handler);
181     }
182 
183     @Nonnull
184     public ApplicationPhase getPhase() {
185         synchronized (lock) {
186             return this.phase;
187         }
188     }
189 
190     protected void setPhase(@Nonnull ApplicationPhase phase) {
191         requireNonNull(phase, "Argument 'phase' must not be null");
192         synchronized (lock) {
193             firePropertyChange(PROPERTY_PHASE, this.phase, this.phase = phase);
194         }
195     }
196 
197     @Nonnull
198     @Override
199     public ApplicationClassLoader getApplicationClassLoader() {
200         return injector.getInstance(ApplicationClassLoader.class);
201     }
202 
203     @Nonnull
204     @Override
205     public Context getContext() {
206         return injector.getInstance(Context.class, named("applicationContext"));
207     }
208 
209     @Nonnull
210     @Override
211     public Configuration getConfiguration() {
212         return getConfigurationManager().getConfiguration();
213     }
214 
215     @Nonnull
216     @Override
217     public ConfigurationManager getConfigurationManager() {
218         return injector.getInstance(ConfigurationManager.class);
219     }
220 
221     @Nonnull
222     @Override
223     public UIThreadManager getUIThreadManager() {
224         return injector.getInstance(UIThreadManager.class);
225     }
226 
227     @Nonnull
228     @Override
229     public EventRouter getEventRouter() {
230         return injector.getInstance(EventRouter.class, named("applicationEventRouter"));
231     }
232 
233     @Nonnull
234     @Override
235     public ArtifactManager getArtifactManager() {
236         return injector.getInstance(ArtifactManager.class);
237     }
238 
239     @Nonnull
240     @Override
241     public ActionManager getActionManager() {
242         return injector.getInstance(ActionManager.class);
243     }
244 
245     @Nonnull
246     @Override
247     public AddonManager getAddonManager() {
248         return injector.getInstance(AddonManager.class);
249     }
250 
251     @Nonnull
252     @Override
253     public MVCGroupManager getMvcGroupManager() {
254         return injector.getInstance(MVCGroupManager.class);
255     }
256 
257     @Nonnull
258     @Override
259     public MessageSource getMessageSource() {
260         return injector.getInstance(MessageSource.class, named("applicationMessageSource"));
261     }
262 
263     @Nonnull
264     @Override
265     public ResourceResolver getResourceResolver() {
266         return injector.getInstance(ResourceResolver.class, named("applicationResourceResolver"));
267     }
268 
269     @Nonnull
270     @Override
271     public ResourceHandler getResourceHandler() {
272         return injector.getInstance(ResourceHandler.class);
273     }
274 
275     @Nonnull
276     @Override
277     public ResourceInjector getResourceInjector() {
278         return injector.getInstance(ResourceInjector.class, named("applicationResourceInjector"));
279     }
280 
281     @Nonnull
282     @Override
283     public Injector<?> getInjector() {
284         return injector;
285     }
286 
287     public void setInjector(@Nonnull Injector<?> injector) {
288         this.injector = requireNonNull(injector, "Argument 'injector' must not be null");
289         this.injector.injectMembers(this);
290         addShutdownHandler(getWindowManager());
291         MVCGroupExceptionHandler.registerWith(this);
292     }
293 
294     @Nonnull
295     @Override
296     @SuppressWarnings("unchecked")
297     public <W> WindowManager<W> getWindowManager() {
298         return injector.getInstance(WindowManager.class);
299     }
300 
301     protected ApplicationConfigurer getApplicationConfigurer() {
302         return injector.getInstance(ApplicationConfigurer.class);
303     }
304 
305     public void initialize() {
306         if (getPhase() == ApplicationPhase.INITIALIZE) {
307             Parameters parameters = getParameters();
308             if (parameters != null && !parameters.getRaw().isEmpty()) {
309                 int length = parameters.getRaw().size();
310                 startupArgs = new String[length];
311                 System.arraycopy(parameters.getRaw().toArray()0, startupArgs, 0, length);
312             }
313 
314             getApplicationConfigurer().init();
315         }
316     }
317 
318     public void ready() {
319         if (getPhase() != ApplicationPhase.STARTUPreturn;
320 
321         showStartingWindow();
322 
323         setPhase(ApplicationPhase.READY);
324         event(ApplicationEvent.READY_START, asList(this));
325 
326         getApplicationConfigurer().runLifecycleHandler(Lifecycle.READY);
327         event(ApplicationEvent.READY_END, asList(this));
328 
329         setPhase(ApplicationPhase.MAIN);
330     }
331 
332     protected void showStartingWindow() {
333         Object startingWindow = getWindowManager().getStartingWindow();
334         if (startingWindow != null) {
335             getWindowManager().show(startingWindow);
336         }
337     }
338 
339     public boolean canShutdown() {
340         event(ApplicationEvent.SHUTDOWN_REQUESTED, asList(this));
341         synchronized (shutdownLock) {
342             for (ShutdownHandler handler : shutdownHandlers) {
343                 if (!handler.canShutdown(this)) {
344                     event(ApplicationEvent.SHUTDOWN_ABORTED, asList(this));
345                     if (log.isDebugEnabled()) {
346                         try {
347                             log.debug("Shutdown aborted by " + handler);
348                         catch (UnsupportedOperationException uoe) {
349                             log.debug("Shutdown aborted by a handler");
350                         }
351                     }
352                     return false;
353                 }
354             }
355         }
356         return true;
357     }
358 
359     public boolean shutdown() {
360         boolean implicitExit = Platform.isImplicitExit();
361         Platform.setImplicitExit(false);
362         if (!doShutdown()) {
363             Platform.setImplicitExit(implicitExit);
364             return false;
365         }
366         return true;
367     }
368 
369     protected boolean doShutdown() {
370         // avoids reentrant calls to shutdown()
371         // once permission to quit has been granted
372         if (getPhase() == ApplicationPhase.SHUTDOWNreturn false;
373 
374         if (!canShutdown()) return false;
375         log.info("Shutdown is in process");
376 
377         // signal that shutdown is in process
378         setPhase(ApplicationPhase.SHUTDOWN);
379 
380         // stage 1 - alert all app event handlers
381         // wait for all handlers to complete before proceeding
382         // with stage #2 if and only if the current thread is
383         // the ui thread
384         log.debug("Shutdown stage 1: notify all event listeners");
385         if (getEventRouter().isEventPublishingEnabled()) {
386             final CountDownLatch latch = new CountDownLatch(getUIThreadManager().isUIThread() 0);
387             getEventRouter().addEventListener(ApplicationEvent.SHUTDOWN_START.getName()(Object... args-> latch.countDown());
388             event(ApplicationEvent.SHUTDOWN_START, asList(this));
389             try {
390                 latch.await();
391             catch (InterruptedException e) {
392                 // ignore
393             }
394         }
395 
396         // stage 2 - alert all shutdown handlers
397         log.debug("Shutdown stage 2: notify all shutdown handlers");
398         synchronized (shutdownLock) {
399             for (ShutdownHandler handler : shutdownHandlers) {
400                 handler.onShutdown(this);
401             }
402         }
403 
404         // stage 3 - destroy all mvc groups
405         log.debug("Shutdown stage 3: destroy all MVC groups");
406         List<String> mvcIds = new ArrayList<>();
407         mvcIds.addAll(getMvcGroupManager().getGroups().keySet());
408         for (String id : mvcIds) {
409             getMvcGroupManager().destroyMVCGroup(id);
410         }
411 
412         // stage 4 - call shutdown script
413         log.debug("Shutdown stage 4: execute Shutdown script");
414         getApplicationConfigurer().runLifecycleHandler(Lifecycle.SHUTDOWN);
415 
416         injector.getInstance(ExecutorServiceManager.class).shutdownAll();
417         injector.close();
418 
419         return true;
420     }
421 
422     @SuppressWarnings("unchecked")
423     public void startup() {
424         if (getPhase() != ApplicationPhase.INITIALIZEreturn;
425 
426         setPhase(ApplicationPhase.STARTUP);
427         event(ApplicationEvent.STARTUP_START, asList(this));
428 
429         Object startupGroups = getConfiguration().get("application.startupGroups"null);
430         if (startupGroups instanceof List) {
431             log.info("Initializing all startup groups: {}", startupGroups);
432 
433             for (String groupName : (List<String>startupGroups) {
434                 getMvcGroupManager().createMVC(groupName.trim());
435             }
436         else if (startupGroups != null && startupGroups.getClass().isArray()) {
437             Object[] groups = (Object[]) startupGroups;
438             log.info("Initializing all startup groups: {}", Arrays.toString(groups));
439 
440             for (Object groupName : groups) {
441                 getMvcGroupManager().createMVC(String.valueOf(groupName).trim());
442             }
443         else if (startupGroups != null && startupGroups instanceof CharSequence) {
444             String[] groups = (String.valueOf(startupGroups)).split(",");
445             log.info("Initializing all startup groups: {}", Arrays.toString(groups));
446 
447             for (String groupName : groups) {
448                 getMvcGroupManager().createMVC(groupName.trim());
449             }
450         }
451 
452         for (Map.Entry<String, GriffonAddon> e : getAddonManager().getAddons().entrySet()) {
453             List<String> groups = e.getValue().getStartupGroups();
454             if (groups.isEmpty()) {
455                 continue;
456             }
457             log.info("Initializing all {} startup groups: {}", e.getKey(), groups);
458             Map<String, Map<String, Object>> mvcGroups = e.getValue().getMvcGroups();
459             for (String groupName : groups) {
460                 if (mvcGroups.containsKey(groupName)) {
461                     getMvcGroupManager().createMVC(groupName.trim());
462                 }
463             }
464         }
465 
466         getApplicationConfigurer().runLifecycleHandler(Lifecycle.STARTUP);
467 
468         event(ApplicationEvent.STARTUP_END, asList(this));
469     }
470 
471     protected void event(@Nonnull ApplicationEvent event, @Nullable List<?> args) {
472         getEventRouter().publishEvent(event.getName(), args);
473     }
474 }