001/* 002 * Copyright 2019-2021 M. Sean Gilligan. 003 * 004 * Licensed under the Apache License, Version 2.0 (the "License"); 005 * you may not use this file except in compliance with the License. 006 * You may obtain a copy of the License at 007 * 008 * http://www.apache.org/licenses/LICENSE-2.0 009 * 010 * Unless required by applicable law or agreed to in writing, software 011 * distributed under the License is distributed on an "AS IS" BASIS, 012 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 013 * See the License for the specific language governing permissions and 014 * limitations under the License. 015 */ 016package app.supernaut.fx; 017 018import app.supernaut.fx.test.NoopBackgroundApp; 019import javafx.application.Application; 020import app.supernaut.BackgroundApp; 021import app.supernaut.fx.internal.OpenJfxProxyApplication; 022import org.slf4j.Logger; 023import org.slf4j.LoggerFactory; 024 025import java.lang.reflect.InvocationTargetException; 026import java.util.concurrent.CompletableFuture; 027import java.util.concurrent.CountDownLatch; 028import java.util.function.Supplier; 029 030/** 031 * Base JavaFX implementation of {@link FxLauncher}. This implementation provides the following functionality: 032 * <ol> 033 * <li> 034 * Starts OpenJFX applications. 035 * </li> 036 * <li> 037 * Constructor provides an option to start {@link BackgroundApp} on a new thread (this allows 038 * the {@code BackgroundApp} and the OpenJFX {@link ApplicationDelegate} to initialize in parallel.) 039 * </li> 040 * <li> 041 * Implements {@link FxLauncher#launchAsync} which initializes the OpenJFX {@link ApplicationDelegate} on 042 * a new thread. This is not needed for a typical, packaged OpenJFX application 043 * which can just call {@link FxLauncher#launch} from its {@code static main()}, but is useful 044 * in various testing scenarios. 045 * </li> 046 * <li> 047 * Defines the {@link AppFactory} interface for constructing the {@link BackgroundApp} and {@link ApplicationDelegate}. 048 * This allows subclasses (or callers) to provide their own implementation of the application creation logic. The 049 * AppFactory interface was designed to allow usage of Dependency Injection frameworks like <b>Micronaut</b> 050 * to create dependency-injected implementations of {@link ApplicationDelegate} and {@link BackgroundApp}. The {@link AppFactory AppFactory} 051 * interface was also designed to be lazily-instantiated so the {@link AppFactory AppFactory} (dependency-injection framework) 052 * can initialize in parallel to OpenJFX. 053 * </li> 054 * <li> 055 * Uses the same {@link AppFactory AppFactory} (dependency-injection context) to initialize the ApplicationDelegate and Background 056 * application. A {@code CountDownLatch} is used to make sure the {@link AppFactory AppFactory} (which may be initialized 057 * on another thread along with the BackgroundApp) is ready when {@link OpenJfxProxyApplication} calls 058 * {@code createAppDelegate(Application proxyApplication)}. 059 * </li> 060 * </ol> 061 * 062 */ 063public abstract class FxLauncherAbstract implements FxLauncher { 064 private static final Logger log = LoggerFactory.getLogger(FxLauncherAbstract.class); 065 private static final String backgroundAppLauncherThreadName = "SupernautFX-Background-Launcher"; 066 private static final String foregroundAppLauncherThreadName = "SupernautFX-JavaFX-Launcher"; 067 068 private final boolean initializeBackgroundAppOnNewThread; 069 private final Supplier<AppFactory> appFactorySupplier; 070 private final CountDownLatch appFactoryInitializedLatch; 071 private AppFactory appFactory; 072 073 /** This future returns an initialized BackgroundApp */ 074 protected final CompletableFuture<BackgroundApp> futureBackgroundApp = new CompletableFuture<>(); 075 /** This future returns an initialized ApplicationDelegate */ 076 protected final CompletableFuture<ApplicationDelegate> futureAppDelegate = new CompletableFuture<>(); 077 078 /* Temporary storage of appDelegateClass for interaction with OpenJfxProxyApplication */ 079 private Class<? extends ApplicationDelegate> appDelegateClass; 080 081 /** 082 * Interface that can be used to create and pre-initialize {@link ApplicationDelegate} and {@link BackgroundApp}. 083 * This interface can be implemented by subclasses (or direct callers of the constructor.) By "pre-initialize" we 084 * mean call implementation-dependent methods prior to {@code init()} or {@code start()}. 085 * This interface is designed to support using Dependency Injection frameworks like Micronaut, see 086 * {@code MicronautSfxLauncher}. 087 */ 088 public interface AppFactory { 089 /** 090 * Create the background class instance from a {@link Class} object 091 * @param backgroundAppClass the class to create 092 * @return application instance 093 */ 094 BackgroundApp createBackgroundApp(Class<? extends BackgroundApp> backgroundAppClass); 095 096 /** 097 * Create the background class instance from a {@link Class} object 098 * @param appDelegateClass the class to create 099 * @param proxyApplication a reference to the proxy {@link Application} created by Supernaut.FX 100 * @return application instance 101 */ 102 ApplicationDelegate createAppDelegate(Class<? extends ApplicationDelegate> appDelegateClass, Application proxyApplication); 103 } 104 105 /** 106 * Default implementation of AppFactory. 107 */ 108 public static class DefaultAppFactory implements AppFactory { 109 110 @Override 111 public BackgroundApp createBackgroundApp(Class<? extends BackgroundApp> backgroundAppClass) { 112 return newInstance(backgroundAppClass); 113 } 114 115 @Override 116 public ApplicationDelegate createAppDelegate(Class<? extends ApplicationDelegate> appDelegateClass, Application proxyApplication) { 117 return newInstance(appDelegateClass); 118 } 119 120 /** 121 * newInstance without checked exceptions. 122 * 123 * @param clazz A Class object that must have a no-args constructor. 124 * @param <T> The type of the class 125 * @return A new instanceof the class 126 * @throws RuntimeException exceptions thrown by {@code newInstance()}. 127 */ 128 private static <T> T newInstance(Class<T> clazz) { 129 T appDelegate; 130 try { 131 appDelegate = clazz.getDeclaredConstructor().newInstance(); 132 } catch (NoSuchMethodException | InstantiationException | IllegalAccessException | InvocationTargetException e) { 133 throw new RuntimeException(e); 134 } 135 return appDelegate; 136 } 137 } 138 139 /** 140 * Construct an Asynchronous Launcher that works with OpenJFX. 141 * 142 * @param appFactorySupplier A Supplier that will lazily instantiate an AppFactory. 143 * @param initializeBackgroundAppOnNewThread If true, initializes {@code appFactorySupplier} and 144 * {@code BackgroundApp} on new thread, if false start them on calling thread (typically the main thread) 145 */ 146 public FxLauncherAbstract(Supplier<AppFactory> appFactorySupplier, boolean initializeBackgroundAppOnNewThread) { 147 this.appFactorySupplier = appFactorySupplier; 148 this.initializeBackgroundAppOnNewThread = initializeBackgroundAppOnNewThread; 149 appFactoryInitializedLatch = new CountDownLatch(1); 150 } 151 152 /** 153 * {@inheritDoc} 154 */ 155 @Override 156 public CompletableFuture<ApplicationDelegate> launchAsync(String[] args, Class<? extends ApplicationDelegate> appDelegate, Class<? extends BackgroundApp> backgroundApp) { 157 log.info("launchAsync..."); 158 launchInternal(args, appDelegate, backgroundApp, true); 159 return getAppDelegate(); 160 } 161 162 /** 163 * {@inheritDoc} 164 */ 165 @Override 166 public void launch(String[] args, Class<? extends ApplicationDelegate> appDelegate, Class<? extends BackgroundApp> backgroundApp) { 167 log.info("launch..."); 168 launchInternal(args, appDelegate, backgroundApp, false); 169 } 170 171 /** 172 * {@inheritDoc} 173 */ 174 @Override 175 public void launch(String[] args, Class<? extends ApplicationDelegate> appDelegate) { 176 launch(args, appDelegate, NoopBackgroundApp.class); 177 } 178 179 /** 180 * Called by {@code OpenJfxProxyApplication} to create its delegate {@link ApplicationDelegate} object. 181 * Waits on a {@link CountDownLatch} to make sure the {@link AppFactory AppFactory} is ready. 182 * 183 * @param proxyApplication The calling instance of {@code OpenJfxProxyApplication} 184 * @return The newly constructed OpenJFX-compatible {@link ApplicationDelegate} 185 */ 186 @Override 187 public ApplicationDelegate createAppDelegate(Application proxyApplication) { 188 try { 189 appFactoryInitializedLatch.await(); 190 } catch (InterruptedException e) { 191 throw new RuntimeException(e); 192 } 193 ApplicationDelegate appDelegate = appFactory.createAppDelegate(appDelegateClass, proxyApplication); 194 appDelegate.setApplication(proxyApplication); 195 // TODO: Create a LauncherAware interface for injecting the launcher into apps? 196 futureAppDelegate.complete(appDelegate); 197 return appDelegate; 198 } 199 200 /** 201 * {@inheritDoc} 202 */ 203 @Override 204 public CompletableFuture<ApplicationDelegate> getAppDelegate() { 205 return futureAppDelegate; 206 } 207 208 /** 209 * {@inheritDoc} 210 */ 211 @Override 212 public CompletableFuture<BackgroundApp> getBackgroundApp() { 213 return futureBackgroundApp; 214 } 215 216 /** 217 * Internal launch method called by both {@code launchAsync()} and {@code launch()} 218 * @param args Command-line arguments to pass to the OpenJFX application 219 * @param initForegroundOnNewThread If true, start OpenJFX on a new thread, if false start it on 220 * calling thead (typically this will be the main thread) 221 */ 222 private void launchInternal(String[] args, Class<? extends ApplicationDelegate> appDelegateClass, Class<? extends BackgroundApp> backgroundAppClass, boolean initForegroundOnNewThread) { 223 launchBackgroundApp(backgroundAppClass); 224 launchForegroundApp(args, appDelegateClass, initForegroundOnNewThread); 225 } 226 227 private void launchBackgroundApp(Class<? extends BackgroundApp> backgroundAppClass) { 228 if (initializeBackgroundAppOnNewThread) { 229 log.info("Launching background app on {} thread", backgroundAppLauncherThreadName); 230 startThread(backgroundAppLauncherThreadName, () -> startBackgroundApp(backgroundAppClass)); 231 } else { 232 log.info("Launching background app on caller's thread"); 233 startBackgroundApp(backgroundAppClass); 234 } 235 } 236 237 private void launchForegroundApp(String[] args, Class<? extends ApplicationDelegate> appDelegateClass, boolean async) { 238 if (async) { 239 log.info("Launching on {} thread", foregroundAppLauncherThreadName); 240 startThread(foregroundAppLauncherThreadName, () -> startForegroundApp(args, appDelegateClass)); 241 } else { 242 log.info("Launching on caller's thread"); 243 startForegroundApp(args, appDelegateClass); 244 } 245 } 246 247 private void startBackgroundApp(Class<? extends BackgroundApp> backgroundAppClass) { 248 log.info("Instantiating appFactory"); 249 this.appFactory = appFactorySupplier.get(); 250 251 /* 252 * Tell the foreground app thread that appFactory is initialized. 253 */ 254 log.info("Release appFactoryInitializedLatch"); 255 appFactoryInitializedLatch.countDown(); 256 257 log.info("Instantiating backgroundApp class"); 258 BackgroundApp backgroundApp = appFactory.createBackgroundApp(backgroundAppClass); 259 260 /* 261 * Do any (hopefully minimal) background initialization that 262 * is needed before starting the foreground 263 */ 264 log.info("Init backgroundApp"); 265 backgroundApp.init(); 266 267 futureBackgroundApp.complete(backgroundApp); 268 269 270 /* 271 * Call the background app, so it can start its own threads 272 */ 273 backgroundApp.start(); 274 } 275 276 private void startForegroundApp(String[] args, Class<? extends ApplicationDelegate> appDelegateClass) { 277 OpenJfxProxyApplication.configuredLauncher = this; 278 this.appDelegateClass = appDelegateClass; 279 log.info("Calling Application.launch()"); 280 Application.launch(OpenJfxProxyApplication.class, args); 281 log.info("OpenJfxProxyApplication exited."); 282 } 283 284 private Thread startThread(String threadName, Runnable target) { 285 Thread thread = new Thread(target); 286 thread.setName(threadName); 287 thread.start(); 288 return thread; 289 } 290}