Skip to content

Method: params()

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))
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("onClosing()");
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: }