/* * RapidMiner * * Copyright (C) 2001-2008 by Rapid-I and the contributors * * Complete list of developers available at our web site: * * http://rapid-i.com * * This program is free software: you can redistribute it and/or modify * it under the terms of the GNU Affero General Public License as published by * the Free Software Foundation, either version 3 of the License, or * (at your option) any later version. * * This program 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 Affero General Public License for more details. * * You should have received a copy of the GNU Affero General Public License * along with this program. If not, see http://www.gnu.org/licenses/. */ package com.rapidminer.tools.plugin; import java.awt.Frame; import java.awt.Image; import java.io.BufferedReader; import java.io.File; import java.io.FilenameFilter; import java.io.IOException; import java.io.InputStream; import java.io.InputStreamReader; import java.lang.reflect.InvocationTargetException; import java.lang.reflect.Method; import java.net.MalformedURLException; import java.net.URL; import java.util.Iterator; import java.util.LinkedList; import java.util.List; import java.util.Properties; import java.util.jar.Attributes; import java.util.jar.JarFile; import java.util.jar.Manifest; import com.rapidminer.RapidMiner; import com.rapidminer.gui.MainFrame; import com.rapidminer.gui.templates.BuildingBlock; import com.rapidminer.gui.tools.AboutBox; import com.rapidminer.tools.LogService; import com.rapidminer.tools.OperatorService; import com.rapidminer.tools.ParameterService; import com.rapidminer.tools.ResourceSource; import com.rapidminer.tools.Tools; /** * <p> * The class for RapidMiner plugins. This class is used to encapsulate the .jar file * which must be in the <code>lib/plugins</code> subdirectory of RapidMiner. * Provides methods for plugin checks, operator registering, and getting * information about the plugin. * </p> * <p> * Plugin dependencies must be defined in the form <br /> * plugin_name1 (plugin_version1) # ... # plugin_nameM (plugin_versionM) < /br> * of the manifest parameter <code>Plugin-Dependencies</code>. You must * define both the name and the version of the desired plugins and separate them * with "#". * </p> * * @author Simon Fischer, Ingo Mierswa * @version $Id: Plugin.java,v 1.12 2008/07/12 17:46:46 ingomierswa Exp $ */ public class Plugin { /** The name for the manifest entry RapidMiner-Type which can be used to indicate that a jar file is a RapidMiner plugin. */ public static final String RAPIDMINER_TYPE = "RapidMiner-Type"; /** The value for the manifest entry RapidMiner-Type which indicates that a jar file is a RapidMiner plugin. */ public static final String RAPIDMINER_TYPE_PLUGIN = "RapidMiner_Plugin"; /** * The jar archive of the plugin which must be placed in the * <code>lib/plugins</code> subdirectory of RapidMiner. */ private JarFile archive; /** The file for this plugin. */ private File file; /** The class loader based on the plugin file. */ private PluginClassLoader classLoader; /** The name of the plugin. */ private String name; /** The version of the plugin. */ private String version; /** The vendor of the plugin. */ private String vendor; /** The url for this plugin (in WWW). */ private String url; /** The RapidMiner version which is needed for this plugin. */ private String necessaryRapidMinerVersion = "0"; /** The plugins and their versions which are needed for this plugin. */ private List<Dependency> pluginDependencies = new LinkedList<Dependency>(); /** The collection of all plugins. */ private static List<Plugin> allPlugins = new LinkedList<Plugin>(); /** Creates a new pluging based on the plugin .jar file. */ public Plugin(File file) throws IOException { this.file = file; this.archive = new JarFile(this.file); URL url = new URL("file", null, this.file.getAbsolutePath()); this.classLoader = new PluginClassLoader(new URL[] { url }); Tools.addResourceSource(new ResourceSource(this.classLoader)); getMetaData(); } /** Returns the name of the plugin. */ public String getName() { return name; } /** Returns the version of this plugin. */ public String getVersion() { return version; } /** Returns the necessary RapidMiner version. */ public String getNecessaryRapidMinerVersion() { return necessaryRapidMinerVersion; } /** Returns the plugin dependencies of this plugin. */ public List getPluginDependencies() { return pluginDependencies; } /** Returns the class loader of this plugin. */ public ClassLoader getClassLoader() { return this.classLoader; } /** Checks the RapidMiner version and plugin dependencies. */ private boolean checkDependencies(List plugins) { if (RapidMiner.getLongVersion().compareTo(necessaryRapidMinerVersion) < 0) return false; // other plugins Iterator i = pluginDependencies.iterator(); while (i.hasNext()) { Dependency dependency = (Dependency) i.next(); if (!dependency.isFulfilled(plugins)) return false; } // all ok return true; } /** Collects all meta data of the plugin from the manifest file. */ private void getMetaData() { try { java.util.jar.Attributes atts = archive.getManifest().getMainAttributes(); name = atts.getValue("Implementation-Title"); if (name == null) { name = archive.getName(); } version = atts.getValue("Implementation-Version"); if (version == null) version = ""; url = atts.getValue("Implementation-URL"); vendor = atts.getValue("Implementation-Vendor"); necessaryRapidMinerVersion = atts.getValue("RapidMiner-Version"); if (necessaryRapidMinerVersion == null) necessaryRapidMinerVersion = "0"; String dependencies = atts.getValue("Plugin-Dependencies"); if (dependencies == null) dependencies = ""; addDependencies(dependencies); RapidMiner.splashMessage("Loading " + name); } catch (Exception e) { e.printStackTrace(); } } /** Register plugin dependencies. */ private void addDependencies(String dependencies) { String[] singleDependencies = dependencies.trim().split("#"); for (int i = 0; i < singleDependencies.length; i++) { if (singleDependencies[i].trim().length() > 0) { String dependencyName = singleDependencies[i].trim(); String dependencyVersion = "0"; if (singleDependencies[i].trim().indexOf("[") >= 0) { dependencyName = singleDependencies[i].trim().substring(0, singleDependencies[i].trim().indexOf("[")).trim(); dependencyVersion = singleDependencies[i].trim().substring(singleDependencies[i].trim().indexOf("[") + 1, singleDependencies[i].trim().indexOf("]")).trim(); } pluginDependencies.add(new Dependency(dependencyName, dependencyVersion)); } } } /** Register the operators of this plugin in RapidMiner. */ public void register() { InputStream in = null; // trying normal plugins URL operatorsURL = this.classLoader.getResource("META-INF/operators.xml"); if (operatorsURL == null) { operatorsURL = this.classLoader.getResource("operators.xml"); if (operatorsURL != null) { LogService.getGlobal().log(name + ": putting operators.xml in root directory of jar is deprecated. Use META-INF directory instead!", LogService.WARNING); } } if (operatorsURL != null) { // register operators try { in = operatorsURL.openStream(); } catch (IOException e) { LogService.getGlobal().log("Cannot read operators.xml from '" + archive.getName() + "'!", LogService.ERROR); } } else { // if no operators.xml found: Try via PluginInit method getOperatorStream() try { Class<?> pluginInitator = Class.forName("com.rapidminer.PluginInit",false, getClassLoader()); Method registerOperatorMethod = pluginInitator.getMethod("getOperatorStream", new Class[] {ClassLoader.class}); in = (InputStream) registerOperatorMethod.invoke(null, new Object[] {getClassLoader()}); } catch (ClassNotFoundException e) { } catch (SecurityException e) { } catch (NoSuchMethodException e) { } catch (IllegalArgumentException e) { } catch (IllegalAccessException e) { } catch (InvocationTargetException e) { } } if (in != null) { // add URLs of plugins this plugin depends on Iterator i = pluginDependencies.iterator(); while (i.hasNext()) { String pluginName = ((Dependency) i.next()).getPluginName(); Plugin other = getPlugin(pluginName); mergeClassLoader(other); } //LogService.getGlobal().log("Loading " + name, LogService.INIT); OperatorService.registerOperators(archive.getName(), in, this.classLoader, true); } } /** Returns a list of building blocks. If this plugin does not define any * building blocks, an empty list will be returned. */ public List<BuildingBlock> getBuildingBlocks() { List<BuildingBlock> result = new LinkedList<BuildingBlock>(); URL url = null; try { url = new URL("file", null, this.file.getAbsolutePath()); } catch (MalformedURLException e1) { LogService.getGlobal().log("Cannot load plugin building blocks. Skipping...", LogService.ERROR); } if (url != null) { ClassLoader independentLoader = new PluginClassLoader(new URL[] { url }); URL bbDefinition = independentLoader.getResource(Tools.RESOURCE_PREFIX + "buildingblocks.txt"); if (bbDefinition != null) { BufferedReader in = null; try { in = new BufferedReader(new InputStreamReader(bbDefinition.openStream())); String line = null; while ((line = in.readLine()) != null) { URL bbURL = this.classLoader.getResource(Tools.RESOURCE_PREFIX + line); BufferedReader bbIn = null; try { bbIn = new BufferedReader(new InputStreamReader(bbURL.openStream())); result.add(new BuildingBlock(bbIn)); } catch (IOException e) { LogService.getGlobal().log("Cannot load plugin building blocks. Skipping...", LogService.ERROR); } finally { if (bbIn != null) { bbIn.close(); } } } } catch (IOException e) { LogService.getGlobal().log("Cannot load plugin building blocks.", LogService.WARNING); } finally { if (in != null) { try { in.close(); } catch (IOException e) { LogService.getGlobal().log("Cannot close stream to plugin building blocks.", LogService.ERROR); } } } } } return result; } /** Creates the about box for this plugin. */ public AboutBox createAboutBox(Frame owner, Image productLogo) { String about = ""; try { URL url = this.classLoader.getResource("META-INF/ABOUT.NFO"); if (url != null) about = Tools.readTextFile(new InputStreamReader(url.openStream())); } catch (Throwable e) {} return new AboutBox(owner, name, version, "Vendor: " + ((vendor != null) ? vendor : "unknown"), "URL: " + ((url != null) ? url : "unknown"), about, productLogo); } /** Returns a list of Plugins found in the plugins directory. */ public static void findPlugins(File pluginDir, boolean showWarningForNonPluginJars) { if (!(pluginDir.exists() && pluginDir.isDirectory())) return; File[] files = pluginDir.listFiles(new FilenameFilter() { public boolean accept(File dir, String name) { return name.endsWith(".jar"); } }); allPlugins = new LinkedList<Plugin>(); for (int i = 0; i < files.length; i++) { try { JarFile jarFile = new JarFile(files[i]); Manifest manifest = jarFile.getManifest(); Attributes attributes = manifest.getMainAttributes(); if (RAPIDMINER_TYPE_PLUGIN.equals(attributes.getValue(RAPIDMINER_TYPE))) { allPlugins.add(new Plugin(files[i])); } else { if (showWarningForNonPluginJars) LogService.getGlobal().logWarning("The jar file '" + jarFile.getName() + "' does not contain an entry '" + RAPIDMINER_TYPE + "' in its manifest and will therefore not be loaded (if this file actually is a plugin updating the plugin file might help)."); } } catch (Throwable e) { LogService.getGlobal().log("Cannot load plugin '" + files[i] + "': " + e.getMessage(), LogService.ERROR); } } } /** * Adds the URLs of the given Plugin to class loader of this one. This * method should only be used to resolve plugin dependencies. */ protected void mergeClassLoader(Plugin other) { mergeClassLoaders(other.classLoader, this.classLoader); } /** Merges the URL from the first classloader into the second one. */ private static void mergeClassLoaders(PluginClassLoader first, PluginClassLoader second) { URL[] otherURLs = first.getURLs(); //this.classLoader = new PluginClassLoader(otherURLs); for (int i = 0; i < otherURLs.length; i++) second.addDependingURL(otherURLs[i]); } public String toString() { return name + " " + version + " (" + archive.getName() + ") depending on " + pluginDependencies; } /** Returns a list of Plugins found in the given plugins directory. If the given directory is null, * then RapidMiner tries to find plugins in the directory rapidminer.home/lib/plugins. */ public static void registerAllPlugins(File pluginDirectory, boolean showWarningForNonPluginJars) { File pluginDir = pluginDirectory; if (pluginDir == null) pluginDir = ParameterService.getPluginDir(); findPlugins(pluginDir, showWarningForNonPluginJars); Iterator i = allPlugins.iterator(); while (i.hasNext()) { Plugin plugin = (Plugin) i.next(); if (!plugin.checkDependencies(allPlugins)) { LogService.getGlobal().log("Cannot register operators from '" + plugin.getName() + "': Dependencies not fulfilled! This plugin needs a RapidMiner version " + plugin.getNecessaryRapidMinerVersion() + " and the following plugins:" + Tools.getLineSeparator() + plugin.getPluginDependencies(), LogService.ERROR); i.remove(); } } if (allPlugins.size() > 0) { //LogService.getGlobal().log("Found " + allPlugins.size() + " plugins in " + ParameterService.getPluginDir(), LogService.INIT); i = allPlugins.iterator(); while (i.hasNext()) { ((Plugin) i.next()).register(); } } } /** Returns a class loader which is able to load all classes (core _and_ all plugins). */ public static ClassLoader getMajorClassLoader() { PluginClassLoader majorPluginClassLoader = null; Iterator<Plugin> i = allPlugins.iterator(); while (i.hasNext()) { Plugin plugin = i.next(); if (majorPluginClassLoader == null) { majorPluginClassLoader = plugin.classLoader; } else { mergeClassLoaders(plugin.classLoader, majorPluginClassLoader); } } if (majorPluginClassLoader == null) { return ClassLoader.getSystemClassLoader(); } else { return majorPluginClassLoader; } } /** Returns the collection of all plugins. */ public static List<Plugin> getAllPlugins() { return allPlugins; } /** Returns the desired plugin. */ public static Plugin getPlugin(String name) { Iterator<Plugin> i = allPlugins.iterator(); while (i.hasNext()) { Plugin plugin = i.next(); if (plugin.getName().equals(name)) return plugin; } return null; } /** * This method will try to invoke the method void initGui(MainFrame) of * PluginInit class of every plugin. */ public static void initPluginGuis(MainFrame mainframe) { callPluginInitMethods("initGui", new Class[] {MainFrame.class}, new Object[] {mainframe}); } public static void initPluginUpdateManager() { callPluginInitMethods("initPluginManager", new Class[] {}, new Object[] {}); } public static void initFinalChecks() { callPluginInitMethods("initFinalChecks", new Class[] {}, new Object[] {}); } private static void callPluginInitMethods(String methodName, Class[] arguments, Object[] argumentValues) { List<Plugin> plugins = getAllPlugins(); for (Plugin plugin: plugins) { try { Class<?> pluginInitator = Class.forName("com.rapidminer.PluginInit",false, plugin.getClassLoader()); Method initGuiMethod = pluginInitator.getMethod(methodName, arguments); initGuiMethod.invoke(null, argumentValues); } catch (ClassNotFoundException e) { } catch (SecurityException e) { } catch (NoSuchMethodException e) { } catch (IllegalArgumentException e) { } catch (IllegalAccessException e) { } catch (InvocationTargetException e) { } } } public static void initPluginSplashTexts() { callPluginInitMethods("initSplashTexts", new Class[] {}, new Object[] {}); } public static void initAboutTexts(Properties properties) { callPluginInitMethods("initAboutTexts", new Class[] {Properties.class}, new Object[] {properties}); } public boolean showAboutBox() { try { Class<?> pluginInitator = Class.forName("com.rapidminer.PluginInit",false, this.getClassLoader()); Method initGuiMethod = pluginInitator.getMethod("showAboutBox", new Class[] {}); Boolean showAboutBox = (Boolean) initGuiMethod.invoke(null, new Object[] {}); return showAboutBox.booleanValue(); } catch (ClassNotFoundException e) { } catch (SecurityException e) { } catch (NoSuchMethodException e) { } catch (IllegalArgumentException e) { } catch (IllegalAccessException e) { } catch (InvocationTargetException e) { } return true; } }