package com.twasyl.slideshowfx.osgi;
import com.twasyl.slideshowfx.engine.presentation.Presentations;
import com.twasyl.slideshowfx.global.configuration.GlobalConfiguration;
import com.twasyl.slideshowfx.plugin.IPlugin;
import com.twasyl.slideshowfx.plugin.InstalledPlugin;
import org.osgi.framework.*;
import org.osgi.framework.launch.Framework;
import org.osgi.framework.launch.FrameworkFactory;
import java.io.File;
import java.io.FileNotFoundException;
import java.io.IOException;
import java.net.MalformedURLException;
import java.net.URL;
import java.nio.channels.OverlappingFileLockException;
import java.nio.file.Files;
import java.nio.file.Path;
import java.nio.file.StandardCopyOption;
import java.util.*;
import java.util.logging.Level;
import java.util.logging.Logger;
import java.util.stream.Collectors;
import static java.util.logging.Level.SEVERE;
import static java.util.logging.Level.WARNING;
import static org.osgi.framework.Constants.*;
/**
* This class manages all OSGi bundles: from installation to uninstallation. It also starts the OSGi container as well
* as it can stop it properly.
*
* @author Thierry Wasylczenko
* @version 1.2
* @since SlideshowFX 1.0
*/
public class OSGiManager {
private static final Logger LOGGER = Logger.getLogger(OSGiManager.class.getName());
private static OSGiManager singleton = null;
public static final String PRESENTATION_FOLDER = "presentation.folder";
public static final String PRESENTATION_RESOURCES_FOLDER = "presentation.resources.folder";
protected Framework osgiFramework;
protected File pluginsDirectory;
protected File osgiCache;
/**
* Default constructor of the class.
*/
protected OSGiManager() {
this.pluginsDirectory = GlobalConfiguration.PLUGINS_DIRECTORY;
this.osgiCache = new File(System.getProperty("user.home") + "/.SlideshowFX/felix-cache");
}
public static final synchronized OSGiManager getInstance() {
if(OSGiManager.singleton == null) {
OSGiManager.singleton = new OSGiManager();
}
return OSGiManager.singleton;
}
/**
* Start the OSGi container.
*/
public void start() {
final Map configurationMap = new HashMap<>();
configurationMap.put(FRAMEWORK_STORAGE_CLEAN, FRAMEWORK_STORAGE_CLEAN_ONFIRSTINIT);
configurationMap.put(FRAMEWORK_STORAGE, this.osgiCache.getAbsolutePath().replaceAll("\\\\", "/"));
configurationMap.put(FRAMEWORK_BUNDLE_PARENT, FRAMEWORK_BUNDLE_PARENT_APP);
final StringJoiner bootdelegation = new StringJoiner(",");
bootdelegation.add("com.twasyl.slideshowfx.markup")
.add("com.twasyl.slideshowfx.content.extension")
.add("com.twasyl.slideshowfx.hosting.connector")
.add("com.twasyl.slideshowfx.hosting.connector.io")
.add("com.twasyl.slideshowfx.hosting.connector.exceptions")
.add("com.twasyl.slideshowfx.snippet.executor")
.add("com.twasyl.slideshowfx.osgi")
.add("com.twasyl.slideshowfx.engine.*")
.add("com.twasyl.slideshowfx.global.configuration")
.add("com.twasyl.slideshowfx.utils.*")
.add("com.twasyl.slideshowfx.plugin")
.add("com.twasyl.slideshowfx.server.beans.quiz")
.add("de.jensd.fx.glyphs")
.add("de.jensd.fx.glyphs.fontawesome")
.add("sun.misc")
.add("org.w3c.*")
.add("javax.*")
.add("javafx.*")
.add("com.sun.javafx");
configurationMap.put(FRAMEWORK_BOOTDELEGATION, bootdelegation.toString());
// Starting OSGi
final FrameworkFactory frameworkFactory = ServiceLoader.load(FrameworkFactory.class).iterator().next();
osgiFramework = frameworkFactory.newFramework(configurationMap);
try {
osgiFramework.start();
LOGGER.fine("OSGI container has bee started successfully");
} catch (BundleException | OverlappingFileLockException e) {
LOGGER.log(SEVERE, "Can not start OSGi server", e);
try {
osgiFramework.stop();
} catch (BundleException e1) {
LOGGER.log(SEVERE, "Can not correctly abort OSGi starting process", e1);
} finally {
this.osgiFramework = null;
}
}
}
/**
* Start the OSGi container and deploy all plugins in the plugins' directory.
*/
public void startAndDeploy() {
start();
if(this.osgiFramework != null) {
// Deploy initially present plugins
if (!this.pluginsDirectory.exists()) {
if (!this.pluginsDirectory.mkdirs()) {
LOGGER.log(SEVERE, "Can not create plugins directory");
}
}
if (this.pluginsDirectory.exists()) {
Arrays.stream(this.pluginsDirectory.listFiles((dir, name) -> name.endsWith(".jar")))
.forEach(file -> {
try {
this.deployBundle(file, false);
} catch (IOException e) {
LOGGER.log(WARNING, "Can not deploy bundle", e);
}
});
Arrays.stream(this.osgiFramework.getBundleContext().getBundles())
.filter(this::isPluginInactive)
.forEach(this::startBundle);
}
}
}
/**
* Stop the OSGi container.
*/
public void stop() {
if(osgiFramework != null) {
try {
osgiFramework.stop();
osgiFramework.waitForStop(0);
} catch (BundleException e) {
LOGGER.log(SEVERE, "Can not stop Felix", e);
} catch (InterruptedException e) {
LOGGER.log(SEVERE, "Can not wait for stopping Felix", e);
}
}
}
/**
* Deploys a bundleFile in the OSGi container and start it. This method copies the given bundleFile to directory of
* plugins and then deploys it.
* If the bundle is already in the plugins' directory, it is simply deployed.
* If the plugin exists in a more recent version, the given plugin will not be installed.
* If the plugin is more recent than an already installed version, the old version is uninstalled and the new one
* is installed.
*
* @param bundleFile The bundleFile to deploy.
* @throws IllegalArgumentException If the bundleFile is not a directory.
* @throws FileNotFoundException If the bundleFile is not found.
* @throws NullPointerException If the bundleFile is null.
* @return The installed service.
*/
public Object deployBundle(File bundleFile) throws IllegalArgumentException, NullPointerException, IOException {
return this.deployBundle(bundleFile, true);
}
/**
* Deploys a bundleFile in the OSGi container. This method copies the given bundleFile to directory of plugins and then deploys it.
* If the bundle is already in the plugins' directory, it is simply deployed.
* If the plugin exists in a more recent version, the given plugin will not be installed.
* If the plugin is more recent than an already installed version, the old version is uninstalled and the new one
* is installed.
*
* @param bundleFile The bundleFile to deploy.
* @param start Indicate if the bundle should be started.
* @throws IllegalArgumentException If the bundleFile is not a directory.
* @throws FileNotFoundException If the bundleFile is not found.
* @throws NullPointerException If the bundleFile is null.
* @return The installed service.
*/
protected Object deployBundle(final File bundleFile, final boolean start) throws IllegalArgumentException, NullPointerException, IOException {
if(bundleFile == null) throw new NullPointerException("The bundleFile to deploy is null");
if(!bundleFile.exists()) throw new FileNotFoundException("The bundleFile does not exist");
if(!bundleFile.isFile()) throw new IllegalArgumentException("The bundleFile has to be a file");
if(!bundleFile.getParentFile().toPath().normalize().equals(this.pluginsDirectory.toPath())) {
Files.copy(bundleFile.toPath(), this.pluginsDirectory.toPath().resolve(bundleFile.getName()), StandardCopyOption.REPLACE_EXISTING);
}
Bundle bundle = null;
try {
bundle = this.osgiFramework.getBundleContext()
.installBundle(String.format("file:%1$s/%2$s", this.pluginsDirectory.getAbsolutePath(), bundleFile.getName()));
} catch (BundleException e) {
LOGGER.log(WARNING, "Can not install bundle", e);
}
if(bundle != null) {
final boolean isPluginInAnotherVersionInstalled = isPluginInAnotherVersionInstalled(bundle);
final boolean isPluginMostRecent = isPluginMostRecent(bundle);
if(isPluginInAnotherVersionInstalled && isPluginMostRecent) {
final Bundle pluginInAnotherVersion = getPluginInAnotherVersion(bundle);
uninstallBundle(pluginInAnotherVersion);
if(start) {
startBundle(bundle);
}
} else if(!isPluginInAnotherVersionInstalled && !isPluginActive(bundle) && isPluginMostRecent && start) {
startBundle(bundle);
} else if(isPluginInAnotherVersionInstalled && !isPluginMostRecent) {
uninstallBundle(bundle);
bundle = null;
}
}
Object service = null;
if(bundle != null && bundle.getRegisteredServices() != null && bundle.getRegisteredServices().length > 0) {
ServiceReference serviceReference = bundle.getRegisteredServices()[0];
service = osgiFramework.getBundleContext().getService(serviceReference);
}
return service;
}
/**
* Starts a given bundle. The bundle must already have been installed in the OSGi framework.
* @param bundle The bundle to start.
*/
protected void startBundle(Bundle bundle) {
try {
bundle.start();
} catch (BundleException e) {
LOGGER.log(WARNING, String.format("Can not install bundle [%1$s] in version [%2$s]", bundle.getSymbolicName(), bundle.getVersion()), e);
}
}
/**
* Uninstall a bundle from the OSGi container.
* @param bundle The bundle to uninstall.
*/
protected void uninstallBundle(Bundle bundle) {
if(bundle != null) {
try {
bundle.uninstall();
} catch (BundleException e) {
LOGGER.log(WARNING, String.format("Can not uninstall bundle [%1$s] in version [%2$s]", bundle.getSymbolicName(), bundle.getVersion().toString()));
}
try {
final File bundleFile = new File(new URL(bundle.getLocation()).getFile());
bundleFile.deleteOnExit();
} catch (MalformedURLException e) {
LOGGER.log(Level.SEVERE, "Can not determine bundle location", e);
}
}
}
/**
* Uninstall a bundle from the OSGi container. If the bundle file is found in the OSGi container, then it is
* uninstalled and the bundle file is marked for being deleted at the application's shutdown.
* @param bundleFile The bundle to uninstall.
* @throws FileNotFoundException If the bundle file doesn't exist.
* @throws BundleException If an error occurred while trying to remove the bundle.
*/
public void uninstallBundle(final File bundleFile) throws FileNotFoundException, BundleException {
if(bundleFile == null) throw new NullPointerException("The bundleFile to deploy is null");
if(!bundleFile.exists()) throw new FileNotFoundException("The bundleFile does not exist");
if(!bundleFile.isFile()) throw new IllegalArgumentException("The bundleFile has to be a file");
final Path bundlePath = bundleFile.toPath().toAbsolutePath();
final Bundle[] installedBundles = osgiFramework.getBundleContext().getBundles();
boolean continueSearching = true;
int index = 0;
while(continueSearching && index < installedBundles.length) {
final Bundle installedBundle = installedBundles[index++];
final File installedBundleFile;
try {
installedBundleFile = new File(new URL(installedBundle.getLocation()).getFile());
continueSearching = !bundlePath.equals(installedBundleFile.toPath().toAbsolutePath());
if(!continueSearching) {
uninstallBundle(installedBundle);
}
} catch (MalformedURLException e) {
LOGGER.log(Level.FINE, "Can not create the URL of the bundle: " + bundleFile.getName(), e);
}
}
}
/**
* Return the list of installed services which are from the given {@code serviceType} class.
* @param <T> The type of service.
* @param serviceType The class of service to look for.
* @return the list of installed services or an empty list if there is no service corresponding to the given class.
*/
public <T> List<T> getInstalledServices(Class<T> serviceType) {
final List<T> services = new ArrayList<>();
try {
Collection<ServiceReference<T>> references =
osgiFramework.getBundleContext().getServiceReferences(serviceType, "(objectClass=" + serviceType.getName() + ")");
references.stream().forEach(ref -> services.add(osgiFramework.getBundleContext().getService(ref)));
} catch (InvalidSyntaxException e) {
LOGGER.log(WARNING, "Can not list all installed service of type " + serviceType.getName());
}
return services;
}
/**
* Get the list of {@link InstalledPlugin} of the given type.
* @param pluginType The type of the plugin to list.
* @param <T> The type of the plugins.
* @return The list containing all installed plugins of the desired type.
*/
public <T extends IPlugin> List<InstalledPlugin> getInstalledPlugins(Class<T> pluginType) {
final List<InstalledPlugin> installedPlugins = new ArrayList<>();
try {
final Collection<ServiceReference<T>> services =
osgiFramework.getBundleContext().getServiceReferences(pluginType, "(objectClass=" + pluginType.getName() + ")");
installedPlugins.addAll(
services.stream()
.map(service -> {
final Bundle bundle = service.getBundle();
return new InstalledPlugin(bundle.getHeaders().get("Bundle-Name"), bundle.getVersion().toString());
})
.sorted((plugin1, plugin2) -> plugin1.getName().compareTo(plugin2.getName()))
.collect(Collectors.toList()));
} catch (InvalidSyntaxException e) {
LOGGER.log(WARNING, "Can not list all installed plugin of type " + pluginType.getName());
}
return installedPlugins;
}
/**
* Get the list of active plugins.
* @return The list of active plugins.
*/
public List<File> getActivePlugins() {
return Arrays.stream(this.osgiFramework.getBundleContext().getBundles())
.filter(bundle -> !SYSTEM_BUNDLE_LOCATION.equals(bundle.getLocation()))
.map(bundle -> {
try {
return new File(new URL(bundle.getLocation()).getFile());
} catch (MalformedURLException e) {
LOGGER.log(Level.SEVERE, "Can not determine plugin location", e);
return null;
}
})
.filter(file -> file != null)
.collect(Collectors.toList());
}
public Object getPresentationProperty(String property) {
Object value = null;
if(Presentations.getCurrentDisplayedPresentation() != null) {
if(PRESENTATION_FOLDER.equals(property)) {
value = Presentations.getCurrentDisplayedPresentation().getWorkingDirectory();
}
else if(PRESENTATION_RESOURCES_FOLDER.equals(property)) {
value = Presentations.getCurrentDisplayedPresentation().getTemplateConfiguration().getResourcesDirectory();
}
}
return value;
}
/**
* Checks if a given plugin is the most recent compared to installed plugin. If the plugin version is strictly
* greater than the first plugin's version found then it is considered as the most recent plugin.
* @param plugin The plugin to check.
* @return {@code true} if the plugin is the most recent, {@code false} otherwise.
*/
protected boolean isPluginMostRecent(final Bundle plugin) {
boolean isMostRecent;
final Version installedPluginVersion = Arrays.stream(osgiFramework.getBundleContext().getBundles())
.filter(installedPlugin -> {
boolean isSameName = installedPlugin.getSymbolicName().equals(plugin.getSymbolicName());
return isSameName;
})
.map(Bundle::getVersion)
.findFirst()
.orElse(null);
if(installedPluginVersion != null) {
isMostRecent = plugin.getVersion().compareTo(installedPluginVersion) >= 0;
} else {
isMostRecent = true;
}
return isMostRecent;
}
/**
* Checks if a given plugin is already installed in another version in the OSGi framework. The plugin's match is
* performed on the {@link Bundle#getSymbolicName() symbolic name} of the plugin.
* @param plugin The plugin to check.
* @return {@code true} if the plugin is installed in another version, {@code false} otherwise.
*/
protected boolean isPluginInAnotherVersionInstalled(final Bundle plugin) {
return Arrays.stream(osgiFramework.getBundleContext().getBundles())
.anyMatch(installedPlugin -> {
boolean isSameName = installedPlugin.getSymbolicName().equals(plugin.getSymbolicName());
boolean isNotSameVersion = !installedPlugin.getVersion().equals(plugin.getVersion());
return isSameName && isNotSameVersion;
});
}
/**
* Get the other version of the given plugin. The plugin's match is performed on the
* {@link Bundle#getSymbolicName() symbolic name} of the plugin.
* @param plugin The plugin to check.
* @return The plugin in the other version of the given plugin, {@code null} if not found.
*/
protected Bundle getPluginInAnotherVersion(final Bundle plugin) {
return Arrays.stream(osgiFramework.getBundleContext().getBundles())
.filter(installedPlugin -> {
boolean isSameName = installedPlugin.getSymbolicName().equals(plugin.getSymbolicName());
boolean isNotSameVersion = !installedPlugin.getVersion().equals(plugin.getVersion());
return isSameName && isNotSameVersion;
})
.findFirst()
.orElse(null);
}
/**
* Check if the given plugin is active or not. A plugin is considered active if it's state is equal to {@link Bundle#ACTIVE}
* or {@link Bundle#STARTING}.
* @param plugin The plugin to check.
* @return {@code true} if the plugin is active, {@code false} otherwise.
*/
protected boolean isPluginActive(final Bundle plugin) {
return Bundle.ACTIVE == plugin.getState() || Bundle.STARTING == plugin.getState();
}
/**
* Check if the given plugin is inactive or not.
* @param plugin The plugin to check.
* @return {@code true} if the plugin is inactive, {@code false} otherwise.
* @see #isPluginActive(Bundle)
*/
protected boolean isPluginInactive(final Bundle plugin) {
return !isPluginActive(plugin);
}
}