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}