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