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 }