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