package org.joget.plugin.base; import org.joget.commons.util.LogUtil; import org.joget.commons.util.SetupManager; import java.io.BufferedInputStream; import java.io.File; import java.io.FileInputStream; import java.io.FileOutputStream; import java.io.IOException; import java.io.InputStream; import java.net.MalformedURLException; import java.net.URI; import java.net.URL; import java.util.ArrayList; import java.util.Collection; import java.util.Map; import java.util.Properties; import java.util.jar.JarFile; import org.apache.felix.framework.Felix; import org.apache.felix.framework.util.StringMap; import org.osgi.framework.Bundle; import org.osgi.framework.BundleContext; import org.osgi.framework.ServiceReference; import org.springframework.beans.BeansException; import org.springframework.context.ApplicationContext; import org.springframework.context.ApplicationContextAware; public class PluginManager implements ApplicationContextAware { private Felix felix = null; private String baseDirectory = SetupManager.getBaseSharedDirectory() + File.separator + "plugins"; private ApplicationContext applicationContext; public PluginManager() { init(); } public PluginManager(String baseDirectory) { if (baseDirectory != null) { this.baseDirectory = baseDirectory; } init(); } /** * Retrieves plugin base directory from system setup */ public String getBaseDirectory() { try { SetupManager setupManager = (SetupManager) applicationContext.getBean("setupManager"); String dataFileBasePath = setupManager.getSettingValue("dataFileBasePath"); if (dataFileBasePath != null && dataFileBasePath.length() > 0) { return dataFileBasePath + File.separator + "plugins"; } else { return baseDirectory; } } catch (Exception ex) { return baseDirectory; } } /** * Initializes the plugin manager */ protected void init() { Properties config = new Properties(); try { config.load(getClass().getClassLoader().getResourceAsStream("config.properties")); } catch (IOException ex) { LogUtil.error(PluginManager.class.getName(), ex, ""); } // Create a case-insensitive configuration property map. Map configMap = new StringMap(false); configMap.putAll(config); // Configure the Felix instance to be embedded. // Explicitly specify the directory to use for caching bundles. String targetCache = "target/felix-cache/"; String tempDir = System.getProperty("java.io.tmpdir"); File targetDir = new File(tempDir, targetCache); File targetCacheDir = new File(targetDir, "cache"); // locate empty cache directory to use boolean proceed = false; int count = 0; while(!proceed) { // check for existing cache String dirName = "cache" + count; targetCacheDir = new File(targetDir, dirName); File[] bundles = targetCacheDir.listFiles(); proceed = bundles == null || bundles.length <= 1; count++; } // set configuration configMap.put("org.osgi.framework.storage", targetCacheDir.getAbsolutePath()); configMap.put("felix.log.level", "0"); configMap.put("org.osgi.framework.storage.clean", "onFirstInit"); try { if (felix == null) { felix = new Felix(configMap); felix.start(); } //refresh(); LogUtil.info(PluginManager.class.getName(), "PluginManager initialized"); } catch (Exception ex) { LogUtil.error(PluginManager.class.getName(), ex, "Could not create framework"); } } /** * Find and install plugins from the baseDirectory */ public void refresh() { uninstallAll(false); installBundles(); } protected void installBundles() { Collection<URL> urlList = new ArrayList<URL>(); File baseDirFile = new File(getBaseDirectory()); recurseDirectory(urlList, baseDirFile); Collection<Bundle> bundleList = new ArrayList<Bundle>(); for (URL url : urlList) { // install the JAR file as a bundle String location = url.toExternalForm(); Bundle bundle = installBundle(location); if (bundle != null) { bundleList.add(bundle); } } for (Bundle bundle : bundleList) { startBundle(bundle); } } protected void recurseDirectory(Collection<URL> urlList, File baseDirFile) { File[] files = baseDirFile.listFiles(); if (files != null) { for (File file : files) { //LogUtil.info(getClass().getName(), " -" + file.getName()); if (file.isFile() && file.getName().toLowerCase().endsWith(".jar")) { try { urlList.add(file.toURI().toURL()); LogUtil.debug(PluginManager.class.getName(), " found jar " + file.toURI().toURL()); } catch (MalformedURLException ex) { LogUtil.error(PluginManager.class.getName(), ex, ""); } } else if (file.isDirectory()) { recurseDirectory(urlList, file); } } } } protected Bundle installBundle(String location) { try { BundleContext context = felix.getBundleContext(); Bundle newBundle = context.installBundle(location); if (newBundle.getSymbolicName() == null) { newBundle.uninstall(); newBundle = null; } else { newBundle.update(); } return newBundle; } catch (Exception be) { LogUtil.error(PluginManager.class.getName(), be, "Failed bundle installation from " + location + ": " + be.toString()); return null; } } protected boolean startBundle(Bundle bundle) { try { //bundle.update(); bundle.start(); LogUtil.info(PluginManager.class.getName(), "Bundle " + bundle.getSymbolicName() + " started"); } catch (Exception be) { LogUtil.error(PluginManager.class.getName(), be, "Failed bundle start for " + bundle + ": " + be.toString()); return true; } return false; } /** * List registered plugins * @return */ public Collection<Plugin> list() { Collection<Plugin> list = new ArrayList<Plugin>(); BundleContext context = felix.getBundleContext(); Bundle[] bundles = context.getBundles(); for (Bundle b : bundles) { ServiceReference[] refs = b.getRegisteredServices(); if (refs != null) { for (ServiceReference sr : refs) { LogUtil.debug(PluginManager.class.getName(), " bundle service: " + sr); Object obj = context.getService(sr); if (obj instanceof Plugin) { list.add((Plugin) obj); } context.ungetService(sr); } } } return list; } /** * Disable plugin * @param name */ public boolean disable(String name) { boolean result = false; BundleContext context = felix.getBundleContext(); ServiceReference sr = context.getServiceReference(name); if (sr != null) { try { sr.getBundle().stop(); context.ungetService(sr); result = true; } catch (Exception ex) { LogUtil.error(PluginManager.class.getName(), ex, ""); } } return result; } /** * Install a new plugin * @return */ public boolean upload(String filename, InputStream in) { String location = null; File outputFile = null; try { // check filename if (filename == null || filename.trim().length() == 0) { throw new PluginException("Invalid plugin name"); } if (!filename.endsWith(".jar")) { filename += ".jar"; } // write file FileOutputStream out = null; try { outputFile = new File(getBaseDirectory(), filename); File outputDir = outputFile.getParentFile(); if (!outputDir.exists()) { outputDir.mkdirs(); } out = new FileOutputStream(outputFile); BufferedInputStream bin = new BufferedInputStream(in); int len = 0; byte[] buffer = new byte[4096]; while ((len = bin.read(buffer)) > 0) { out.write(buffer, 0, len); } out.flush(); location = outputFile.toURI().toURL().toExternalForm(); } finally { try { if (out != null) { out.close(); } if (in != null) { in.close(); } } catch (IOException ex) { LogUtil.error(PluginManager.class.getName(), ex, ""); } } // validate jar file boolean isValid = false; try { JarFile jarFile = new JarFile(outputFile); isValid = true; } catch (IOException ex) { //delete invalid file try { outputFile.delete(); } catch (Exception e) { LogUtil.error(PluginManager.class.getName(), ex, ""); } LogUtil.error(PluginManager.class.getName(), ex, ""); throw new PluginException("Invalid jar file"); } catch (Exception ex) { LogUtil.error(PluginManager.class.getName(), ex, ""); throw new PluginException("Invalid jar file"); } // install if (location != null && isValid) { Bundle newBundle = installBundle(location); if (newBundle != null) { startBundle(newBundle); } } return true; } catch (Exception ex) { LogUtil.error(PluginManager.class.getName(), ex, ""); throw new PluginException("Unable to write plugin file", ex); } } /** * Uninstall/remove all plugin, without deleting the plugin file * @param name * @return */ public void uninstallAll(boolean deleteFiles) { Collection<Plugin> pluginList = this.list(); for (Plugin plugin : pluginList) { uninstall(plugin.getClass().getName(), deleteFiles); } } /** * Uninstall/remove a plugin, and delete the plugin file * @param name * @return */ public boolean uninstall(String name) { return uninstall(name, true); } /** * Uninstall/remove a plugin * @param name * @return */ public boolean uninstall(String name, boolean deleteFile) { boolean result = false; BundleContext context = felix.getBundleContext(); ServiceReference sr = context.getServiceReference(name); if (sr != null) { try { Bundle bundle = sr.getBundle(); bundle.stop(); bundle.uninstall(); String location = bundle.getLocation(); context.ungetService(sr); // delete location if (deleteFile) { File file = new File(new URI(location)); boolean deleted = file.delete(); } result = true; } catch (Exception ex) { LogUtil.error(PluginManager.class.getName(), ex, ""); } } return result; } public Plugin getPlugin(String name) { Plugin plugin = null; try { BundleContext context = felix.getBundleContext(); ServiceReference sr = context.getServiceReference(name); if (sr != null) { Object obj = context.getService(sr); //Class clazz = sr.getBundle().loadClass(name); //Object obj = clazz.newInstance(); boolean isPlugin = obj instanceof Plugin; LogUtil.debug(PluginManager.class.getName(), " plugin obj " + obj + " class: " + obj.getClass().getName() + " " + isPlugin); LogUtil.debug(PluginManager.class.getName(), " plugin classloader: " + obj.getClass().getClassLoader()); LogUtil.debug(PluginManager.class.getName(), " current classloader: " + Plugin.class.getClassLoader()); if (isPlugin) { plugin = (Plugin) obj; } context.ungetService(sr); } } catch (Exception ex) { LogUtil.error(PluginManager.class.getName(), ex, ""); throw new PluginException("Plugin " + name + " could not be retrieved", ex); } return plugin; } /** * Execute a plugin * @param name The fully qualified class name of the plugin * @param properties * @return */ public Object execute(String name, Map properties) { Object result = null; Plugin plugin = getPlugin(name); if (plugin != null) { result = plugin.execute(properties); LogUtil.info(PluginManager.class.getName(), " Executed plugin " + plugin + ": " + result); } else { LogUtil.info(PluginManager.class.getName(), " Plugin " + name + " not found"); } return result; } /** * Stop the plugin manager */ public void shutdown() { if (felix != null) { try { uninstallAll(false); felix.stop(); } catch (Exception ex) { LogUtil.error(PluginManager.class.getName(), ex, "Could not stop Felix"); } felix = null; } } @Override public void finalize() { shutdown(); } public void testPlugin(String name, String location, Map properties, boolean override) { LogUtil.info(PluginManager.class.getName(), "====testPlugin===="); // check for existing plugin Plugin plugin = getPlugin(name); boolean existing = (plugin != null); boolean install = (location != null && location.trim().length() > 0); // install plugin if (install && (!existing || override)) { InputStream in = null; try { LogUtil.info(PluginManager.class.getName(), " ===install=== "); File file = new File(location); if (file.exists()) { in = new FileInputStream(file); upload(file.getName(), in); } } catch (Exception ex) { LogUtil.error(PluginManager.class.getName(), ex, ""); } finally { try { if (in != null) { in.close(); } } catch (IOException ex) { LogUtil.error(PluginManager.class.getName(), ex, ""); } } } // execute plugin LogUtil.info(PluginManager.class.getName(), " ===execute=== "); Object result = execute(name, properties); LogUtil.info(PluginManager.class.getName(), " result: " + result); // uninstall plugin if (install && (!existing || override)) { LogUtil.info(PluginManager.class.getName(), " ===uninstall=== "); uninstall(name); } LogUtil.info(PluginManager.class.getName(), "====testPlugin end===="); } public static void main(String[] args) { // String pluginDirectory = "target/wflow-bundles"; PluginManager pm = new PluginManager(); FileInputStream in = null; try { LogUtil.info(PluginManager.class.getName(), " ===Plugin List=== "); for (Plugin p : pm.list()) { LogUtil.info(PluginManager.class.getName(), " plugin: " + p.getName() + "; " + p.getClass().getName()); } String samplePluginFile = "../wflow-plugins/wflow-plugin-sample/target/wflow-plugin-sample-2.0-SNAPSHOT.jar"; String samplePlugin = "org.joget.plugin.sample.SamplePlugin"; try { LogUtil.info(PluginManager.class.getName(), " ===Install SamplePlugin=== "); File file = new File(samplePluginFile); in = new FileInputStream(file); pm.upload(file.getName(), in); } catch (Exception ex) { LogUtil.error(PluginManager.class.getName(), ex, ""); } finally { try { if (in != null) { in.close(); } } catch (IOException ex) { LogUtil.error(PluginManager.class.getName(), ex, ""); } } LogUtil.info(PluginManager.class.getName(), " ===Plugin List after install=== "); for (Plugin p : pm.list()) { LogUtil.info(PluginManager.class.getName(), " plugin: " + p.getName() + "; " + p.getClass().getName()); } LogUtil.info(PluginManager.class.getName(), " ===Execute SamplePlugin=== "); pm.execute(samplePlugin, null); LogUtil.info(PluginManager.class.getName(), " ===Uninstall SamplePlugin=== "); pm.uninstall(samplePlugin); LogUtil.info(PluginManager.class.getName(), " ===New Plugin List after removal=== "); for (Plugin p : pm.list()) { LogUtil.info(PluginManager.class.getName(), " plugin: " + p.getName() + "; " + p.getClass().getName()); } pm.refresh(); LogUtil.info(PluginManager.class.getName(), " ===New Plugin List after refresh=== "); for (Plugin p : pm.list()) { LogUtil.info(PluginManager.class.getName(), " plugin: " + p.getName() + "; " + p.getClass().getName()); } pm.testPlugin(samplePlugin, samplePluginFile, null, true); } finally { pm.shutdown(); } } public Object getBean(String beanName) { Object bean = null; if (applicationContext != null) { bean = applicationContext.getBean(beanName); } return bean; } public void setApplicationContext(ApplicationContext appContext) throws BeansException { this.applicationContext = appContext; refresh(); } }