DefaultApplicationManager.java
/*
* Copyright (C) 2016 essobedo.
*
* This is free software; you can redistribute it and/or modify it
* under the terms of the GNU Lesser General Public License as
* published by the Free Software Foundation; either version 2.1 of
* the License, or (at your option) any later version.
*
* This software is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
* Lesser General Public License for more details.
*
* You should have received a copy of the GNU Lesser General Public
* License along with this software; if not, write to the Free
* Software Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA
* 02110-1301 USA, or see the FSF site: http://www.fsf.org.
*/
package com.github.essobedo.appma.core;
import com.github.essobedo.appma.core.config.ConfigFromProperties;
import com.github.essobedo.appma.core.config.ConfigurationFactory;
import com.github.essobedo.appma.core.io.Folder;
import com.github.essobedo.appma.core.progress.LogProgress;
import com.github.essobedo.appma.core.progress.StatusBar;
import com.github.essobedo.appma.core.util.Classpath;
import com.github.essobedo.appma.core.zip.UnzipTask;
import com.github.essobedo.appma.exception.ApplicationException;
import com.github.essobedo.appma.exception.TaskInterruptedException;
import com.github.essobedo.appma.spi.Manageable;
import com.github.essobedo.appma.spi.VersionManager;
import com.github.essobedo.appma.task.Task;
import edu.umd.cs.findbugs.annotations.SuppressFBWarnings;
import java.io.Closeable;
import java.io.File;
import java.io.FileOutputStream;
import java.io.IOException;
import java.io.OutputStream;
import java.lang.reflect.ParameterizedType;
import java.lang.reflect.Type;
import java.net.URL;
import java.net.URLClassLoader;
import java.nio.file.Files;
import java.util.Arrays;
import java.util.ServiceLoader;
import java.util.concurrent.Callable;
import java.util.concurrent.Future;
import java.util.concurrent.FutureTask;
import java.util.concurrent.atomic.AtomicReference;
import java.util.function.Predicate;
import java.util.logging.Level;
import java.util.logging.Logger;
import javafx.application.Platform;
import javafx.event.Event;
import javafx.geometry.Rectangle2D;
import javafx.scene.Scene;
import javafx.stage.Screen;
import javafx.stage.Stage;
/**
* The default implementation of {@link ApplicationManager}.
*
* @author Nicolas Filotto (nicolas.filotto@gmail.com)
* @version $Id$
* @since 1.0
*/
class DefaultApplicationManager implements ApplicationManager {
/**
* The logger of the class.
*/
private static final Logger LOG = Logger.getLogger(DefaultApplicationManager.class.getName());
/**
* The format of the error message to use in case of an illegal state while upgrading.
*/
private static final String COULD_NOT_UPGRADE_ILLEGAL_STATE =
"Could not upgrade the application as the state is illegal: %s";
/**
* The arguments to pass to the application on initialization.
*/
private final String[] arguments;
/**
* The root directory of the application.
*/
private final File root;
/**
* The file in which the compressed content of the patch should be stored in case of an upgrade.
*/
private final File patchTargetFile;
/**
* The folder in which the uncompressed content of the patch should be stored in case of an upgrade.
*/
private final File patchContentTargetFolder;
/**
* The configuration of the application manager.
*/
private Configuration configuration;
/**
* The current application.
*/
private Manageable application;
/**
* The current state of the application.
*/
private final AtomicReference<ApplicationState> state = new AtomicReference<>(ApplicationState.DESTROYED);
/**
* The executor used to execute all the asynchronous tasks.
*/
private final AsyncTaskExecutor executor = new AsyncTaskExecutor();
/**
* The current stage.
*/
private Stage stage;
/**
* The predicate to test to know if the application can exit or not.
*/
private Predicate<Void> onCloseRequestPredicate;
/**
* Constructs a {@code DefaultApplicationManager} with the specified root folder and arguments.
* @param root the root folder of the application.
* @param arguments the arguments to pass to the application on initialization.
* @throws ApplicationException The configuration could not be loaded.
*/
DefaultApplicationManager(final File root, final String... arguments) throws ApplicationException {
this(root, null, null, arguments);
}
/**
* Constructs a {@code DefaultApplicationManager} with the specified root folder, patch file,
* patch content folder and arguments.
*
* <p><i>This constructor is for testing purpose only</i>
* @param root the root folder of the application.
* @param patchTargetFile The patch file to use in case of an upgrade.
* @param patchContentTargetFolder the folder to use to store the content of the patch in case
* of an upgrade.
* @param arguments the arguments to pass to the application on initialization.
* @throws ApplicationException The configuration could not be loaded.
*/
DefaultApplicationManager(final File root, final File patchTargetFile,
final File patchContentTargetFolder, final String... arguments) throws ApplicationException {
this.root = root;
this.arguments = arguments;
this.patchTargetFile = patchTargetFile;
this.patchContentTargetFolder = patchContentTargetFolder;
loadConfiguration();
}
/**
* Loads the configuration from the root directory.
* @throws ApplicationException If the configuration could not be loaded.
*/
private void loadConfiguration() throws ApplicationException {
final ConfigurationFactory factory = new ConfigurationFactory(root);
setConfiguration(factory.create());
}
/**
* Persists if needed the provided configuration and reloads the configuration from the
* root directory.
* @param configuration The configuration to store if needed.
* @throws ApplicationException If the configuration could not be re-loaded.
*/
private void reload(final Configuration configuration) throws ApplicationException {
final String configurationName = ConfigurationFactory.getConfigurationName();
final File configFile = new File(root, configurationName);
if (configuration == null) {
if (configFile.exists() && !configFile.delete() && LOG.isLoggable(Level.WARNING)) {
LOG.log(Level.WARNING, String.format("The file '%s' could not be deleted",
configFile.getAbsolutePath()));
}
loadConfiguration();
} else {
try {
ConfigFromProperties.store(configuration, configFile);
setConfiguration(configuration);
} catch (IOException e) {
if (LOG.isLoggable(Level.SEVERE)) {
LOG.log(Level.SEVERE, "The configuration could not be stored", e);
}
loadConfiguration();
}
}
}
/**
* Sets the new configuration.
* @param configuration The new configuration.
*/
private void setConfiguration(final Configuration configuration) {
synchronized (this) {
this.configuration = configuration;
}
}
/**
* Gives the configuration of the application manager.
* @return the configuration of the application manager.
*/
private Configuration getConfiguration() {
synchronized (this) {
return configuration;
}
}
/**
* Gives the current application.
* @return the current application.
*/
Manageable getApplication() {
synchronized (this) {
return application;
}
}
@Override
public Stage getStage() {
synchronized (this) {
return stage;
}
}
@Override
public void setOnCloseRequestPredicate(final Predicate<Void> predicate) {
synchronized (this) {
this.onCloseRequestPredicate = predicate;
}
}
/**
* @return The predicate to test to know if the application can exit or not.
*/
private Predicate<Void> getOnCloseRequestPredicate() {
synchronized (this) {
return onCloseRequestPredicate;
}
}
/**
* Creates the application.
* @return The application that has been created by the application manager.
* @throws ApplicationException if the application could not be created.
*/
protected Manageable create() throws ApplicationException {
if (!state.compareAndSet(ApplicationState.DESTROYED, ApplicationState.CREATING)) {
throw new ApplicationException(String.format(
"Could not create the application as the state is illegal: %s", state.get()));
}
final ClassLoader classLoader = getClassLoader(getConfiguration());
final ServiceLoader<Manageable> loader = ServiceLoader.load(Manageable.class, classLoader);
Manageable application = null;
for (final Manageable app : loader) {
if (LOG.isLoggable(Level.FINE)) {
LOG.log(Level.FINE, String.format("The application '%s' version '%s' has ben found", app.name(),
app.version()));
}
if (app.accept(arguments)) {
application = app;
break;
} else if (LOG.isLoggable(Level.FINE)) {
LOG.log(Level.FINE, String.format(
"The application '%s' version '%s' is not compatible with the arguments '%s'",
app.name(), app.version(), Arrays.toString(arguments)));
}
}
if (application == null) {
throw new ApplicationException("Could not find any compliant application");
}
synchronized (this) {
this.application = application;
}
state.set(ApplicationState.CREATED);
if (LOG.isLoggable(Level.INFO)) {
LOG.log(Level.INFO, String.format("The application '%s' version '%s' has ben found", application.name(),
application.version()));
}
return application;
}
/**
* Initializes the application.
* @return The {@link Scene} of the initialized application in case of a Java FX application
* or {@code null} otherwise.
* @throws ApplicationException if the application could not be created.
*/
protected Scene init() throws ApplicationException {
if (!state.compareAndSet(ApplicationState.CREATED, ApplicationState.INITIALIZING)) {
throw new ApplicationException(String.format(
"Could not init the application as the state is illegal: %s", state.get()));
}
final Manageable application = getApplication();
final ClassLoader contextCL = Thread.currentThread().getContextClassLoader();
final Scene scene;
try {
Thread.currentThread().setContextClassLoader(application.getClass().getClassLoader());
if (LOG.isLoggable(Level.INFO)) {
LOG.log(Level.INFO, String.format("Init the application '%s' version '%s'", application.name(),
application.version()));
}
scene = application.init(this, arguments);
state.set(ApplicationState.INITIALIZED);
} catch (ApplicationException e) {
state.set(ApplicationState.UNKNOWN);
throw e;
} catch (RuntimeException e) {
state.set(ApplicationState.UNKNOWN);
throw new ApplicationException("Could not init the application", e);
} finally {
Thread.currentThread().setContextClassLoader(contextCL);
}
return scene;
}
/**
* Creates the {@link ClassLoader} corresponding to the specified {@link Configuration}.
* @param configuration The configuration to use to create the {@link ClassLoader}.
* @return The {@link ClassLoader} corresponding to the specified {@link Configuration}.
* @throws ApplicationException if the {@link ClassLoader} could not be created.
*/
private ClassLoader getClassLoader(final Configuration configuration) throws ApplicationException {
final URL[] urls = configuration.getClasspathAsUrls();
return new URLClassLoader(urls, getClass().getClassLoader());
}
/**
* Destroys the application.
* @throws ApplicationException if the application could not be created.
*/
protected void destroy() throws ApplicationException {
if (!state.compareAndSet(ApplicationState.INITIALIZED, ApplicationState.DESTROYING)) {
throw new ApplicationException(String.format(
"Could not destroy the application as the state is illegal: %s", state.get()));
}
final Manageable application = getApplication();
final ClassLoader contextCL = Thread.currentThread().getContextClassLoader();
try {
Thread.currentThread().setContextClassLoader(application.getClass().getClassLoader());
if (LOG.isLoggable(Level.INFO)) {
LOG.log(Level.INFO, String.format("Destroy the application '%s' version '%s'", application.name(),
application.version()));
}
if (getStage() != null && application.icon() != null) {
Platform.runLater(() -> getStage().getIcons().removeAll(application.icon()));
}
application.destroy();
closeClasspath(getConfiguration());
close(application.getClass().getClassLoader());
synchronized (this) {
this.application = null;
}
state.set(ApplicationState.DESTROYED);
} catch (ApplicationException e) {
state.set(ApplicationState.UNKNOWN);
throw e;
} catch (RuntimeException e) {
state.set(ApplicationState.UNKNOWN);
throw new ApplicationException("Could not destroy the application", e);
} finally {
Thread.currentThread().setContextClassLoader(contextCL);
}
}
/**
* Closes all the resources corresponding to the classpath.
* @param configuration The configuration from which it extracts the URLs corresponding to the classpath.
*/
private void closeClasspath(final Configuration configuration) {
if (configuration == null) {
return;
}
try {
final Classpath classpath = new Classpath(configuration.getClasspathAsUrls());
classpath.release();
} catch (ApplicationException e) {
if (LOG.isLoggable(Level.WARNING)) {
LOG.log(Level.WARNING, "Could not close the jar files used by the classloader", e);
}
}
}
/**
* Closes the classloader if possible in order to properly release the resources.
* @param classLoader The classloader to close.
*/
@SuppressWarnings("PMD.DoNotCallGarbageCollectionExplicitly")
@SuppressFBWarnings(value = "DM_GC", justification = "Needed to properly release the jar files")
private void close(final ClassLoader classLoader) {
if (classLoader == null) {
return;
}
if (classLoader instanceof Closeable) {
if (LOG.isLoggable(Level.INFO)) {
LOG.log(Level.INFO, "Closing the classloader");
}
try {
((Closeable) classLoader).close();
} catch (IOException e) {
if (LOG.isLoggable(Level.WARNING)) {
LOG.log(Level.WARNING, "Could not close properly the classloader", e);
}
} finally {
// Ensure that there is no more references relying on this classloader.
System.gc();
}
}
}
@Override
public Task<String> checkForUpdate() throws ApplicationException {
final Manageable application = getApplication();
if (application == null) {
throw new ApplicationException("Could not check for update as there is no application running");
}
final ClassLoader contextCL = Thread.currentThread().getContextClassLoader();
try {
Thread.currentThread().setContextClassLoader(application.getClass().getClassLoader());
final VersionManager versionManager = getVersionManager(application.getClass().getName(),
application.getClass().getClassLoader());
if (versionManager == null) {
throw new ApplicationException("No version manager could be found");
}
if (LOG.isLoggable(Level.INFO)) {
LOG.log(Level.INFO, String.format("Checking for update for the application '%s' version '%s'",
application.name(),
application.version()));
}
return versionManager.check(application);
} catch (ApplicationException e) {
if (LOG.isLoggable(Level.SEVERE)) {
LOG.log(Level.SEVERE, "Could not check for update", e);
}
throw e;
} finally {
Thread.currentThread().setContextClassLoader(contextCL);
}
}
@Override
public Future<Void> upgrade() {
final Callable<Void> task = () -> {
try {
doUpgrade();
} catch (ApplicationException e) {
if (LOG.isLoggable(Level.SEVERE)) {
LOG.log(Level.SEVERE, e.getMessage(), e);
}
exit();
throw e;
}
return null;
};
final FutureTask<Void> future = new FutureTask<>(task);
executor.execute(future);
return future;
}
/**
* Upgrades the application.
* @throws ApplicationException if the application could not be upgraded.
*/
void doUpgrade() throws ApplicationException {
if (state.get() != ApplicationState.INITIALIZED) {
throw new ApplicationException(String.format(COULD_NOT_UPGRADE_ILLEGAL_STATE, state.get()));
}
final Manageable application = getApplication();
if (application == null) {
throw new ApplicationException(String.format(COULD_NOT_UPGRADE_ILLEGAL_STATE, state.get()));
}
final String className = application.getClass().getName();
final VersionManager versionManager = getVersionManager(className,
application.getClass().getClassLoader());
if (versionManager == null) {
throw new ApplicationException("No version manager could be found");
}
final File patchFolder = getPatchContent(application, versionManager);
final String oldVersion = application.version();
destroy();
applyNShow(className, patchFolder, oldVersion);
}
/**
* Applies the patch and launches the upgraded application.
* @param className the name of the class of the application to upgrade.
* @param patchFolder the folder that contains the content of the patch.
* @param oldVersion the previous version of the application.
* @throws ApplicationException In case an error occurs.
*/
private void applyNShow(final String className, final File patchFolder, final String oldVersion)
throws ApplicationException {
if (!state.compareAndSet(ApplicationState.DESTROYED, ApplicationState.UPGRADING)) {
throw new ApplicationException(String.format(COULD_NOT_UPGRADE_ILLEGAL_STATE, state.get()));
}
if (patchFolder == null || !applyPatch(className, patchFolder, oldVersion)) {
return;
}
if (!state.compareAndSet(ApplicationState.UPGRADING, ApplicationState.DESTROYED)) {
throw new ApplicationException(String.format(COULD_NOT_UPGRADE_ILLEGAL_STATE, state.get()));
}
create();
if (getStage() != null && getApplication().icon() != null) {
Platform.runLater(() -> getStage().getIcons().add(getApplication().icon()));
}
initNShow();
}
/**
* Triggers an initialization of the application. It will be done asynchronously.
* @param stage the stage to use to initialize the application.
* @param callbackOnError callback to use in case of an error.
* @return The {@link Future} object allowing to be notified once the task is over.
*/
Future<Void> asyncInitNShow(final Stage stage, final Runnable callbackOnError) {
final Callable<Void> task = () -> {
try {
synchronized (this) {
this.stage = stage;
}
initNShow();
} catch (ApplicationException e) {
if (callbackOnError != null) {
Platform.runLater(callbackOnError::run);
}
if (LOG.isLoggable(Level.SEVERE)) {
LOG.log(Level.SEVERE, e.getMessage(), e);
}
throw e;
}
return null;
};
final FutureTask<Void> future = new FutureTask<>(task);
executor.execute(future);
return future;
}
/**
* Initializes and shows the application.
* @throws ApplicationException in case the application could not be initialized.
*/
private void initNShow() throws ApplicationException {
final Manageable application = getApplication();
final Scene scene = init();
if (getStage() != null) {
Platform.runLater(() -> showApplication(application, scene));
}
}
/**
* Shows the application in the middle of the screen.
* @param application the application to show
* @param scene the scene to display in the middle of the screen.
*/
private void showApplication(final Manageable application, final Scene scene) {
final ClassLoader contextCL = Thread.currentThread().getContextClassLoader();
try {
Thread.currentThread().setContextClassLoader(application.getClass().getClassLoader());
final Stage primaryStage = getStage();
primaryStage.setResizable(true);
primaryStage.setOnCloseRequest(event -> {
final Predicate<Void> predicate = getOnCloseRequestPredicate();
if (predicate == null || predicate.test(null)) {
// Can exit
onExit();
} else {
// Discard the event to prevent closing the window.
event.consume();
}
});
primaryStage.setScene(scene);
final Rectangle2D primScreenBounds = Screen.getPrimary().getVisualBounds();
primaryStage.setX((primScreenBounds.getWidth() - primaryStage.getWidth()) / 2);
primaryStage.setY((primScreenBounds.getHeight() - primaryStage.getHeight()) / 2);
} finally {
Thread.currentThread().setContextClassLoader(contextCL);
}
}
/**
* Applies the patch.
* @param className The name of the application to upgrade.
* @param patchFolder the folder containing the content of the patch.
* @param oldVersion the previous version of the application.
* @return {@code true} if the patch could be applied, {@code false} otherwise.
* @throws ApplicationException in case the patch could not be applied.
*/
private boolean applyPatch(final String className, final File patchFolder,
final String oldVersion) throws ApplicationException {
final ClassLoader contextCL = Thread.currentThread().getContextClassLoader();
Configuration config = null;
ClassLoader classLoader = null;
try {
final ConfigurationFactory factory = new ConfigurationFactory(patchFolder);
config = factory.create();
classLoader = getClassLoader(config);
Thread.currentThread().setContextClassLoader(classLoader);
final VersionManager versionManager = getVersionManager(className, classLoader);
if (versionManager == null) {
throw new ApplicationException("No version manager could be found");
}
final Configuration configuration = executeTask("Applying the patch",
((VersionManager<?>) versionManager).upgrade(patchFolder, root, oldVersion));
reload(configuration);
} catch (TaskInterruptedException e) {
if (LOG.isLoggable(Level.FINE)) {
LOG.log(Level.FINE, "The task has been interrupted", e);
}
exit();
return false;
} catch (ApplicationException e) {
state.set(ApplicationState.UNKNOWN);
throw e;
} catch (RuntimeException e) {
state.set(ApplicationState.UNKNOWN);
throw new ApplicationException("Could not upgrade the application", e);
} finally {
Thread.currentThread().setContextClassLoader(contextCL);
closeClasspath(config);
close(classLoader);
final Folder folder = new Folder(patchFolder);
folder.delete();
}
return true;
}
/**
* Gets the content of the patch and stores it into a folder.
* @param application the application for which we want to get the patch.
* @param versionManager the version manager to use to get the content of the patch.
* @return a {@code File} corresponding to the folder that contains the content of the patch.
* @throws ApplicationException if the content of the patch could not be retrieved.
*/
private File getPatchContent(final Manageable application, final VersionManager versionManager)
throws ApplicationException {
File destFolder;
final ClassLoader contextCL = Thread.currentThread().getContextClassLoader();
File file2Delete = null;
try {
Thread.currentThread().setContextClassLoader(application.getClass().getClassLoader());
final File zipFile = getPatchTargetFile();
file2Delete = zipFile;
try (OutputStream out = new FileOutputStream(zipFile)) {
executeTask(String.format("Getting the new version of the application '%s'",
application.name()), versionManager.store(application, out));
}
destFolder = getPatchContentTargetFolder();
final Task<Void> unzip = new UnzipTask(zipFile, destFolder);
executeTask("Unzipping the patch", unzip);
} catch (TaskInterruptedException e) {
if (LOG.isLoggable(Level.FINE)) {
LOG.log(Level.FINE, "The task has been interrupted", e);
}
exit();
destFolder = null;
} catch (ApplicationException e) {
state.set(ApplicationState.UNKNOWN);
throw e;
} catch (RuntimeException | IOException e) {
state.set(ApplicationState.UNKNOWN);
throw new ApplicationException("Could not upgrade the application", e);
} finally {
if (file2Delete != null && !file2Delete.delete() && LOG.isLoggable(Level.WARNING)) {
LOG.log(Level.WARNING, String.format("The file '%s' could not be deleted",
file2Delete.getAbsolutePath()));
}
Thread.currentThread().setContextClassLoader(contextCL);
}
return destFolder;
}
/**
* Gives the folder that will contain the content of the patch.
* @return the folder that will contain the content of the patch.
* @throws IOException in case the temporary directory could not be created.
*/
private File getPatchContentTargetFolder() throws IOException {
if (patchContentTargetFolder == null) {
return new File(Files.createTempDirectory("upgrade").toString());
} else {
return patchContentTargetFolder;
}
}
/**
* Gives the file that will contain the patch.
* @return the file that will contain the patch.
* @throws IOException in case the temporary file could not be created.
*/
private File getPatchTargetFile() throws IOException {
if (patchTargetFile == null) {
return File.createTempFile("upgrade", "tmp");
} else {
return patchTargetFile;
}
}
/**
* Executes the specified task and use {@link LogProgress} or {@link StatusBar} to
* provide information about how the task is progressing. If the application is
* a Java FX application, it will use the {@link StatusBar} otherwise it will use the
* {@link LogProgress}.
* @param messageInfo the info message to log before executing the task.
* @param task the task to execute.
* @param <T> the return type of the task to execute.
* @return The result of the task
* @throws ApplicationException if the task fails.
* @throws TaskInterruptedException if the task has been interrupted.
*/
private <T> T executeTask(final String messageInfo, final Task<T> task)
throws ApplicationException, TaskInterruptedException {
if (LOG.isLoggable(Level.INFO)) {
LOG.log(Level.INFO, messageInfo);
}
if (getStage() == null) {
new LogProgress(task);
} else {
final StatusBar bar = new StatusBar(task);
final Scene scene = new Scene(bar, 300.0d, 150.0d);
Platform.runLater(() -> showStatusWindow(scene));
}
return task.execute();
}
/**
* Show the window providing the status of a task in the middle of the screen.
* @param scene the window to display in the middle of the screen.
*/
private void showStatusWindow(final Scene scene) {
final Stage primaryStage = getStage();
primaryStage.setResizable(false);
primaryStage.setOnCloseRequest(Event::consume);
primaryStage.setScene(scene);
final Rectangle2D primScreenBounds = Screen.getPrimary().getVisualBounds();
primaryStage.setX((primScreenBounds.getWidth() - primaryStage.getWidth()) / 2);
primaryStage.setY((primScreenBounds.getHeight() - primaryStage.getHeight()) / 2);
}
/**
* Gives the version manager that could be found using the given class loader and that matches
* with the specified full qualified name of the application.
* @param className the full qualified name of the application for which we look for a version manager.
* @param classLoader the classloader to use to find the version manager.
* @return the version manager that matches with the specified criteria, {@code null} if none could
* be found.
* @throws ApplicationException if an error occurs while looking for a version manager.
*/
private VersionManager<?> getVersionManager(final String className, final ClassLoader classLoader)
throws ApplicationException {
final ServiceLoader<VersionManager> loader = ServiceLoader.load(VersionManager.class, classLoader);
for (final VersionManager versionManager : loader) {
if (LOG.isLoggable(Level.FINE)) {
LOG.log(Level.FINE, String.format("The version manager '%s' has ben found",
versionManager.getClass().getName()));
LOG.log(Level.FINE, String.format("The version manager '%s' has '%s' generic interfaces",
versionManager.getClass().getName(), versionManager.getClass().getGenericInterfaces().length));
LOG.log(Level.FINE, String.format("The version manager '%s' has '%s' as generic super class",
versionManager.getClass().getName(), versionManager.getClass().getGenericSuperclass()));
}
if (accept(className, classLoader, versionManager)) {
return versionManager;
}
}
return null;
}
/**
* Indicates whether the given {@link VersionManager} matches with the specified criteria.
* @param className the full qualified name of the application for which we want a version manager.
* @param classLoader the classloader to use to check the version manager.
* @param versionManager the version manager to check.
* @return {@code true} if the version manager matches, {@code false} otherwise.
* @throws ApplicationException if the version manager could not be checked.
*/
@SuppressWarnings("PMD.AvoidLiteralsInIfCondition")
private boolean accept(final String className, final ClassLoader classLoader,
final VersionManager versionManager) throws ApplicationException {
final Type[] types = getTypes(versionManager);
if (types.length == 1) {
final ParameterizedType type;
if (types[0] instanceof ParameterizedType) {
type = (ParameterizedType) types[0];
} else {
return true;
}
if (LOG.isLoggable(Level.FINE)) {
LOG.log(Level.FINE, String.format("The version manager '%s' has '%s' type arguments",
versionManager.getClass().getName(), type.getActualTypeArguments().length));
}
if (type.getActualTypeArguments().length == 1) {
final Class<?> typeClass = (Class<?>) type.getActualTypeArguments()[0];
if (LOG.isLoggable(Level.FINE)) {
LOG.log(Level.FINE, String.format("The version manager '%s' is for the type '%s'",
versionManager.getClass().getName(), typeClass));
}
try {
return typeClass.isAssignableFrom(Class.forName(className, false, classLoader));
} catch (ClassNotFoundException e) {
throw new ApplicationException(String.format("Could not find the class '%s'", className), e);
}
}
}
return false;
}
/**
* Gives the generic types of the specified version manager.
* @param versionManager the version manager for which we want the generic types.
* @return the generic types of the provided version manager.
*/
private Type[] getTypes(final VersionManager versionManager) {
final Class<?> versionManagerClass = versionManager.getClass();
if (versionManagerClass.getGenericInterfaces().length == 0) {
if (versionManagerClass.getGenericSuperclass() == null) {
return new Type[]{};
} else {
return new Type[]{versionManagerClass.getGenericSuperclass()};
}
} else {
return versionManagerClass.getGenericInterfaces();
}
}
/**
* Exit the whole application properly.
*/
private void exit() {
executor.stop();
if (getStage() != null) {
Platform.runLater(() -> {
getStage().close();
Platform.exit();
});
}
}
@Override
public void onExit() {
try {
destroy();
} catch (ApplicationException e) {
if (LOG.isLoggable(Level.WARNING)) {
LOG.log(Level.WARNING, "Could not destroy the application on exit.", e);
}
}
exit();
}
}