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.spi;
27
28 import javax.annotation.CheckForNull;
29 import jakarta.annotation.Nonnull;
30 import java.util.Objects;
31 import java.util.Optional;
32 import java.util.TreeMap;
33 import java.util.concurrent.ExecutorService;
34 import java.util.concurrent.Executors;
35 import java.io.IOException;
36 import javafx.stage.Stage;
37 import javafx.application.Application;
38 import javafx.application.Platform;
39 import org.springframework.context.ApplicationContext;
40 import org.springframework.context.ConfigurableApplicationContext;
41 import edu.umd.cs.findbugs.annotations.SuppressFBWarnings;
42 import it.tidalwave.ui.core.message.PowerOffEvent;
43 import it.tidalwave.ui.core.message.PowerOnEvent;
44 import it.tidalwave.ui.javafx.ApplicationPresentationAssembler;
45 import it.tidalwave.ui.javafx.JavaFXApplicationWithSplash;
46 import it.tidalwave.ui.javafx.JavaFXSafeProxyCreator;
47 import it.tidalwave.ui.javafx.NodeAndDelegate;
48 import it.tidalwave.ui.javafx.PresentationAssembler;
49 import org.slf4j.Logger;
50 import org.slf4j.LoggerFactory;
51 import it.tidalwave.util.Key;
52 import it.tidalwave.util.PreferencesHandler;
53 import it.tidalwave.util.TypeSafeMap;
54 import it.tidalwave.util.annotation.VisibleForTesting;
55 import it.tidalwave.messagebus.MessageBus;
56 import lombok.AccessLevel;
57 import lombok.Getter;
58 import lombok.RequiredArgsConstructor;
59 import lombok.With;
60 import static lombok.AccessLevel.PRIVATE;
61
62 /***************************************************************************************************************************************************************
63 *
64 * A base class for all variants of JavaFX applications with Spring.
65 *
66 * @author Fabrizio Giudici
67 *
68 **************************************************************************************************************************************************************/
69 public abstract class AbstractJavaFXSpringApplication extends JavaFXApplicationWithSplash
70 {
71 /***********************************************************************************************************************************************************
72 * The initialisation parameters to pass to {@link #launch(Class, InitParameters)}.
73 * @since 1.1-ALPHA-6
74 **********************************************************************************************************************************************************/
75 @RequiredArgsConstructor(access = PRIVATE) @With
76 public static class InitParameters
77 {
78 @Nonnull
79 private final String[] args;
80
81 @Nonnull
82 private final String applicationName;
83
84 @Nonnull
85 private final String logFolderPropertyName;
86
87 private final boolean implicitExit;
88
89 @Nonnull
90 private final TypeSafeMap propertyMap;
91
92 @Nonnull
93 public <T> InitParameters withProperty (@Nonnull final Key<T> key, @Nonnull final T value)
94 {
95 return new InitParameters(args, applicationName, logFolderPropertyName, implicitExit, propertyMap.with(key, value));
96 }
97
98 public void validate()
99 {
100 requireNotEmpty(applicationName, "applicationName");
101 requireNotEmpty(logFolderPropertyName, "logFolderPropertyName");
102 }
103
104 private void requireNotEmpty (@CheckForNull final String name, @Nonnull final String message)
105 {
106 if (name == null || name.isEmpty())
107 {
108 throw new IllegalArgumentException(message);
109 }
110 }
111 }
112
113 public static final String APPLICATION_MESSAGE_BUS_BEAN_NAME = "applicationMessageBus";
114
115 // Don't use Slf4j and its static logger - give Main a chance to initialize things
116 private final Logger log = LoggerFactory.getLogger(AbstractJavaFXSpringApplication.class);
117
118 private ConfigurableApplicationContext applicationContext;
119
120 private Optional<MessageBus> messageBus = Optional.empty();
121
122 @Getter(AccessLevel.PACKAGE) @Nonnull
123 private final ExecutorService executorService = Executors.newSingleThreadExecutor();
124
125 /***********************************************************************************************************************************************************
126 * Launches the application.
127 * @param appClass the class of the application to instantiate
128 * @param initParameters the initialisation parameters
129 **********************************************************************************************************************************************************/
130 @SuppressFBWarnings("DM_EXIT")
131 public static void launch (@Nonnull final Class<? extends Application> appClass, @Nonnull final InitParameters initParameters)
132 {
133 try
134 {
135 initParameters.validate();
136 System.setProperty(PreferencesHandler.PROP_APP_NAME, initParameters.applicationName);
137 Platform.setImplicitExit(initParameters.implicitExit);
138 final var preferencesHandler = PreferencesHandler.getInstance();
139 initParameters.propertyMap.forEach(preferencesHandler::setProperty);
140 System.setProperty(initParameters.logFolderPropertyName, preferencesHandler.getLogFolder().toAbsolutePath().toString());
141 Application.launch(appClass, initParameters.args);
142 }
143 catch (Throwable t)
144 {
145 // Don't use logging facilities here, they could be not initialized
146 t.printStackTrace();
147 System.exit(-1);
148 }
149 }
150
151 /***********************************************************************************************************************************************************
152 * {@return an empty set of parameters} to populate and pass to {@link #launch(Class, InitParameters)}
153 * @since 1.1-ALPHA-6
154 **********************************************************************************************************************************************************/
155 @Nonnull
156 protected static InitParameters params()
157 {
158 return new InitParameters(new String[0], "", "", true, TypeSafeMap.newInstance());
159 }
160
161 /***********************************************************************************************************************************************************
162 *
163 **********************************************************************************************************************************************************/
164 @Override @Nonnull
165 protected NodeAndDelegate<?> createParent()
166 throws IOException
167 {
168 return NodeAndDelegate.load(getClass(), applicationFxml);
169 }
170
171 /***********************************************************************************************************************************************************
172 *
173 **********************************************************************************************************************************************************/
174 @Override
175 protected void initializeInBackground()
176 {
177 log.info("initializeInBackground()");
178
179 try
180 {
181 logProperties();
182 // TODO: workaround for NWRCA-41
183 System.setProperty("it.tidalwave.util.spring.ClassScanner.basePackages", "it");
184 applicationContext = createApplicationContext();
185 applicationContext.registerShutdownHook(); // this actually seems not working, onClosing() does
186
187 if (applicationContext.getBeanFactory().containsBean(APPLICATION_MESSAGE_BUS_BEAN_NAME))
188 {
189 messageBus = Optional.of(applicationContext.getBeanFactory().getBean(APPLICATION_MESSAGE_BUS_BEAN_NAME, MessageBus.class));
190 }
191 }
192 catch (Throwable t)
193 {
194 log.error("", t);
195 }
196 }
197
198 /***********************************************************************************************************************************************************
199 * {@return a created application context.}
200 **********************************************************************************************************************************************************/
201 @Nonnull
202 protected abstract ConfigurableApplicationContext createApplicationContext();
203
204 /***********************************************************************************************************************************************************
205 * {@inheritDoc}
206 **********************************************************************************************************************************************************/
207 @Override
208 protected final void onStageCreated (@Nonnull final Stage stage, @Nonnull final NodeAndDelegate<?> applicationNad)
209 {
210 assert Platform.isFxApplicationThread();
211 JavaFXSafeProxyCreator.getJavaFxBinder().setMainWindow(stage);
212 onStageCreated2(applicationNad);
213 }
214
215 /***********************************************************************************************************************************************************
216 * This method is separated to make testing simpler (it does not depend on JavaFX stuff).
217 * @param applicationNad
218 **********************************************************************************************************************************************************/
219 @VisibleForTesting final void onStageCreated2 (@Nonnull final NodeAndDelegate<?> applicationNad)
220 {
221 final var delegate = applicationNad.getDelegate();
222
223 if (PresentationAssembler.class.isAssignableFrom(delegate.getClass()))
224 {
225 ((PresentationAssembler)delegate).assemble(applicationContext);
226 }
227
228 runApplicationAssemblers(applicationNad);
229 executorService.execute(() ->
230 {
231 onStageCreated(applicationContext);
232 messageBus.ifPresent(mb -> mb.publish(new PowerOnEvent()));
233 });
234 }
235
236 /***********************************************************************************************************************************************************
237 * Invoked when the {@link Stage} is created and the {@link ApplicationContext} has been initialized. Typically, the main class overrides this, retrieves
238 * a reference to the main controller and boots it. This method is executed in a background thread.
239 * @param applicationContext the application context
240 **********************************************************************************************************************************************************/
241 protected void onStageCreated (@Nonnull final ApplicationContext applicationContext)
242 {
243 }
244
245 /***********************************************************************************************************************************************************
246 * {@inheritDoc}
247 **********************************************************************************************************************************************************/
248 @Override
249 protected void onClosing()
250 {
251 messageBus.ifPresent(mb -> mb.publish(new PowerOffEvent()));
252 applicationContext.close();
253 }
254
255 /***********************************************************************************************************************************************************
256 *
257 **********************************************************************************************************************************************************/
258 private void runApplicationAssemblers (@Nonnull final NodeAndDelegate applicationNad)
259 {
260 Objects.requireNonNull(applicationContext, "applicationContext is null");
261 applicationContext.getBeansOfType(ApplicationPresentationAssembler.class).values().forEach(a -> a.assemble(applicationNad.getDelegate()));
262 }
263
264 /***********************************************************************************************************************************************************
265 * Logs all the system properties.
266 **********************************************************************************************************************************************************/
267 private void logProperties()
268 {
269 for (final var e : new TreeMap<>(System.getProperties()).entrySet())
270 {
271 log.debug("{}: {}", e.getKey(), e.getValue());
272 }
273 }
274 }