/*
 * 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.nio.charset.StandardCharsets;
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.annotation.Nonnull;
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.manager.BufferManager;
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 25698 2023-11-17 01:31:50Z vampire0 $
 * @since jEdit 4.2pre1
 */
public class PluginJAR
{
	public static final PluginDepends[] EMPTY_PLUGIN_DEPENDS_ARRAY = new PluginDepends[0];
	//{{{ 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<>();
	
	/** The plugins that uses me as optional dependency. */
	private final Set<String> theseUseMe = new LinkedHashSet<>();
	
	/** This plugin requires these plugins. */
	private final Set<String> weRequireThese = new LinkedHashSet<>();
	
	/** 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<>();
	//}}}

	//{{{ 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
		Collection<String> jarsPaths = jar.getJars();
		for(String _jarPath: jarsPaths)
		{
			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;
	} // }}}
	
	//{{{ parseJarsFilesString(String path, String jarsString) method
	/**
	 * parse the files listed in plugin.CLASSNAME.jars or plugin.CLASSNAME.files
	 * and return full paths to each file of the list.
	 * @since jEdit 5.3pre1
	 */
	public static Collection<String> parseJarsFilesString(String path, String jarsString)
	{
		String dir = MiscUtilities.getParentOfPath(path);
		StringTokenizer st = new StringTokenizer(jarsString);
		Collection<String> jarPaths = new LinkedList<>();
		while(st.hasMoreTokens())
		{
			String _jarPath = MiscUtilities.constructPath(dir,st.nextToken());
			jarPaths.add(_jarPath);
		}
		return jarPaths;
	}
	// }}}
	//{{{ parseJarsFilesStringNames(String path, String jarsString) method
	/**
	 * parse the files listed in plugin.CLASSNAME.jars or plugin.CLASSNAME.files
	 * and return them as a collection
	 * @since jEdit 5.3pre1
	 */
	public static Collection<String> parseJarsFilesStringNames(String jarsString)
	{
		StringTokenizer st = new StringTokenizer(jarsString);
		Collection<String> jarPaths = new LinkedList<>();
		while(st.hasMoreTokens())
		{
			jarPaths.add(st.nextToken());
		}
		return jarPaths;
	}
	// }}}

	//{{{ getJars() method
	/**
	 * Get the jars listed in this plugin and return full paths to them
	 *
	 * @return jars full paths or empty collection if plugin is null
	 * @since jEdit 5.3pre1
	 */
	public Collection<String> getJars()
	{
		if(plugin != null)
		{
			String jars = jEdit.getProperty("plugin." + plugin.getClassName() + ".jars");
			if(jars != null)
			{
				return parseJarsFilesString(path, jars);
			}
		}
		return Collections.emptyList();
	}
	// }}}

	// {{{ getFiles() method
	/**
	 * Get the files listed in this plugin and return full paths to them
	 *
	 * @return files full paths or empty collection if plugin is null
	 * @since jEdit 5.3pre1
	 */
	public Collection<String> getFiles()
	{
		if(plugin != null)
		{
			String files = jEdit.getProperty("plugin." + plugin.getClassName() + ".files");
			if(files != null)
			{
				return parseJarsFilesString(path, files);
			}
		}
		return Collections.emptyList();
	}
	// }}}

	//{{{ 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<>();
		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
		Collection<String> jarsPaths = getJars();

		for(String jarPath: jarsPaths)
		{
			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
	{
		var 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(EMPTY_PLUGIN_DEPENDS_ARRAY);
	} //}}}

	//{{{ 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.
	 */
	@Nonnull
	public static Set<String> getDependencies(String classname) throws IllegalArgumentException
	{
		var 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
	{
		var 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(StandardUtilities.EMPTY_STRING_ARRAY);
	} //}}}

	//{{{ getOptionallyDependentPlugins() method
	/**
	* @return an array of plugin names that have an optional dependency on this plugin
	*/
	public String[] getOptionallyDependentPlugins()
	{
		return theseUseMe.toArray(StandardUtilities.EMPTY_STRING_ARRAY);
	} //}}}
	
	//{{{ 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 <code>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
	 */
	@SuppressWarnings("unchecked")
	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.getDeclaredConstructor().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))
		{
			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...
			BufferManager bufferManager = jEdit.getBufferManager();
			bufferManager
				.getBuffers()
				.stream()
				.filter(buffer -> buffer.getFoldHandler().getClass().getClassLoader() == classLoader)
				.forEach(buffer -> buffer.setFoldHandler(new DummyFoldHandler()));
		}

		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();
		}
	} //}}}

	//{{{ getPluginCacheEntry() method
	/**
	 * Returns the cache entry for an installed but not loaded plugin.
	 * There is no need to use this method if the plugin is loaded.
	 *
	 * @param path path to the the plugin jar
	 * @return cache entry or null
	 * @throws IOException if jEdit cannot generate cache
	 * @since jEdit 5.3pre1
	 */
	public static PluginJAR.PluginCacheEntry getPluginCacheEntry(String path) throws IOException
	{
		PluginJAR pluginJAR = new PluginJAR(new File(path));
		PluginJAR.PluginCacheEntry pluginCacheEntry = PluginJAR.getPluginCache(pluginJAR);
		if (pluginCacheEntry == null)
		{
			try
			{
				pluginCacheEntry = pluginJAR.generateCache();
			}
			finally
			{
				IOUtilities.closeQuietly(pluginJAR.getZipFile());
			}
		}
		if(pluginCacheEntry == null)
		{
			// this happens when, for some reason, two versions
			// of a plugin are installed, e.g when XSLT.jar and
			// xslt.jar are both in $JEDIT_HOME/jars on Linux.
			Log.log(Log.WARNING, PluginJAR.class,
					"couldn't load plugin "+pluginJAR.getPath()
					+" (most likely other version exists)");
		}
		return pluginCacheEntry;
	}//}}}

	//}}}

	//{{{ 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<>();

		var classes = new LinkedList<String>();
		var resources = new LinkedList<String>();

		ZipFile zipFile = getZipFile();

		var 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(StandardCharsets.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(StandardUtilities.EMPTY_STRING_ARRAY);
		this.resources = cache.resources = resources.toArray(StandardUtilities.EMPTY_STRING_ARRAY);

		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,this, "Error while starting plugin " + plugin.getClassName());
			Log.log(Log.ERROR,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...
		BufferManager bufferManager = jEdit.getBufferManager();
		bufferManager.getBuffers()
			.forEach(buffer ->
			{
				FoldHandler handler = FoldHandler.getFoldHandler(buffer.getStringProperty("folding"));
				// == null before loaded
				if(handler != null && handler != buffer.getFoldHandler())
					buffer.setFoldHandler(handler);
			});
	} //}}}

	//{{{ startPluginLater() method
	private void startPluginLater()
	{
		EventQueue.invokeLater(() ->
		{
			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(!jEdit.getBuild().equals(cacheBuild))
				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<>(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());
			}
		} //}}}

		//}}}
	} //}}}
}
