View Javadoc
1   /*
2    * *************************************************************************************************************************************************************
3    *
4    * SteelBlue: DCI User Interfaces
5    * http://tidalwave.it/projects/steelblue
6    *
7    * Copyright (C) 2015 - 2025 by Tidalwave s.a.s. (http://tidalwave.it)
8    *
9    * *************************************************************************************************************************************************************
10   *
11   * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License.
12   * You may obtain a copy of the License at
13   *
14   *     http://www.apache.org/licenses/LICENSE-2.0
15   *
16   * Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR
17   * CONDITIONS OF ANY KIND, either express or implied.  See the License for the specific language governing permissions and limitations under the License.
18   *
19   * *************************************************************************************************************************************************************
20   *
21   * git clone https://bitbucket.org/tidalwave/steelblue-src
22   * git clone https://github.com/tidalwave-it/steelblue-src
23   *
24   * *************************************************************************************************************************************************************
25   */
26  package it.tidalwave.ui.javafx.impl;
27  
28  import java.lang.reflect.InvocationTargetException;
29  import java.lang.reflect.Method;
30  import jakarta.annotation.Nonnull;
31  import jakarta.annotation.Nullable;
32  import java.util.Arrays;
33  import java.util.Collections;
34  import java.util.HashMap;
35  import java.util.List;
36  import java.util.Map;
37  import java.util.Optional;
38  import java.util.concurrent.atomic.AtomicBoolean;
39  import java.util.function.Consumer;
40  import java.util.function.Function;
41  import javafx.scene.Parent;
42  import javafx.scene.Scene;
43  import javafx.stage.Stage;
44  import javafx.stage.Window;
45  import javafx.application.Application;
46  import javafx.application.Platform;
47  import org.springframework.context.ApplicationContext;
48  import org.springframework.context.ConfigurableApplicationContext;
49  import org.springframework.scheduling.concurrent.ThreadPoolTaskExecutor;
50  import edu.umd.cs.findbugs.annotations.SuppressFBWarnings;
51  import it.tidalwave.ui.core.annotation.Assemble;
52  import it.tidalwave.ui.core.annotation.PresentationAssembler;
53  import it.tidalwave.ui.core.message.PowerOffEvent;
54  import it.tidalwave.ui.core.message.PowerOnEvent;
55  import it.tidalwave.ui.javafx.JavaFXBinder;
56  import it.tidalwave.ui.javafx.JavaFXMenuBarControl;
57  import it.tidalwave.ui.javafx.JavaFXToolBarControl;
58  import it.tidalwave.ui.javafx.NodeAndDelegate;
59  import it.tidalwave.ui.javafx.impl.JavaFXSafeProxy.Proxied;
60  import jfxtras.styles.jmetro.JMetro;
61  import jfxtras.styles.jmetro.Style;
62  import org.slf4j.Logger;
63  import org.slf4j.LoggerFactory;
64  import it.tidalwave.util.Key;
65  import it.tidalwave.util.PreferencesHandler;
66  import it.tidalwave.util.TypeSafeMap;
67  import it.tidalwave.util.annotation.VisibleForTesting;
68  import it.tidalwave.messagebus.MessageBus;
69  import lombok.Getter;
70  import lombok.RequiredArgsConstructor;
71  import lombok.With;
72  import static java.util.Objects.requireNonNull;
73  import static java.util.stream.Collectors.toList;
74  import static it.tidalwave.util.CollectionUtils.concat;
75  import static it.tidalwave.util.FunctionalCheckedExceptionWrappers.*;
76  import static it.tidalwave.util.ShortNames.shortIds;
77  import static lombok.AccessLevel.PRIVATE;
78  
79  /***************************************************************************************************************************************************************
80   *
81   * A base class for all variants of JavaFX applications with Spring.
82   *
83   * @author  Fabrizio Giudici
84   *
85   **************************************************************************************************************************************************************/
86  public abstract class AbstractJavaFXSpringApplication extends JavaFXApplicationWithSplash
87    {
88      /** Configures the JMetro style, light mode. @since 3.0-ALPHA-1 */
89      public static final Consumer<Scene> STYLE_METRO_LIGHT = scene ->  new JMetro(Style.LIGHT).setScene(scene);
90  
91      /** Configures the JMetro style, dark mode.  @since 3.0-ALPHA-1 */
92      public static final Consumer<Scene> STYLE_METRO_DARK = scene ->  new JMetro(Style.DARK).setScene(scene);
93  
94      /***********************************************************************************************************************************************************
95       * The initialisation parameters to pass to {@link #launch(Class, InitParameters)}.
96       * @since   1.1-ALPHA-6
97       **********************************************************************************************************************************************************/
98      @RequiredArgsConstructor(access = PRIVATE) @With
99      public static class InitParameters
100       {
101         @Nonnull
102         private final String[] args;
103 
104         @Nonnull
105         private final String applicationName;
106 
107         @Nonnull
108         private final String logFolderPropertyName;
109 
110         private final boolean implicitExit;
111 
112         @Nonnull
113         private final TypeSafeMap propertyMap;
114 
115         @Nonnull
116         private final List<Consumer<Scene>> sceneFinalizers;
117 
118         @Nonnull
119         public <T> InitParameters withProperty (@Nonnull final Key<T> key, @Nonnull final T value)
120           {
121             return new InitParameters(args, applicationName, logFolderPropertyName, implicitExit, propertyMap.with(key, value), sceneFinalizers);
122           }
123 
124         @Nonnull
125         public InitParameters withSceneFinalizer (@Nonnull final Consumer<Scene> stageFinalizer)
126           {
127             return new InitParameters(args, applicationName, logFolderPropertyName, implicitExit, propertyMap, concat(sceneFinalizers, stageFinalizer));
128           }
129 
130         private void validate()
131           {
132             requireNotEmpty(applicationName, "applicationName");
133             requireNotEmpty(logFolderPropertyName, "logFolderPropertyName");
134           }
135 
136         private static void requireNotEmpty (@Nullable final String name, @Nonnull final String message)
137           {
138             if (name == null || name.isEmpty())
139               {
140                 throw new IllegalArgumentException(message);
141               }
142           }
143       }
144 
145     public static final String APPLICATION_MESSAGE_BUS_BEAN_NAME = "applicationMessageBus";
146 
147     private static final Map<Class<?>, Object> BEANS = new HashMap<>();
148 
149     private static final int QUEUE_CAPACITY = 10000;
150 
151     private static InitParameters initParameters;
152 
153     @Nullable
154     protected Window mainWindow;
155 
156     @Getter
157     private final ThreadPoolTaskExecutor executor = new ThreadPoolTaskExecutor();
158 
159     @Getter
160     private final JavaFXBinder javaFxBinder = new DefaultJavaFXBinder(executor, () -> requireNonNull(mainWindow, "mainWindow not set"));
161 
162     @Getter
163     private final JavaFXToolBarControl toolBarControl = new DefaultJavaFXToolBarControl();
164 
165     @Getter
166     private final JavaFXMenuBarControl menuBarControl = new DefaultJavaFXMenuBarControl();
167 
168     // Don't use Lombok and its static logger - give Main a chance to initialize things
169     private final Logger log = LoggerFactory.getLogger(AbstractJavaFXSpringApplication.class);
170 
171     private ConfigurableApplicationContext applicationContext;
172 
173     private Optional<MessageBus> messageBus = Optional.empty();
174 
175     private static final AtomicBoolean constructionGuard = new AtomicBoolean(false);
176 
177     /***********************************************************************************************************************************************************
178      * Launches the application.
179      * @param   appClass          the class of the application to instantiate
180      * @param   initParameters    the initialisation parameters
181      **********************************************************************************************************************************************************/
182     @SuppressFBWarnings("DM_EXIT")
183     public static void launch (@Nonnull final Class<? extends Application> appClass, @Nonnull final InitParameters initParameters)
184       {
185         try
186           {
187             initParameters.validate();
188             System.setProperty(PreferencesHandler.PROP_APP_NAME, initParameters.applicationName);
189             Platform.setImplicitExit(initParameters.implicitExit);
190             final var preferencesHandler = PreferencesHandler.getInstance();
191             initParameters.propertyMap.forEach(preferencesHandler::setProperty);
192             System.setProperty(initParameters.logFolderPropertyName, preferencesHandler.getLogFolder().toAbsolutePath().toString());
193             JavaFXSafeProxy.setLogDelegateInvocations(initParameters.propertyMap.getOptional(K_LOG_DELEGATE_INVOCATIONS).orElse(false));
194             AbstractJavaFXSpringApplication.initParameters = initParameters;
195             launch(appClass, initParameters.args);
196           }
197         catch (Throwable t)
198           {
199             // Don't use logging facilities here, they could be not initialized
200             t.printStackTrace();
201             System.exit(-1);
202           }
203       }
204 
205     /***********************************************************************************************************************************************************
206      *
207      **********************************************************************************************************************************************************/
208     @Nonnull
209     public static Map<Class<?>, Object> getBeans()
210       {
211         return Collections.unmodifiableMap(BEANS);
212       }
213 
214     /***********************************************************************************************************************************************************
215      *
216      **********************************************************************************************************************************************************/
217     protected AbstractJavaFXSpringApplication()
218       {
219         if (constructionGuard.getAndSet(true)) // See STB-142
220           {
221             throw new IllegalStateException("Instantiated more than once");
222           }
223 
224         executor.setWaitForTasksToCompleteOnShutdown(false);
225         executor.setAwaitTerminationMillis(2000); // FIXME
226         executor.setThreadNamePrefix("ui-service-pool-");
227         // Fix for STB-26
228         executor.setCorePoolSize(1);
229         executor.setMaxPoolSize(1);
230         executor.setQueueCapacity(QUEUE_CAPACITY);
231         executor.initialize(); // it's used before Spring completes initialisation
232         BEANS.put(JavaFXBinder.class, javaFxBinder);
233         BEANS.put(JavaFXToolBarControl.class, toolBarControl);
234         BEANS.put(JavaFXMenuBarControl.class, menuBarControl);
235         BEANS.put(PreferencesHandler.class, PreferencesHandler.getInstance());
236       }
237 
238     /***********************************************************************************************************************************************************
239      * {@return an empty set of parameters} to populate and pass to {@link #launch(Class, InitParameters)}
240      * @since   1.1-ALPHA-6
241      **********************************************************************************************************************************************************/
242     @Nonnull
243     protected static InitParameters params()
244       {
245         return new InitParameters(new String[0], "", "", true, TypeSafeMap.newInstance(), List.of());
246       }
247 
248     /***********************************************************************************************************************************************************
249      *
250      **********************************************************************************************************************************************************/
251     @Override @Nonnull
252     protected NodeAndDelegate<?> createParent()
253       {
254         return NodeAndDelegate.of(getClass(), applicationFxml);
255       }
256 
257     /***********************************************************************************************************************************************************
258      *
259      **********************************************************************************************************************************************************/
260     @Override
261     protected void initializeInBackground()
262       {
263         log.info("initializeInBackground()");
264         System.getProperties().forEach((name, value) -> log.debug("{}: {}", name, value));
265         // TODO: workaround for NWRCA-41
266         System.setProperty("it.tidalwave.util.spring.ClassScanner.basePackages", "it");
267         applicationContext = createApplicationContext();
268         applicationContext.registerShutdownHook(); // this actually seems not working, onClosing() does
269         log.info(">>>> application context created with beans: {}", Arrays.toString(applicationContext.getBeanDefinitionNames()));
270 
271         if (applicationContext.containsBean(APPLICATION_MESSAGE_BUS_BEAN_NAME))
272           {
273             messageBus = Optional.of(applicationContext.getBean(APPLICATION_MESSAGE_BUS_BEAN_NAME, MessageBus.class));
274           }
275       }
276 
277     /***********************************************************************************************************************************************************
278      * {@return a created application context.}
279      **********************************************************************************************************************************************************/
280     @Nonnull
281     protected abstract ConfigurableApplicationContext createApplicationContext();
282 
283     /***********************************************************************************************************************************************************
284      *
285      **********************************************************************************************************************************************************/
286     @Override @Nonnull
287     protected Scene createScene (@Nonnull final Parent parent)
288       {
289         final var scene = super.createScene(parent);
290         initParameters.sceneFinalizers.forEach(f -> f.accept(scene));
291         return scene;
292       }
293 
294     /***********************************************************************************************************************************************************
295      * {@inheritDoc}
296      **********************************************************************************************************************************************************/
297     @Override
298     protected final void onStageCreated (@Nonnull final Stage stage, @Nonnull final NodeAndDelegate<?> applicationNad)
299       {
300         assert Platform.isFxApplicationThread();
301         this.mainWindow = stage;
302         onStageCreated2(applicationNad);
303       }
304 
305     /***********************************************************************************************************************************************************
306      * This method is separated to make testing simpler (it does not depend on JavaFX stuff).
307      * @param   applicationNad
308      **********************************************************************************************************************************************************/
309     @VisibleForTesting final void onStageCreated2 (@Nonnull final NodeAndDelegate<?> applicationNad)
310       {
311         requireNonNull(applicationContext, "applicationContext is null");
312         final var delegate = applicationNad.getDelegate();
313         final var actualDelegate = getActualDelegate(delegate);
314         log.info("Application presentation delegate: {} --- actual: {}", delegate, actualDelegate);
315 
316         if (actualDelegate.getClass().getAnnotation(PresentationAssembler.class) != null)
317           {
318             callAssemble(actualDelegate);
319           }
320 
321         callPresentationAssemblers();
322         executor.execute(() ->
323           {
324             onStageCreated(applicationContext);
325             messageBus.ifPresent(mb -> mb.publish(new PowerOnEvent())); // must be after onStageCreated()
326           });
327       }
328 
329     /***********************************************************************************************************************************************************
330      * Invoked when the {@link Stage} is created and the {@link ApplicationContext} has been initialized. Typically, the main class overrides this, retrieves
331      * a reference to the main controller and boots it. This method is executed in a background thread.
332      * @param   applicationContext  the application context
333      **********************************************************************************************************************************************************/
334     protected void onStageCreated (@Nonnull final ApplicationContext applicationContext)
335       {
336       }
337 
338     /***********************************************************************************************************************************************************
339      * {@inheritDoc}
340      **********************************************************************************************************************************************************/
341     @Override
342     protected final void onCloseRequest()
343       {
344         log.info("onCloseRequest()");
345         messageBus.ifPresent(mb -> mb.publish(new PowerOffEvent()));
346         executor.execute(() ->
347           {
348             // applicationContext.close();
349             Platform.runLater(() ->
350               {
351                 Platform.exit();
352                 exit();
353               });
354           });
355       }
356 
357     /***********************************************************************************************************************************************************
358      *
359      **********************************************************************************************************************************************************/
360     protected void exit()
361       {
362         System.exit(0);
363       }
364 
365     /***********************************************************************************************************************************************************
366      * Finds all classes annotated with {@link PresentationAssembler} and invokes methods annotated with {@link Assemble}.
367      **********************************************************************************************************************************************************/
368     private void callPresentationAssemblers()
369       {
370         applicationContext.getBeansWithAnnotation(PresentationAssembler.class).values().forEach(this::callAssemble);
371       }
372 
373     /***********************************************************************************************************************************************************
374      * Call a method annotated with {@link Assemble} in the given object.
375      * @param     assembler       the assembler
376      **********************************************************************************************************************************************************/
377     private void callAssemble (@Nonnull final Object assembler)
378       {
379         log.info("Calling presentation assembler: {}", assembler);
380         Arrays.stream(assembler.getClass().getDeclaredMethods())
381               .filter(_p(m -> m.getDeclaredAnnotation(Assemble.class) != null))
382               .forEach(_c(m -> invokeInjecting(m, assembler, this::resolveBean)));
383       }
384 
385     /***********************************************************************************************************************************************************
386      * Instantiates an object of the given class performing dependency injections through the constructor.
387      * TODO: possibly replace with a Spring utility doing method injection.
388      * @throws        RuntimeException if something fails
389      **********************************************************************************************************************************************************/
390     private void invokeInjecting (@Nonnull final Method method, @Nonnull final Object object, @Nonnull final Function<Class<?>, Object> beanFactory)
391       {
392         try
393           {
394             final var parameters = Arrays.stream(method.getParameterTypes()).map(beanFactory).collect(toList());
395             log.info(">>>> calling {}({})", method.getName(), shortIds(parameters));
396             method.invoke(object, parameters.toArray());
397           }
398         catch (IllegalAccessException | InvocationTargetException e)
399           {
400             throw new RuntimeException(e);
401           }
402       }
403 
404     /***********************************************************************************************************************************************************
405      *
406      **********************************************************************************************************************************************************/
407     @Nonnull
408     private <T> T resolveBean (@Nonnull final Class<T> type)
409       {
410         return type.cast(Optional.ofNullable(BEANS.get(type)).orElseGet(() -> applicationContext.getBean(type)));
411       }
412 
413     /***********************************************************************************************************************************************************
414      *
415      **********************************************************************************************************************************************************/
416     @Nonnull
417     private static Object getActualDelegate (@Nonnull final Object delegate)
418       {
419         return delegate instanceof Proxied ? ((Proxied)delegate).__getProxiedObject() : delegate;
420       }
421   }