1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
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
82
83
84
85
86 public abstract class AbstractJavaFXSpringApplication extends JavaFXApplicationWithSplash
87 {
88
89 public static final Consumer<Scene> STYLE_METRO_LIGHT = scene -> new JMetro(Style.LIGHT).setScene(scene);
90
91
92 public static final Consumer<Scene> STYLE_METRO_DARK = scene -> new JMetro(Style.DARK).setScene(scene);
93
94
95
96
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
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
179
180
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
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))
220 {
221 throw new IllegalStateException("Instantiated more than once");
222 }
223
224 executor.setWaitForTasksToCompleteOnShutdown(false);
225 executor.setAwaitTerminationMillis(2000);
226 executor.setThreadNamePrefix("ui-service-pool-");
227
228 executor.setCorePoolSize(1);
229 executor.setMaxPoolSize(1);
230 executor.setQueueCapacity(QUEUE_CAPACITY);
231 executor.initialize();
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
240
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
266 System.setProperty("it.tidalwave.util.spring.ClassScanner.basePackages", "it");
267 applicationContext = createApplicationContext();
268 applicationContext.registerShutdownHook();
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
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
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
307
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()));
326 });
327 }
328
329
330
331
332
333
334 protected void onStageCreated (@Nonnull final ApplicationContext applicationContext)
335 {
336 }
337
338
339
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
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
367
368 private void callPresentationAssemblers()
369 {
370 applicationContext.getBeansWithAnnotation(PresentationAssembler.class).values().forEach(this::callAssemble);
371 }
372
373
374
375
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
387
388
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 }