/* * PluginJAR.java - Controls JAR loading and unloading * :tabSize=4:indentSize=4:noTabs=false: * :folding=explicit:collapseFolds=1: * * Copyright (C) 1999, 2004 Slava Pestov * * This program is free software; you can redistribute it and/or * modify it under the terms of the GNU General Public License * as published by the Free Software Foundation; either version 2 * of the License, or 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 General Public License for more details. * * You should have received a copy of the GNU General Public License * along with this program; if not, write to the Free Software * Foundation, Inc., 59 Temple Place - Suite 330, Boston, MA 02111-1307, USA. */ package org.gjt.sp.jedit; //{{{ Imports import java.awt.EventQueue; import java.io.BufferedInputStream; import java.io.BufferedOutputStream; import java.io.Closeable; import java.io.DataInputStream; import java.io.DataOutputStream; import java.io.File; import java.io.FileInputStream; import java.io.FileNotFoundException; import java.io.FileOutputStream; import java.io.IOException; import java.io.InputStream; import java.io.Reader; import java.lang.reflect.Modifier; import java.net.URL; import java.util.ArrayList; import java.util.Collection; import java.util.Collections; import java.util.Enumeration; import java.util.HashMap; import java.util.HashSet; import java.util.LinkedHashSet; import java.util.LinkedList; import java.util.List; import java.util.Map; import java.util.Properties; import java.util.Set; import java.util.StringTokenizer; import java.util.regex.Matcher; import java.util.regex.Pattern; import java.util.zip.ZipEntry; import java.util.zip.ZipFile; import javax.swing.SwingUtilities; import org.gjt.sp.jedit.browser.VFSBrowser; import org.gjt.sp.jedit.buffer.DummyFoldHandler; import org.gjt.sp.jedit.buffer.FoldHandler; import org.gjt.sp.jedit.gui.DockableWindowFactory; import org.gjt.sp.jedit.gui.DockableWindowManager; import org.gjt.sp.jedit.io.CharsetEncoding; import org.gjt.sp.jedit.msg.PluginUpdate; import org.gjt.sp.jedit.msg.PropertiesChanged; import org.gjt.sp.util.Log; import org.gjt.sp.util.StandardUtilities; import org.gjt.sp.util.IOUtilities; import static org.gjt.sp.jedit.EditBus.EBHandler; //}}} /** * Loads and unloads plugins.<p> * * <h3>JAR file contents</h3> * * When loading a plugin, jEdit looks for the following resources: * * <ul> * <li>A file named <code>actions.xml</code> defining plugin actions. * Only one such file per plugin is allowed. See {@link ActionSet} for * syntax.</li> * <li>A file named <code>browser.actions.xml</code> defining file system * browser actions. * Only one such file per plugin is allowed. See {@link ActionSet} for * syntax.</li> * <li>A file named <code>dockables.xml</code> defining dockable windows. * Only one such file per plugin is allowed. See {@link * DockableWindowManager} for * syntax.</li> * <li>A file named <code>services.xml</code> defining additional services * offered by the plugin, such as virtual file systems. * Only one such file per plugin is allowed. See {@link * ServiceManager} for * syntax.</li> * <li>File with extension <code>.props</code> containing name/value pairs * separated by an equals sign. * A plugin can supply any number of property files. Property files are used * to define plugin men items, plugin option panes, as well as arbitriary * settings and strings used by the plugin. See {@link EditPlugin} for * information about properties used by jEdit. See * <code>java.util.Properties</code> for property file syntax.</li> * <li>Since jEdit 5.0, files named lang_[language_iso_code].properties are * localization files. If one of those files match the current language, jEdit * will load it. If a label is missing in the localization file, it will be * loaded from the other .props files. * Those files will be ignored by jEdit's versions older than 5.0 and do not * cause any problem * See <a href="http://sourceforge.net/apps/mediawiki/jedit/index.php?title=Localization"> * jEdit's localization wiki</a> * </ul> * * For a plugin to actually do something once it is resident in memory, * it must contain a class whose name ends with <code>Plugin</code>. * This class, known as the <i>plugin core class</i> must extend * {@link EditPlugin} and define a few required properties, otherwise it is * ignored. * * <h3>Dynamic and deferred loading</h3> * * Unlike in prior jEdit versions, jEdit 4.2 and later allow * plugins to be added and removed to the resident set at any time using * the {@link jEdit#addPluginJAR(String)} and * {@link jEdit#removePluginJAR(PluginJAR,boolean)} methods. Furthermore, the * plugin core class might not be loaded until the plugin is first used. See * {@link EditPlugin#start()} for a full description. * * * @see jEdit#getProperty(String) * @see jEdit#getPlugin(String) * @see jEdit#getPlugins() * @see jEdit#getPluginJAR(String) * @see jEdit#getPluginJARs() * @see jEdit#addPluginJAR(String) * @see jEdit#removePluginJAR(PluginJAR,boolean) * @see ActionSet * @see DockableWindowManager * @see OptionPane * @see PluginJAR * @see ServiceManager * * @author Slava Pestov * @version $Id: PluginJAR.java 23624 2014-07-28 09:21:28Z kpouer $ * @since jEdit 4.2pre1 */ public class PluginJAR { //{{{ Instance variables private final String path; private String cachePath; private final File file; private final JARClassLoader classLoader; private ZipFile zipFile; private Properties properties; private Map<String, Properties> localizationProperties; /** * The class list contained in this jar. */ private String[] classes; /** * The resource list in this jar. */ private String[] resources; private ActionSet actions; private ActionSet browserActions; private EditPlugin plugin; private URL dockablesURI; private URL servicesURI; private boolean activated; // Lists of jarPaths /** These plugins require this plugin */ private final Set<String> theseRequireMe = new LinkedHashSet<String>(); /** The plugins that uses me as optional dependency. */ private final Set<String> theseUseMe = new LinkedHashSet<String>(); /** This plugin requires these plugins. */ private final Set<String> weRequireThese = new LinkedHashSet<String>(); /** These plugins are an optional dependency for me, I'll use them if they are available, no worries if they aren't. */ private final Set<String> weUseThese = new LinkedHashSet<String>(); //}}} //{{{ load(String jarPath, boolean activateDependentIfNecessary) /** * Loads a plugin, and its dependent plugins if necessary. * * @since jEdit 4.3pre7 */ public static PluginJAR load(String path, boolean loadDependents) { PluginJAR jar = jEdit.getPluginJAR(path); if (jar != null && jar.getPlugin() != null) { return jar; } jEdit.addPluginJAR(path); jar = jEdit.getPluginJAR(path); if (jar == null) return null; EditPlugin plugin = jar.getPlugin(); if (plugin == null) { // No plugin, maybe it is a library return jar; } String className = plugin.getClassName(); if (loadDependents) { Set<String> pluginLoadList = getDependencySet(className); for (String jarName: pluginLoadList) { String jarPath = findPlugin(jarName); if (jarPath == null) { Log.log(Log.WARNING, PluginJAR.class, "Unable to load dependency " + jarName+ " the plugin is not installed"); continue; } load(jarPath, true); } } // Load extra jars that are part of this plugin String jars = jEdit.getProperty("plugin." + className + ".jars"); if(jars != null) { String dir = MiscUtilities.getParentOfPath(path); StringTokenizer st = new StringTokenizer(jars); while(st.hasMoreTokens()) { String _jarPath = MiscUtilities.constructPath(dir,st.nextToken()); PluginJAR _jar = jEdit.getPluginJAR(_jarPath); if(_jar == null) { jEdit.addPluginJAR(_jarPath); } } } jar.checkDependencies(); jar.activatePluginIfNecessary(); jEdit.propertiesChanged(); // check all other installed plugins to see if any of them // use me. Reload those that do so the classloaders work together. PluginJAR[] installedPlugins = jEdit.getPluginJARs(); for (PluginJAR installed : installedPlugins) { if (installed == null || installed.equals(jar) ) { continue; } EditPlugin ep = installed.getPlugin(); if (ep == null) { continue; } String installedClassname = ep.getClassName(); PluginDepends[] deps = getPluginDepends(installedClassname); for (PluginDepends dep : deps) { if ("plugin".equals(dep.what) && className.equals(dep.name)) { String reloadPath = ep.getPluginJAR().getPath(); jEdit.removePluginJAR(ep.getPluginJAR(), false); load(reloadPath, true); } } } return jar; } // }}} //{{{ getPath() method /** * Returns the full path name of this plugin's JAR file. */ public String getPath() { return path; } //}}} //{{{ findPlugin() method /** * Unlike getPlugin(), will return a PluginJAR that is not yet loaded, * given its classname. * * @param className a class name * @return the JARpath of the first PluginJAR it can find which contains this className, * or null if not found. * @since 4.3pre7 */ public static String findPlugin(String className) { EditPlugin ep = jEdit.getPlugin(className); if (ep != null) return ep.getPluginJAR().getPath(); for (String JARpath: jEdit.getNotLoadedPluginJARs()) { PluginJAR pjar = new PluginJAR(new File(JARpath)); if (pjar.containsClass(className)) { return JARpath; } } return null; } // }}} //{{{ containsClass() function /** * @param className a class name * @return true if this jar contains a class with that classname. * @since jedit 4.3pre7 */ boolean containsClass(String className) { try { getZipFile(); } catch (IOException ioe) { throw new RuntimeException(ioe); } Enumeration<? extends ZipEntry> itr = zipFile.entries(); while (itr.hasMoreElements()) { String entry = itr.nextElement().toString(); if (entry.endsWith(".class")) { String name = entry.substring(0, entry.length() - 6).replace('/', '.'); if (name.equals(className)) return true; } } return false; } // }}} //{{{ getCachePath() method /** * Returns the full path name of this plugin's summary file. * The summary file is used to store certain information which allows * loading of the plugin's resources and core class to be deferred * until the plugin is first used. As long as a plugin is using the * jEdit 4.2 plugin API, no extra effort is required to take advantage * of the summary cache. */ public String getCachePath() { return cachePath; } //}}} //{{{ getDependencySet() method /** * * @param className of a plugin that we wish to load * @return an ordered set of JARpaths that contains the * plugins that need to be (re)loaded, in the correct order. */ public static Set<String> getDependencySet(String className) { Set<String> retval = new LinkedHashSet<String>(); PluginDepends[] deps = getPluginDepends(className); for (PluginDepends pluginDepends : deps) { if (pluginDepends.optional) { continue; } if("plugin".equals(pluginDepends.what)) { int index2 = pluginDepends.arg.indexOf(' '); if ( index2 == -1) { Log.log(Log.ERROR, PluginJAR.class, className + " has an invalid dependency: " + pluginDepends.dep + " (version is missing)"); continue; } String pluginName = pluginDepends.arg.substring(0,index2); String needVersion = pluginDepends.arg.substring(index2 + 1); //todo : check version ? Set<String> loadTheseFirst = getDependencySet(pluginName); loadTheseFirst.add(pluginName); loadTheseFirst.addAll(retval); retval = loadTheseFirst; } } return retval; } // }}} //{{{ getFile() method /** * Returns a file pointing to the plugin JAR. */ public File getFile() { return file; } //}}} //{{{ getClassLoader() method /** * Returns the plugin's class loader. */ public JARClassLoader getClassLoader() { return classLoader; } //}}} //{{{ getZipFile() method /** * Returns the plugin's JAR file, opening it if necessary. * @since jEdit 4.2pre1 */ public synchronized ZipFile getZipFile() throws IOException { if(zipFile == null) { Log.log(Log.DEBUG,this,"Opening " + path); zipFile = new ZipFile(path); } return zipFile; } //}}} //{{{ getActionSet() method /** * Returns the plugin's action set for the jEdit action context * {@link jEdit#getActionContext()}. These actions are loaded from * the <code>actions.xml</code> file; see {@link ActionSet}. *. * @since jEdit 4.2pre1 */ public ActionSet getActionSet() { return actions; } //}}} //{{{ getBrowserActionSet() method /** * Returns the plugin's action set for the file system browser action * context {@link * VFSBrowser#getActionContext()}. * These actions are loaded from * the <code>browser.actions.xml</code> file; see {@link ActionSet}. *. * @since jEdit 4.2pre1 */ public ActionSet getBrowserActionSet() { return browserActions; } //}}} //{{{ checkDependencies() method /** * Returns true if all dependencies are satisified, false otherwise. * Also if dependencies are not satisfied, the plugin is marked as * "broken". * */ public boolean checkDependencies() { if(plugin == null) return true; boolean ok = true; String name = plugin.getClassName(); PluginDepends[] deps = getPluginDepends(name); for (PluginDepends pluginDepends : deps) { if("jdk".equals(pluginDepends.what)) { if(!pluginDepends.optional && StandardUtilities.compareStrings( System.getProperty("java.version"), pluginDepends.arg,false) < 0) { String[] args = { pluginDepends.arg, System.getProperty("java.version") }; jEdit.pluginError(path,"plugin-error.dep-jdk",args); ok = false; } } else if("jedit".equals(pluginDepends.what)) { if(pluginDepends.arg.length() != 11) { Log.log(Log.ERROR,this,"Invalid jEdit version" + " number: " + pluginDepends.arg); ok = false; } if(!pluginDepends.optional && StandardUtilities.compareStrings( jEdit.getBuild(),pluginDepends.arg,false) < 0) { String needs = MiscUtilities.buildToVersion(pluginDepends.arg); String[] args = { needs, jEdit.getVersion() }; jEdit.pluginError(path, "plugin-error.dep-jedit",args); ok = false; } } else if("plugin".equals(pluginDepends.what)) { int index2 = pluginDepends.arg.indexOf(' '); if(index2 == -1) { Log.log(Log.ERROR,this,name + " has an invalid dependency: " + pluginDepends.dep + " (version is missing)"); ok = false; continue; } String pluginName = pluginDepends.arg.substring(0,index2); String needVersion = pluginDepends.arg.substring(index2 + 1); String currVersion = jEdit.getProperty("plugin." + pluginName + ".version"); EditPlugin editPlugin = jEdit.getPlugin(pluginName, false); if(editPlugin == null) { if(!pluginDepends.optional) { String[] args = { needVersion, pluginName }; jEdit.pluginError(path, "plugin-error.dep-plugin.no-version", args); ok = false; } } else if(StandardUtilities.compareStrings( currVersion,needVersion,false) < 0) { if(!pluginDepends.optional) { String[] args = { needVersion, pluginName, currVersion }; jEdit.pluginError(path, "plugin-error.dep-plugin",args); ok = false; } } else if(editPlugin instanceof EditPlugin.Broken) { if(!pluginDepends.optional) { String[] args = { pluginName }; jEdit.pluginError(path, "plugin-error.dep-plugin.broken",args); ok = false; } } else { PluginJAR jar = editPlugin.getPluginJAR(); if (pluginDepends.optional) { jar.theseUseMe.add(path); weUseThese.add(jar.getPath()); } else { jar.theseRequireMe.add(path); weRequireThese.add(jar.getPath()); } } } else if("class".equals(pluginDepends.what)) { if(!pluginDepends.optional) { try { classLoader.loadClass(pluginDepends.arg,false); } catch(Exception e) { String[] args = { pluginDepends.arg }; jEdit.pluginError(path, "plugin-error.dep-class",args); ok = false; } } } else { Log.log(Log.ERROR,this,name + " has unknown" + " dependency: " + pluginDepends.dep); ok = false; } } // each JAR file listed in the plugin's jars property // needs to know that we need them String jars = jEdit.getProperty("plugin." + plugin.getClassName() + ".jars"); if(jars != null) { String dir = MiscUtilities.getParentOfPath(path); StringTokenizer st = new StringTokenizer(jars); while(st.hasMoreTokens()) { String jarPath = MiscUtilities.constructPath( dir,st.nextToken()); PluginJAR jar = jEdit.getPluginJAR(jarPath); if(jar == null) { String[] args = { jarPath }; jEdit.pluginError(path, "plugin-error.missing-jar",args); ok = false; } else { weRequireThese.add(jarPath); jar.theseRequireMe.add(path); } } } if(!ok) breakPlugin(); return ok; } //}}} //{{{ getRequiredJars() method /** * Returns the required jars of this plugin. * * @return the required jars of this plugin * @since jEdit 4.3pre12 */ public Set<String> getRequiredJars() { return weRequireThese; } //}}} //{{{ getPluginDepends() method private static PluginDepends[] getPluginDepends(String classname) throws IllegalArgumentException { List<PluginDepends> ret = new ArrayList<PluginDepends>(); int i = 0; String dep; while((dep = jEdit.getProperty("plugin." + classname + ".depend." + i++)) != null) { boolean optional; if(dep.startsWith("optional ")) { optional = true; dep = dep.substring("optional ".length()); } else { optional = false; } int index = dep.indexOf(' '); if(index == -1) throw new IllegalArgumentException("wrong dependency"); String what = dep.substring(0,index); String arg = dep.substring(index + 1); PluginDepends depends = new PluginDepends(); depends.what = what; depends.arg = arg; depends.optional = optional; depends.dep = dep; if ("plugin".equals(what)) depends.name = arg.indexOf(' ') > 0 ? arg.substring(0, arg.indexOf(' ')) : arg; ret.add(depends); } return ret.toArray(new PluginDepends[ret.size()]); } //}}} //{{{ getDependencies() method /** * Returns a list of dependencies by searching the plugin properties. * @param classname The classname of a plugin * @return A list of classnames of plugins the plugin depends on. */ public static Set<String> getDependencies(String classname) throws IllegalArgumentException { Set<String> ret = new HashSet<String>(); int i = 0; String dep; while((dep = jEdit.getProperty("plugin." + classname + ".depend." + i++)) != null) { int index = dep.indexOf(' '); String what = dep.substring(0,index); String arg = dep.substring(index + 1); if ("plugin".equals(what)) { ret.add(arg.indexOf(' ') > 0 ? arg.substring(0, arg.indexOf(' ')) : arg); } } return ret; } //}}} //{{{ getOptionalDependencies() method /** * Returns a list of optional dependencies by searching the plugin properties. * @param classname The classname of a plugin * @return A list of classnames of plugins the plugin optionally depends on. */ public static Set<String> getOptionalDependencies(String classname) throws IllegalArgumentException { Set<String> ret = new HashSet<String>(); int i = 0; String dep; while((dep = jEdit.getProperty("plugin." + classname + ".depend." + i++)) != null) { int index = dep.indexOf(' '); String what = dep.substring(0,index); String arg = dep.substring(index + 1); if ("optional".equals(what)) { index = arg.indexOf(' '); what = arg.substring(0, index); arg = arg.substring(index + 1); if ("plugin".equals(what)) { ret.add(arg.indexOf(' ') > 0 ? arg.substring(0, arg.indexOf(' ')) : arg); } } } return ret; } //}}} //{{{ PluginDepends class private static class PluginDepends { String dep; // full string, e.g. plugin errorlist.ErrorList 1.3 String what; // depends type, e.g. jedit, jdk, plugin String arg; // classname + version, e.g errorlist.ErrorList 1.3 String name; // just the class name, e.g. errorlist.ErrorList, only filled in if what is plugin boolean optional; } //}}} //{{{ transitiveClosure() /** * If plugin A is needed by B, and B is needed by C, we want to * tell the user that A is needed by B and C when they try to * unload A. * * @param dependents a set of plugins which we wish to disable * @param listModel a set of plugins which will be affected, and will need * to be disabled also. */ public static void transitiveClosure(String[] dependents, List<String> listModel) { for (String jarPath : dependents) { if (!listModel.contains(jarPath)) { listModel.add(jarPath); PluginJAR jar = jEdit.getPluginJAR(jarPath); if (jar == null) { Log.log(Log.WARNING, PluginJAR.class, "The jar file " + jarPath + " doesn't exist, the plugin may have been partially removed"); } else { transitiveClosure(jar.getDependentPlugins(), listModel); } } } } //}}} //{{{ getDependentPlugins() method /** * @return an array of plugin names that have a hard dependency on this plugin */ public String[] getDependentPlugins() { return theseRequireMe.toArray(new String[theseRequireMe.size()]); } //}}} //{{{ getOptionallyDependentPlugins() method /** * @return an array of plugin names that have an optional dependency on this plugin */ public String[] getOptionallyDependentPlugins() { return theseUseMe.toArray(new String[theseUseMe.size()]); } //}}} //{{{ getAllDependentPlugins() method /** * @return an array of plugin names that have a dependency or an optional dependency on this plugin, * this returns a combination of <code>getDependentPlugins</code> and getOptionallyDependentPlugins</code>. */ public String[] getAllDependentPlugins() { String[] dependents = new String[theseRequireMe.size() + theseUseMe.size()]; System.arraycopy( theseRequireMe.toArray(), 0, dependents, 0, theseRequireMe.size() ); System.arraycopy( theseUseMe.toArray(), 0, dependents, theseRequireMe.size(), theseUseMe.size()); return dependents; } //}}} //{{{ getPlugin() method /** * Returns the plugin core class for this JAR file. Note that if the * plugin has not been activated, this will return an instance of * {@link EditPlugin.Deferred}. If you need the actual plugin core * class instance, call {@link #activatePlugin()} first. * If the plugin is not yet loaded, returns null * * @since jEdit 4.2pre1 */ public EditPlugin getPlugin() { return plugin; } //}}} //{{{ activatePlugin() method /** * Loads the plugin core class. Does nothing if the plugin core class * has already been loaded. This method might be called on startup, * depending on what properties are set. See {@link EditPlugin#start()}. * This method is thread-safe. * * @since jEdit 4.2pre1 */ public void activatePlugin() { synchronized (this) { if (activated) { // recursive call return; } activated = true; } if (!(plugin instanceof EditPlugin.Deferred)) { return; } String className = plugin.getClassName(); try { Class clazz = classLoader.loadClass(className,false); int modifiers = clazz.getModifiers(); if(Modifier.isInterface(modifiers) || Modifier.isAbstract(modifiers) || !EditPlugin.class.isAssignableFrom(clazz)) { Log.log(Log.ERROR,this,"Plugin has properties but does not extend EditPlugin: " + className); breakPlugin(); return; } plugin = (EditPlugin)clazz.newInstance(); plugin.jar = this; } catch (Throwable t) { breakPlugin(); Log.log(Log.ERROR,this,"Error while starting plugin " + className); Log.log(Log.ERROR,this,t); String[] args = { t.toString() }; jEdit.pluginError(path,"plugin-error.start-error",args); return; } if (jEdit.isMainThread() || SwingUtilities.isEventDispatchThread()) { startPlugin(); } else { // for thread safety startPluginLater(); } EditBus.sendAsync(new PluginUpdate(this,PluginUpdate.ACTIVATED,false)); } //}}} //{{{ activateIfNecessary() method /** * Should be called after a new plugin is installed. * @since jEdit 4.2pre2 */ public void activatePluginIfNecessary() { String filename = MiscUtilities.getFileName(getPath()); jEdit.unsetProperty("plugin-blacklist." + filename); if(!(plugin instanceof EditPlugin.Deferred && plugin != null)) { return; } String className = plugin.getClassName(); // default for plugins that don't specify this property (ie, // 4.1-style plugins) is to load them on startup String activate = jEdit.getProperty("plugin." + className + ".activate"); if(activate == null) { // 4.1 plugin if(!jEdit.isMainThread()) { breakPlugin(); jEdit.pluginError(path,"plugin-error.not-42",null); } else { activatePlugin(); } } else { // 4.2 plugin // if at least one property listed here is true, // load the plugin boolean load = false; StringTokenizer st = new StringTokenizer(activate); while(st.hasMoreTokens()) { String prop = st.nextToken(); boolean value = jEdit.getBooleanProperty(prop); if(value) { Log.log(Log.DEBUG,this,"Activating " + className + " because of " + prop); load = true; break; } } if(load) { activatePlugin(); } } } //}}} //{{{ deactivatePlugin() method /** * Unloads the plugin core class. Does nothing if the plugin core class * has not been loaded. * This method can only be called from the AWT event dispatch thread! * @see EditPlugin#stop() * * @since jEdit 4.2pre3 */ public void deactivatePlugin(boolean exit) { if(!activated) return; if(!exit) { // buffers retain a reference to the fold handler in // question... and the easiest way to handle fold // handler unloading is this... Buffer buffer = jEdit.getFirstBuffer(); while(buffer != null) { if(buffer.getFoldHandler().getClass() .getClassLoader() == classLoader) { buffer.setFoldHandler( new DummyFoldHandler()); } buffer = buffer.getNext(); } } if(plugin != null && !(plugin instanceof EditPlugin.Broken)) { if(plugin instanceof EBPlugin || plugin.getClass().getAnnotation(EBHandler.class) != null) { EditBus.removeFromBus(plugin); } try { plugin.stop(); } catch(Throwable t) { Log.log(Log.ERROR,this,"Error while " + "stopping plugin:"); Log.log(Log.ERROR,this,t); } plugin = new EditPlugin.Deferred(this, plugin.getClassName()); EditBus.send(new PluginUpdate(this, PluginUpdate.DEACTIVATED,exit)); if(!exit) { // see if this is a 4.1-style plugin String activate = jEdit.getProperty("plugin." + plugin.getClassName() + ".activate"); if(activate == null) { breakPlugin(); jEdit.pluginError(path,"plugin-error.not-42",null); } } } activated = false; } //}}} //{{{ getDockablesURI() method /** * Returns the location of the plugin's * <code>dockables.xml</code> file. * @since jEdit 4.2pre1 */ public URL getDockablesURI() { return dockablesURI; } //}}} //{{{ getServicesURI() method /** * Returns the location of the plugin's * <code>services.xml</code> file. * @since jEdit 4.2pre1 */ public URL getServicesURI() { return servicesURI; } //}}} //{{{ toString() method @Override public String toString() { if(plugin == null) return path; else return path + ",class=" + plugin.getClassName(); } //}}} //{{{ Package-private members //{{{ Static methods //{{{ getPluginCache() method public static PluginCacheEntry getPluginCache(PluginJAR plugin) { String jarCachePath = plugin.getCachePath(); if(jarCachePath == null) return null; DataInputStream din = null; try { PluginCacheEntry cache = new PluginCacheEntry(); cache.plugin = plugin; cache.modTime = plugin.getFile().lastModified(); din = new DataInputStream( new BufferedInputStream( new FileInputStream(jarCachePath))); if(cache.read(din)) return cache; else { // returns false with outdated cache return null; } } catch(FileNotFoundException fnf) { return null; } catch(IOException io) { Log.log(Log.ERROR,PluginJAR.class,io); return null; } finally { IOUtilities.closeQuietly((Closeable)din); } } //}}} //{{{ setPluginCache() method static void setPluginCache(PluginJAR plugin, PluginCacheEntry cache) { String jarCachePath = plugin.getCachePath(); if(jarCachePath == null) return; Log.log(Log.DEBUG,PluginJAR.class,"Writing " + jarCachePath); DataOutputStream dout = null; try { dout = new DataOutputStream( new BufferedOutputStream( new FileOutputStream(jarCachePath))); cache.write(dout); dout.close(); } catch(IOException io) { Log.log(Log.ERROR,PluginJAR.class,io); IOUtilities.closeQuietly((Closeable)dout); new File(jarCachePath).delete(); } } //}}} //}}} //{{{ PluginJAR constructor /** * Creates a PluginJAR object which is not necessarily loaded, but can be later. * @see #load(String, boolean) */ public PluginJAR(File file) { path = file.getPath(); String jarCacheDir = jEdit.getJARCacheDirectory(); if(jarCacheDir != null) { cachePath = MiscUtilities.constructPath( jarCacheDir,file.getName() + ".summary"); } this.file = file; classLoader = new JARClassLoader(this); actions = new ActionSet(); } //}}} //{{{ init() method public boolean init() { PluginCacheEntry cache = getPluginCache(this); if(cache != null) { if (!loadCache(cache)) return false; classLoader.activate(); } else { try { cache = generateCache(); if(cache != null) { setPluginCache(this,cache); classLoader.activate(); } else { return false; } } catch(IOException io) { Log.log(Log.ERROR,this,"Cannot load" + " plugin " + path); Log.log(Log.ERROR,this,io); String[] args = { io.toString() }; jEdit.pluginError(path,"plugin-error.load-error",args); uninit(false); } } return true; } //}}} //{{{ uninit() method public void uninit(boolean exit) { deactivatePlugin(exit); if(!exit) { for (String path : weRequireThese) { PluginJAR jar = jEdit.getPluginJAR(path); if(jar != null) jar.theseRequireMe.remove(this.path); } for (String path : weUseThese) { PluginJAR jar = jEdit.getPluginJAR(path); if(jar != null) jar.theseUseMe.remove(this.path); } classLoader.deactivate(); BeanShell.resetClassManager(); if(actions != null) jEdit.removeActionSet(actions); if(browserActions != null) VFSBrowser.getActionContext().removeActionSet(browserActions); DockableWindowFactory.getInstance() .unloadDockableWindows(this); ServiceManager.unloadServices(this); jEdit.removePluginProps(properties); if (localizationProperties != null) { Collection<Properties> values = localizationProperties.values(); for (Properties value : values) { jEdit.removePluginLocalizationProps(value); } } try { if(zipFile != null) { zipFile.close(); zipFile = null; } } catch(IOException io) { Log.log(Log.ERROR,this,io); } removePluginCache(); } } //}}} //{{{ getClasses() method String[] getClasses() { return classes; } //}}} //{{{ getResources() method public String[] getResources() { return resources; } //}}} //}}} //{{{ Private members //{{{ actionsPresentButNotCoreClass() method private void actionsPresentButNotCoreClass() { Log.log(Log.WARNING,this,getPath() + " has an actions.xml but no plugin core class"); actions.setLabel("MISSING PLUGIN CORE CLASS"); } //}}} //{{{ loadCache() method private boolean loadCache(PluginCacheEntry cache) { // Check if a plugin with the same name // is already loaded if(cache.pluginClass != null) { // Check if a plugin with the same name // is already loaded if (!continueLoading(cache.pluginClass, cache.cachedProperties)) { return false; } else { EditPlugin otherPlugin = jEdit.getPlugin(cache.pluginClass); if (otherPlugin != null) jEdit.removePluginJAR(otherPlugin.getPluginJAR(), false); } } classes = cache.classes; resources = cache.resources; // this must be done before loading cachedProperties if (cache.localizationProperties != null) { localizationProperties = cache.localizationProperties; String currentLanguage = jEdit.getCurrentLanguage(); Properties langProperties = localizationProperties.get(currentLanguage); if (langProperties != null) { jEdit.addPluginLocalizationProps(langProperties); } } /* this should be before dockables are initialized */ if(cache.cachedProperties != null) { properties = cache.cachedProperties; jEdit.addPluginProps(cache.cachedProperties); } if(cache.actionsURI != null && cache.cachedActionNames != null) { actions = new ActionSet(this, cache.cachedActionNames, cache.cachedActionToggleFlags, cache.actionsURI); } if(cache.browserActionsURI != null && cache.cachedBrowserActionNames != null) { browserActions = new ActionSet(this, cache.cachedBrowserActionNames, cache.cachedBrowserActionToggleFlags, cache.browserActionsURI); String label = jEdit.getProperty( "plugin." + cache.pluginClass + ".name"); browserActions.setLabel(jEdit.getProperty( "action-set.plugin", new String[] { label })); VFSBrowser.getActionContext().addActionSet(browserActions); } if(cache.dockablesURI != null && cache.cachedDockableNames != null && cache.cachedDockableActionFlags != null && cache.cachedDockableMovableFlags != null) { dockablesURI = cache.dockablesURI; DockableWindowFactory.getInstance() .cacheDockableWindows(this, cache.cachedDockableNames, cache.cachedDockableActionFlags, cache.cachedDockableMovableFlags); } if(actions.size() != 0) jEdit.addActionSet(actions); if(cache.servicesURI != null && cache.cachedServices != null) { servicesURI = cache.servicesURI; for(int i = 0; i < cache.cachedServices.length; i++) { ServiceManager.Descriptor d = cache.cachedServices[i]; ServiceManager.registerService(d); } } if(cache.pluginClass != null) { String label = jEdit.getProperty( "plugin." + cache.pluginClass + ".name"); actions.setLabel(jEdit.getProperty( "action-set.plugin", new String[] { label })); plugin = new EditPlugin.Deferred(this, cache.pluginClass); } else { if(actions.size() != 0) actionsPresentButNotCoreClass(); } return true; } //}}} //{{{ generateCache() method public PluginCacheEntry generateCache() throws IOException { properties = new Properties(); localizationProperties = new HashMap<String, Properties>(); List<String> classes = new LinkedList<String>(); List<String> resources = new LinkedList<String>(); ZipFile zipFile = getZipFile(); List<String> plugins = new LinkedList<String>(); PluginCacheEntry cache = new PluginCacheEntry(); cache.modTime = file.lastModified(); Enumeration<? extends ZipEntry> entries = zipFile.entries(); Pattern languageFilePattern = Pattern.compile("lang_(\\w+).properties"); while(entries.hasMoreElements()) { ZipEntry entry = entries.nextElement(); String name = entry.getName(); String lname = name.toLowerCase(); if("actions.xml".equals(lname)) { cache.actionsURI = classLoader.getResource(name); } else if("browser.actions.xml".equals(lname)) { cache.browserActionsURI = classLoader.getResource(name); } else if("dockables.xml".equals(lname)) { dockablesURI = classLoader.getResource(name); cache.dockablesURI = dockablesURI; } else if("services.xml".equals(lname)) { servicesURI = classLoader.getResource(name); cache.servicesURI = servicesURI; } else if(lname.endsWith(".props")) { InputStream in = null; try { in = classLoader.getResourceAsStream(name); properties.load(in); } finally { IOUtilities.closeQuietly((Closeable)in); } } else if(name.endsWith(".class")) { String className = MiscUtilities .fileToClass(name); if(className.endsWith("Plugin")) { plugins.add(className); } classes.add(className); } else { Matcher matcher = languageFilePattern.matcher(lname); if (matcher.matches()) { String languageName = matcher.group(1); Properties props = new Properties(); InputStream in = null; try { in = classLoader.getResourceAsStream(name); CharsetEncoding utf8 = new CharsetEncoding("UTF-8"); Reader utf8in = utf8.getTextReader(in); props.load(utf8in); localizationProperties.put(languageName, props); } finally { IOUtilities.closeQuietly((Closeable)in); } } else resources.add(name); } } cache.cachedProperties = properties; cache.localizationProperties = localizationProperties; // this must be done before loading cachedProperties if (cache.localizationProperties != null) { localizationProperties = cache.localizationProperties; String currentLanguage = jEdit.getCurrentLanguage(); Properties langProperties = localizationProperties.get(currentLanguage); if (langProperties != null) { jEdit.addPluginLocalizationProps(langProperties); } } jEdit.addPluginProps(properties); this.classes = cache.classes = classes.toArray( new String[classes.size()]); this.resources = cache.resources = resources.toArray( new String[resources.size()]); String label = null; for (String className : plugins) { String _label = jEdit.getProperty("plugin." + className + ".name"); String version = jEdit.getProperty("plugin." + className + ".version"); if(_label == null || version == null) { Log.log(Log.WARNING,this,"Ignoring: " + className); } else { cache.pluginClass = className; // Check if a plugin with the same name // is already loaded if (!continueLoading(className, cache.cachedProperties)) { return null; } else { EditPlugin otherPlugin = jEdit.getPlugin(className); if (otherPlugin != null) { jEdit.removePluginJAR(otherPlugin.getPluginJAR(), false); // otherPlugin.getPluginJAR().uninit(false); } } plugin = new EditPlugin.Deferred(this, className); label = _label; break; } } boolean isBeingLoaded = jEdit.getPluginJAR(getPath()) != null; if(!isBeingLoaded) { Log.log(Log.DEBUG, PluginJAR.class, "not loading actions, dockables, services " +"because the plugin is not really being loaded"); return cache; } if(cache.actionsURI != null) { actions = new ActionSet(this,null,null, cache.actionsURI); actions.load(); cache.cachedActionNames = actions.getCacheableActionNames(); cache.cachedActionToggleFlags = new boolean[cache.cachedActionNames.length]; for(int i = 0; i < cache.cachedActionNames.length; i++) { cache.cachedActionToggleFlags[i] = jEdit.getBooleanProperty( cache.cachedActionNames[i] + ".toggle"); } } if(cache.browserActionsURI != null) { browserActions = new ActionSet(this,null,null, cache.browserActionsURI); browserActions.load(); VFSBrowser.getActionContext().addActionSet(browserActions); cache.cachedBrowserActionNames = browserActions.getCacheableActionNames(); cache.cachedBrowserActionToggleFlags = new boolean[ cache.cachedBrowserActionNames.length]; for(int i = 0; i < cache.cachedBrowserActionNames.length; i++) { cache.cachedBrowserActionToggleFlags[i] = jEdit.getBooleanProperty( cache.cachedBrowserActionNames[i] + ".toggle"); } } if(dockablesURI != null) { DockableWindowFactory.getInstance() .loadDockableWindows(this, dockablesURI,cache); } if(actions.size() != 0) { if(label != null) { actions.setLabel(jEdit.getProperty( "action-set.plugin", new String[] { label })); } else actionsPresentButNotCoreClass(); jEdit.addActionSet(actions); } if(servicesURI != null) { ServiceManager.loadServices(this,servicesURI,cache); } return cache; } //}}} private static boolean continueLoading(String clazz, Properties cachedProperties) { if(jEdit.getPlugin(clazz) != null) { String otherVersion = jEdit.getProperty("plugin."+clazz+".version"); String thisVersion = cachedProperties.getProperty("plugin."+clazz+".version"); if (otherVersion.compareTo(thisVersion) > 0) return false; } return true; } //{{{ startPlugin() method private void startPlugin() { try { plugin.start(); } catch(Throwable t) { breakPlugin(); Log.log(Log.ERROR,PluginJAR.this, "Error while starting plugin " + plugin.getClassName()); Log.log(Log.ERROR,PluginJAR.this,t); String[] args = { t.toString() }; jEdit.pluginError(path, "plugin-error.start-error",args); } if(plugin instanceof EBPlugin || plugin.getClass().getAnnotation(EBHandler.class) != null) { if(jEdit.getProperty("plugin." + plugin.getClassName() + ".activate") == null) { // old plugins expected jEdit 4.1-style // behavior, where a PropertiesChanged // was sent after plugins were started ((EBComponent)plugin).handleMessage( new PropertiesChanged(null)); } EditBus.addToBus(plugin); } // buffers retain a reference to the fold handler in // question... and the easiest way to handle fold // handler loading is this... Buffer buffer = jEdit.getFirstBuffer(); while(buffer != null) { FoldHandler handler = FoldHandler.getFoldHandler( buffer.getStringProperty("folding")); // == null before loaded if(handler != null && handler != buffer.getFoldHandler()) { buffer.setFoldHandler(handler); } buffer = buffer.getNext(); } } //}}} //{{{ startPluginLater() method private void startPluginLater() { EventQueue.invokeLater(new Runnable() { @Override public void run() { if (!activated) return; startPlugin(); } }); } //}}} //{{{ breakPlugin() method private void breakPlugin() { plugin = new EditPlugin.Broken(this,plugin.getClassName()); // remove action sets, dockables, etc so that user doesn't // see the broken plugin uninit(false); // but we want properties to hang around jEdit.addPluginProps(properties); } //}}} //{{{ removePluginCache() method private void removePluginCache() { if(cachePath != null) new File(cachePath).delete(); } //}}} //}}} //{{{ PluginCacheEntry class /** * Used by the <code>DockableWindowManager</code> and * <code>ServiceManager</code> to handle caching. * @since jEdit 4.2pre1 */ public static class PluginCacheEntry { public static final int MAGIC = 0xB7A2E424; //{{{ Instance variables public PluginJAR plugin; public long modTime; public String[] classes; public String[] resources; public URL actionsURI; public String[] cachedActionNames; public boolean[] cachedActionToggleFlags; public URL browserActionsURI; public String[] cachedBrowserActionNames; public boolean[] cachedBrowserActionToggleFlags; public URL dockablesURI; public String[] cachedDockableNames; public boolean[] cachedDockableActionFlags; public boolean[] cachedDockableMovableFlags; public URL servicesURI; ServiceManager.Descriptor[] cachedServices; public Properties cachedProperties; public Map<String, Properties> localizationProperties; public String pluginClass; //}}} /* read() and write() must be kept perfectly in sync... * its a very simple file format. doing it this way is * faster than serializing since serialization calls * reflection, etc. */ //{{{ read() method public boolean read(DataInputStream din) throws IOException { int cacheMagic = din.readInt(); if(cacheMagic != MAGIC) return false; String cacheBuild = readString(din); if(!cacheBuild.equals(jEdit.getBuild())) return false; long cacheModTime = din.readLong(); if(cacheModTime != modTime) return false; actionsURI = readURI(din); cachedActionNames = readStringArray(din); cachedActionToggleFlags = readBooleanArray(din); browserActionsURI = readURI(din); cachedBrowserActionNames = readStringArray(din); cachedBrowserActionToggleFlags = readBooleanArray(din); dockablesURI = readURI(din); cachedDockableNames = readStringArray(din); cachedDockableActionFlags = readBooleanArray(din); cachedDockableMovableFlags = readBooleanArray(din); servicesURI = readURI(din); int len = din.readInt(); if(len == 0) cachedServices = null; else { cachedServices = new ServiceManager.Descriptor[len]; for(int i = 0; i < len; i++) { ServiceManager.Descriptor d = new ServiceManager.Descriptor( readString(din), readString(din), null, plugin); cachedServices[i] = d; } } classes = readStringArray(din); resources = readStringArray(din); cachedProperties = readMap(din); localizationProperties = readLanguagesMap(din); pluginClass = readString(din); return true; } //}}} //{{{ write() method public void write(DataOutputStream dout) throws IOException { dout.writeInt(MAGIC); writeString(dout,jEdit.getBuild()); dout.writeLong(modTime); writeString(dout,actionsURI); writeStringArray(dout,cachedActionNames); writeBooleanArray(dout,cachedActionToggleFlags); writeString(dout,browserActionsURI); writeStringArray(dout,cachedBrowserActionNames); writeBooleanArray(dout,cachedBrowserActionToggleFlags); writeString(dout,dockablesURI); writeStringArray(dout,cachedDockableNames); writeBooleanArray(dout,cachedDockableActionFlags); writeBooleanArray(dout,cachedDockableMovableFlags); writeString(dout,servicesURI); if(cachedServices == null) dout.writeInt(0); else { dout.writeInt(cachedServices.length); for (ServiceManager.Descriptor cachedService : cachedServices) { writeString(dout, cachedService.clazz); writeString(dout, cachedService.name); } } writeStringArray(dout,classes); writeStringArray(dout,resources); writeMap(dout,cachedProperties); writeLanguages(dout, localizationProperties); writeString(dout,pluginClass); } //}}} //{{{ Private members //{{{ readString() method private static String readString(DataInputStream din) throws IOException { int len = din.readInt(); if(len == 0) return null; char[] str = new char[len]; for(int i = 0; i < len; i++) str[i] = din.readChar(); return new String(str); } //}}} //{{{ readURI() method private static URL readURI(DataInputStream din) throws IOException { String str = readString(din); if(str == null) return null; else return new URL(str); } //}}} //{{{ readStringArray() method private static String[] readStringArray(DataInputStream din) throws IOException { int len = din.readInt(); if(len == 0) return null; String[] str = new String[len]; for(int i = 0; i < len; i++) { str[i] = readString(din); } return str; } //}}} //{{{ readBooleanArray() method private static boolean[] readBooleanArray(DataInputStream din) throws IOException { int len = din.readInt(); if(len == 0) return null; boolean[] bools = new boolean[len]; for(int i = 0; i < len; i++) { bools[i] = din.readBoolean(); } return bools; } //}}} //{{{ readMap() method private static Properties readMap(DataInputStream din) throws IOException { Properties returnValue = new Properties(); int count = din.readInt(); for(int i = 0; i < count; i++) { String key = readString(din); String value = readString(din); if(value == null) value = ""; returnValue.setProperty(key, value); } return returnValue; } //}}} //{{{ readLanguagesMap() method private static Map<String, Properties> readLanguagesMap(DataInputStream din) throws IOException { int languagesCount = din.readInt(); if (languagesCount == 0) return Collections.emptyMap(); Map<String, Properties> languages = new HashMap<String, Properties>(languagesCount); for (int i = 0;i<languagesCount;i++) { String lang = readString(din); Properties props = readMap(din); languages.put(lang, props); } return languages; } //}}} //{{{ writeString() method private static void writeString(DataOutputStream dout, Object obj) throws IOException { if(obj == null) { dout.writeInt(0); } else { String str = obj.toString(); dout.writeInt(str.length()); dout.writeChars(str); } } //}}} //{{{ writeStringArray() method private static void writeStringArray(DataOutputStream dout, String[] str) throws IOException { if(str == null) { dout.writeInt(0); } else { dout.writeInt(str.length); for (String s : str) writeString(dout, s); } } //}}} //{{{ writeBooleanArray() method private static void writeBooleanArray(DataOutputStream dout, boolean[] bools) throws IOException { if(bools == null) { dout.writeInt(0); } else { dout.writeInt(bools.length); for (boolean bool : bools) dout.writeBoolean(bool); } } //}}} //{{{ writeMap() method private static void writeMap(DataOutputStream dout, Properties properties) throws IOException { dout.writeInt(properties.size()); Set<Map.Entry<Object, Object>> set = properties.entrySet(); for (Map.Entry<Object, Object> entry : set) { writeString(dout,entry.getKey()); writeString(dout,entry.getValue()); } } //}}} //{{{ writeLanguages() method private static void writeLanguages(DataOutputStream dout, Map<String, Properties> languages) throws IOException { dout.writeInt(languages.size()); for (Map.Entry<String, Properties> entry : languages.entrySet()) { writeString(dout, entry.getKey()); writeMap(dout, entry.getValue()); } } //}}} //}}} } //}}} }