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 }