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 }