PageRenderTime 100ms CodeModel.GetById 25ms app.highlight 54ms RepoModel.GetById 1ms app.codeStats 0ms

/jEdit/tags/jedit-4-3-pre5/org/gjt/sp/jedit/PluginJAR.java

#
Java | 1483 lines | 1056 code | 160 blank | 267 comment | 198 complexity | 1ad9a147d914449056237441fdc3bd35 MD5 | raw file
Possible License(s): BSD-3-Clause, AGPL-1.0, Apache-2.0, LGPL-2.0, LGPL-3.0, GPL-2.0, CC-BY-SA-3.0, LGPL-2.1, GPL-3.0, MPL-2.0-no-copyleft-exception, IPL-1.0
   1/*
   2 * PluginJAR.java - Controls JAR loading and unloading
   3 * :tabSize=8:indentSize=8:noTabs=false:
   4 * :folding=explicit:collapseFolds=1:
   5 *
   6 * Copyright (C) 1999, 2004 Slava Pestov
   7 *
   8 * This program is free software; you can redistribute it and/or
   9 * modify it under the terms of the GNU General Public License
  10 * as published by the Free Software Foundation; either version 2
  11 * of the License, or any later version.
  12 *
  13 * This program is distributed in the hope that it will be useful,
  14 * but WITHOUT ANY WARRANTY; without even the implied warranty of
  15 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
  16 * GNU General Public License for more details.
  17 *
  18 * You should have received a copy of the GNU General Public License
  19 * along with this program; if not, write to the Free Software
  20 * Foundation, Inc., 59 Temple Place - Suite 330, Boston, MA  02111-1307, USA.
  21 */
  22
  23package org.gjt.sp.jedit;
  24
  25//{{{ Imports
  26import javax.swing.SwingUtilities;
  27import java.io.*;
  28import java.lang.reflect.Modifier;
  29import java.net.URL;
  30import java.util.*;
  31import java.util.zip.*;
  32import org.gjt.sp.jedit.browser.VFSBrowser;
  33import org.gjt.sp.jedit.buffer.*;
  34import org.gjt.sp.jedit.gui.DockableWindowFactory;
  35import org.gjt.sp.jedit.msg.*;
  36import org.gjt.sp.util.Log;
  37//}}}
  38
  39/**
  40 * Loads and unloads plugins.<p>
  41 *
  42 * <h3>JAR file contents</h3>
  43 *
  44 * When loading a plugin, jEdit looks for the following resources:
  45 *
  46 * <ul>
  47 * <li>A file named <code>actions.xml</code> defining plugin actions.
  48 * Only one such file per plugin is allowed. See {@link ActionSet} for
  49 * syntax.</li>
  50 * <li>A file named <code>browser.actions.xml</code> defining file system
  51 * browser actions.
  52 * Only one such file per plugin is allowed. See {@link ActionSet} for
  53 * syntax.</li>
  54 * <li>A file named <code>dockables.xml</code> defining dockable windows.
  55 * Only one such file per plugin is allowed. See {@link
  56 * org.gjt.sp.jedit.gui.DockableWindowManager} for
  57 * syntax.</li>
  58 * <li>A file named <code>services.xml</code> defining additional services
  59 * offered by the plugin, such as virtual file systems.
  60 * Only one such file per plugin is allowed. See {@link
  61 * org.gjt.sp.jedit.ServiceManager} for
  62 * syntax.</li>
  63 * <li>File with extension <code>.props</code> containing name/value pairs
  64 * separated by an equals sign.
  65 * A plugin can supply any number of property files. Property files are used
  66 * to define plugin men items, plugin option panes, as well as arbitriary
  67 * settings and strings used by the plugin. See {@link EditPlugin} for
  68 * information about properties used by jEdit. See
  69 * <code>java.util.Properties</code> for property file syntax.</li>
  70 * </ul>
  71 *
  72 * For a plugin to actually do something once it is resident in memory,
  73 * it must contain a class whose name ends with <code>Plugin</code>.
  74 * This class, known as the <i>plugin core class</i> must extend
  75 * {@link EditPlugin} and define a few required properties, otherwise it is
  76 * ignored.
  77 *
  78 * <h3>Dynamic and deferred loading</h3>
  79 *
  80 * Unlike in prior jEdit versions, jEdit 4.2 and later allow
  81 * plugins to be added and removed to the resident set at any time using
  82 * the {@link jEdit#addPluginJAR(String)} and
  83 * {@link jEdit#removePluginJAR(PluginJAR,boolean)} methods. Furthermore, the
  84 *  plugin core class might not be loaded until the plugin is first used. See
  85 * {@link EditPlugin#start()} for a full description.
  86 *
  87 * @see org.gjt.sp.jedit.jEdit#getProperty(String)
  88 * @see org.gjt.sp.jedit.jEdit#getPlugin(String)
  89 * @see org.gjt.sp.jedit.jEdit#getPlugins()
  90 * @see org.gjt.sp.jedit.jEdit#getPluginJAR(String)
  91 * @see org.gjt.sp.jedit.jEdit#getPluginJARs()
  92 * @see org.gjt.sp.jedit.jEdit#addPluginJAR(String)
  93 * @see org.gjt.sp.jedit.jEdit#removePluginJAR(PluginJAR,boolean)
  94 * @see org.gjt.sp.jedit.ActionSet
  95 * @see org.gjt.sp.jedit.gui.DockableWindowManager
  96 * @see org.gjt.sp.jedit.OptionPane
  97 * @see org.gjt.sp.jedit.PluginJAR
  98 * @see org.gjt.sp.jedit.ServiceManager
  99 *
 100 * @author Slava Pestov
 101 * @version $Id: PluginJAR.java 5295 2005-11-05 06:42:54Z ezust $
 102 * @since jEdit 4.2pre1
 103 */
 104public class PluginJAR
 105{
 106	//{{{ getPath() method
 107	/**
 108	 * Returns the full path name of this plugin's JAR file.
 109	 */
 110	public String getPath()
 111	{
 112		return path;
 113	} //}}}
 114
 115	//{{{ getCachePath() method
 116	/**
 117	 * Returns the full path name of this plugin's summary file.
 118	 * The summary file is used to store certain information which allows
 119	 * loading of the plugin's resources and core class to be deferred
 120	 * until the plugin is first used. As long as a plugin is using the
 121	 * jEdit 4.2 plugin API, no extra effort is required to take advantage
 122	 * of the summary cache.
 123	 */
 124	public String getCachePath()
 125	{
 126		return cachePath;
 127	} //}}}
 128
 129	//{{{ getFile() method
 130	/**
 131	 * Returns a file pointing to the plugin JAR.
 132	 */
 133	public File getFile()
 134	{
 135		return file;
 136	} //}}}
 137
 138	//{{{ getClassLoader() method
 139	/**
 140	 * Returns the plugin's class loader.
 141	 */
 142	public JARClassLoader getClassLoader()
 143	{
 144		return classLoader;
 145	} //}}}
 146
 147	//{{{ getZipFile() method
 148	/**
 149	 * Returns the plugin's JAR file, opening it if necessary.
 150	 * @since jEdit 4.2pre1
 151	 */
 152	public synchronized ZipFile getZipFile() throws IOException
 153	{
 154		if(zipFile == null)
 155		{
 156			Log.log(Log.DEBUG,this,"Opening " + path);
 157			zipFile = new ZipFile(path);
 158		}
 159		return zipFile;
 160	} //}}}
 161
 162	//{{{ getActions() method
 163	/**
 164	 * @deprecated Call getActionSet() instead
 165	 */
 166	public ActionSet getActions()
 167	{
 168		return getActionSet();
 169	} //}}}
 170
 171	//{{{ getActionSet() method
 172	/**
 173	 * Returns the plugin's action set for the jEdit action context
 174	 * {@link jEdit#getActionContext()}. These actions are loaded from
 175	 * the <code>actions.xml</code> file; see {@link ActionSet}.
 176	 *.
 177	 * @since jEdit 4.2pre1
 178	 */
 179	public ActionSet getActionSet()
 180	{
 181		return actions;
 182	} //}}}
 183
 184	//{{{ getBrowserActionSet() method
 185	/**
 186	 * Returns the plugin's action set for the file system browser action
 187	 * context {@link
 188	 * org.gjt.sp.jedit.browser.VFSBrowser#getActionContext()}.
 189	 * These actions are loaded from
 190	 * the <code>browser.actions.xml</code> file; see {@link ActionSet}.
 191	 *.
 192	 * @since jEdit 4.2pre1
 193	 */
 194	public ActionSet getBrowserActionSet()
 195	{
 196		return browserActions;
 197	} //}}}
 198
 199	//{{{ checkDependencies() method
 200	/**
 201	 * Returns true if all dependencies are satisified, false otherwise.
 202	 * Also if dependencies are not satisfied, the plugin is marked as
 203	 * "broken".
 204	 */
 205	public boolean checkDependencies()
 206	{
 207		if(plugin == null)
 208			return true;
 209
 210		int i = 0;
 211
 212		boolean ok = true;
 213		boolean optional = false;
 214
 215		String name = plugin.getClassName();
 216
 217		String dep;
 218		while((dep = jEdit.getProperty("plugin." + name + ".depend." + i++)) != null)
 219		{
 220			if(dep.startsWith("optional "))
 221			{
 222				optional = true;
 223				dep = dep.substring("optional ".length());
 224			}
 225
 226			int index = dep.indexOf(' ');
 227			if(index == -1)
 228			{
 229				Log.log(Log.ERROR,this,name + " has an invalid"
 230					+ " dependency: " + dep);
 231				ok = false;
 232				continue;
 233			}
 234
 235			String what = dep.substring(0,index);
 236			String arg = dep.substring(index + 1);
 237
 238			if(what.equals("jdk"))
 239			{
 240				if(!optional && MiscUtilities.compareStrings(
 241					System.getProperty("java.version"),
 242					arg,false) < 0)
 243				{
 244					String[] args = { arg,
 245						System.getProperty("java.version") };
 246					jEdit.pluginError(path,"plugin-error.dep-jdk",args);
 247					ok = false;
 248				}
 249			}
 250			else if(what.equals("jedit"))
 251			{
 252				if(arg.length() != 11)
 253				{
 254					Log.log(Log.ERROR,this,"Invalid jEdit version"
 255						+ " number: " + arg);
 256					ok = false;
 257				}
 258
 259				if(!optional && MiscUtilities.compareStrings(
 260					jEdit.getBuild(),arg,false) < 0)
 261				{
 262					String needs = MiscUtilities.buildToVersion(arg);
 263					String[] args = { needs,
 264						jEdit.getVersion() };
 265					jEdit.pluginError(path,
 266						"plugin-error.dep-jedit",args);
 267					ok = false;
 268				}
 269			}
 270			else if(what.equals("plugin"))
 271			{
 272				int index2 = arg.indexOf(' ');
 273				if(index2 == -1)
 274				{
 275					Log.log(Log.ERROR,this,name 
 276						+ " has an invalid dependency: "
 277						+ dep + " (version is missing)");
 278					ok = false;
 279					continue;
 280				}
 281
 282				String pluginName = arg.substring(0,index2);
 283				String needVersion = arg.substring(index2 + 1);
 284				String currVersion = jEdit.getProperty("plugin." 
 285					+ pluginName + ".version");
 286
 287				EditPlugin plugin = jEdit.getPlugin(pluginName);
 288				if(plugin == null)
 289				{
 290					if(!optional)
 291					{
 292						String[] args = { needVersion,
 293							pluginName };
 294						jEdit.pluginError(path,
 295							"plugin-error.dep-plugin.no-version",
 296							args);
 297						ok = false;
 298					}
 299				}
 300				else if(MiscUtilities.compareStrings(
 301					currVersion,needVersion,false) < 0)
 302				{
 303					if(!optional)
 304					{
 305						String[] args = { needVersion,
 306							pluginName, currVersion };
 307						jEdit.pluginError(path,
 308							"plugin-error.dep-plugin",args);
 309						ok = false;
 310					}
 311				}
 312				else if(plugin instanceof EditPlugin.Broken)
 313				{
 314					if(!optional)
 315					{
 316						String[] args = { pluginName };
 317						jEdit.pluginError(path,
 318							"plugin-error.dep-plugin.broken",args);
 319						ok = false;
 320					}
 321				}
 322				else
 323				{
 324					PluginJAR jar = plugin.getPluginJAR();
 325					jar.theseRequireMe.add(path);
 326					weRequireThese.add(jar.getPath());
 327				}
 328			}
 329			else if(what.equals("class"))
 330			{
 331				if(!optional)
 332				{
 333					try
 334					{
 335						classLoader.loadClass(arg,false);
 336					}
 337					catch(Exception e)
 338					{
 339						String[] args = { arg };
 340						jEdit.pluginError(path,
 341							"plugin-error.dep-class",args);
 342						ok = false;
 343					}
 344				}
 345			}
 346			else
 347			{
 348				Log.log(Log.ERROR,this,name + " has unknown"
 349					+ " dependency: " + dep);
 350				ok = false;
 351			}
 352		}
 353
 354		// each JAR file listed in the plugin's jars property
 355		// needs to know that we need them
 356		String jars = jEdit.getProperty("plugin."
 357			+ plugin.getClassName() + ".jars");
 358		if(jars != null)
 359		{
 360			String dir = MiscUtilities.getParentOfPath(path);
 361
 362			StringTokenizer st = new StringTokenizer(jars);
 363			while(st.hasMoreTokens())
 364			{
 365				String jarPath = MiscUtilities.constructPath(
 366					dir,st.nextToken());
 367				PluginJAR jar = jEdit.getPluginJAR(jarPath);
 368				if(jar == null)
 369				{
 370					String[] args = { jarPath };
 371					jEdit.pluginError(path,
 372						"plugin-error.missing-jar",args);
 373					ok = false;
 374				}
 375				else
 376				{
 377					weRequireThese.add(jarPath);
 378					jar.theseRequireMe.add(path);
 379				}
 380			}
 381		}
 382
 383		if(!ok)
 384			breakPlugin();
 385
 386		return ok;
 387	} //}}}
 388
 389	//{{{ getDependentPlugins() method
 390	/**
 391	 * Returns an array of all plugins that depend on this one.
 392	 * @since jEdit 4.2pre2
 393	 */
 394	public String[] getDependentPlugins()
 395	{
 396		return (String[])theseRequireMe.toArray(
 397			new String[theseRequireMe.size()]);
 398	} //}}}
 399
 400	//{{{ getPlugin() method
 401	/**
 402	 * Returns the plugin core class for this JAR file. Note that if the
 403	 * plugin has not been activated, this will return an instance of
 404	 * {@link EditPlugin.Deferred}. If you need the actual plugin core
 405	 * class instance, call {@link #activatePlugin()} first.
 406	 *
 407	 * @since jEdit 4.2pre1
 408	 */
 409	public EditPlugin getPlugin()
 410	{
 411		return plugin;
 412	} //}}}
 413
 414	//{{{ activatePlugin() method
 415	/**
 416	 * Loads the plugin core class. Does nothing if the plugin core class
 417	 * has already been loaded. This method might be called on startup,
 418	 * depending on what properties are set. See {@link EditPlugin#start()}.
 419	 * This method is thread-safe.
 420	 *
 421	 * @since jEdit 4.2pre1
 422	 */
 423	public void activatePlugin()
 424	{
 425		synchronized(this)
 426		{
 427			if(activated)
 428			{
 429				// recursive call
 430				return;
 431			}
 432
 433			activated = true;
 434		}
 435
 436		if(!(plugin instanceof EditPlugin.Deferred))
 437			return;
 438
 439		String className = plugin.getClassName();
 440
 441		try
 442		{
 443			Class clazz = classLoader.loadClass(className,false);
 444			int modifiers = clazz.getModifiers();
 445			if(Modifier.isInterface(modifiers)
 446				|| Modifier.isAbstract(modifiers)
 447				|| !EditPlugin.class.isAssignableFrom(clazz))
 448			{
 449				Log.log(Log.ERROR,this,"Plugin has properties but does not extend EditPlugin: "
 450					+ className);
 451				breakPlugin();
 452				return;
 453			}
 454
 455			plugin = (EditPlugin)clazz.newInstance();
 456			plugin.jar = this;
 457		}
 458		catch(Throwable t)
 459		{
 460			breakPlugin();
 461
 462			Log.log(Log.ERROR,this,"Error while starting plugin " + className);
 463			Log.log(Log.ERROR,this,t);
 464			String[] args = { t.toString() };
 465			jEdit.pluginError(path,"plugin-error.start-error",args);
 466
 467			return;
 468		}
 469
 470		if(jEdit.isMainThread()
 471			|| SwingUtilities.isEventDispatchThread())
 472		{
 473			startPlugin();
 474		}
 475		else
 476		{
 477			// for thread safety
 478			startPluginLater();
 479		}
 480
 481		EditBus.send(new PluginUpdate(this,PluginUpdate.ACTIVATED,false));
 482	} //}}}
 483
 484	//{{{ activateIfNecessary() method
 485	/**
 486	 * Should be called after a new plugin is installed.
 487	 * @since jEdit 4.2pre2
 488	 */
 489	public void activatePluginIfNecessary()
 490	{
 491		if(!(plugin instanceof EditPlugin.Deferred && plugin != null))
 492			return;
 493
 494		String className = plugin.getClassName();
 495
 496		// default for plugins that don't specify this property (ie,
 497		// 4.1-style plugins) is to load them on startup
 498		String activate = jEdit.getProperty("plugin."
 499			+ className + ".activate");
 500
 501		if(activate == null)
 502		{
 503			// 4.1 plugin
 504			if(!jEdit.isMainThread())
 505			{
 506				breakPlugin();
 507
 508				jEdit.pluginError(path,"plugin-error.not-42",null);
 509			}
 510			else
 511				activatePlugin();
 512		}
 513		else
 514		{
 515			// 4.2 plugin
 516
 517			// if at least one property listed here is true,
 518			// load the plugin
 519			boolean load = false;
 520
 521			StringTokenizer st = new StringTokenizer(activate);
 522			while(st.hasMoreTokens())
 523			{
 524				String prop = st.nextToken();
 525				boolean value = jEdit.getBooleanProperty(prop);
 526				if(value)
 527				{
 528					Log.log(Log.DEBUG,this,"Activating "
 529						+ className + " because of " + prop);
 530					load = true;
 531					break;
 532				}
 533			}
 534
 535			if(load)
 536				activatePlugin();
 537		}
 538	} //}}}
 539
 540	//{{{ deactivatePlugin() method
 541	/**
 542	 * Unloads the plugin core class. Does nothing if the plugin core class
 543	 * has not been loaded.
 544	 * This method can only be called from the AWT event dispatch thread!
 545	 * @see EditPlugin#stop()
 546	 *
 547	 * @since jEdit 4.2pre3
 548	 */
 549	public void deactivatePlugin(boolean exit)
 550	{
 551		if(!activated)
 552			return;
 553
 554		if(!exit)
 555		{
 556			// buffers retain a reference to the fold handler in
 557			// question... and the easiest way to handle fold
 558			// handler unloading is this...
 559			Buffer buffer = jEdit.getFirstBuffer();
 560			while(buffer != null)
 561			{
 562				if(buffer.getFoldHandler() != null
 563					&& buffer.getFoldHandler().getClass()
 564					.getClassLoader() == classLoader)
 565				{
 566					buffer.setFoldHandler(
 567						new DummyFoldHandler());
 568				}
 569				buffer = buffer.getNext();
 570			}
 571		}
 572
 573		if(plugin != null && !(plugin instanceof EditPlugin.Broken))
 574		{
 575			if(plugin instanceof EBPlugin)
 576				EditBus.removeFromBus((EBPlugin)plugin);
 577
 578			try
 579			{
 580				plugin.stop();
 581			}
 582			catch(Throwable t)
 583			{
 584				Log.log(Log.ERROR,this,"Error while "
 585					+ "stopping plugin:");
 586				Log.log(Log.ERROR,this,t);
 587			}
 588
 589			plugin = new EditPlugin.Deferred(this,
 590				plugin.getClassName());
 591
 592			EditBus.send(new PluginUpdate(this,
 593				PluginUpdate.DEACTIVATED,exit));
 594
 595			if(!exit)
 596			{
 597				// see if this is a 4.1-style plugin
 598				String activate = jEdit.getProperty("plugin."
 599					+ plugin.getClassName() + ".activate");
 600
 601				if(activate == null)
 602				{
 603					breakPlugin();
 604					jEdit.pluginError(path,"plugin-error.not-42",null);
 605				}
 606			}
 607		}
 608
 609		activated = false;
 610	} //}}}
 611
 612	//{{{ getDockablesURI() method
 613	/**
 614	 * Returns the location of the plugin's
 615	 * <code>dockables.xml</code> file.
 616	 * @since jEdit 4.2pre1
 617	 */
 618	public URL getDockablesURI()
 619	{
 620		return dockablesURI;
 621	} //}}}
 622
 623	//{{{ getServicesURI() method
 624	/**
 625	 * Returns the location of the plugin's
 626	 * <code>services.xml</code> file.
 627	 * @since jEdit 4.2pre1
 628	 */
 629	public URL getServicesURI()
 630	{
 631		return servicesURI;
 632	} //}}}
 633
 634	//{{{ toString() method
 635	public String toString()
 636	{
 637		if(plugin == null)
 638			return path;
 639		else
 640			return path + ",class=" + plugin.getClassName();
 641	} //}}}
 642
 643	//{{{ Package-private members
 644
 645	//{{{ Static methods
 646
 647	//{{{ getPluginCache() method
 648	static PluginCacheEntry getPluginCache(PluginJAR plugin)
 649	{
 650		String jarCachePath = plugin.getCachePath();
 651		if(jarCachePath == null)
 652			return null;
 653
 654		DataInputStream din = null;
 655		try
 656		{
 657			PluginCacheEntry cache = new PluginCacheEntry();
 658			cache.plugin = plugin;
 659			cache.modTime = plugin.getFile().lastModified();
 660			din = new DataInputStream(
 661				new BufferedInputStream(
 662				new FileInputStream(jarCachePath)));
 663			if(cache.read(din))
 664				return cache;
 665			else
 666			{
 667				// returns false with outdated cache
 668				return null;
 669			}
 670		}
 671		catch(FileNotFoundException fnf)
 672		{
 673			return null;
 674		}
 675		catch(IOException io)
 676		{
 677			Log.log(Log.ERROR,PluginJAR.class,io);
 678			return null;
 679		}
 680		finally
 681		{
 682			try
 683			{
 684				if(din != null)
 685					din.close();
 686			}
 687			catch(IOException io)
 688			{
 689				Log.log(Log.ERROR,PluginJAR.class,io);
 690			}
 691		}
 692	} //}}}
 693
 694	//{{{ setPluginCache() method
 695	static void setPluginCache(PluginJAR plugin, PluginCacheEntry cache)
 696	{
 697		String jarCachePath = plugin.getCachePath();
 698		if(jarCachePath == null)
 699			return;
 700
 701		Log.log(Log.DEBUG,PluginJAR.class,"Writing " + jarCachePath);
 702
 703		DataOutputStream dout = null;
 704		try
 705		{
 706			dout = new DataOutputStream(
 707				new BufferedOutputStream(
 708				new FileOutputStream(jarCachePath)));
 709			cache.write(dout);
 710			dout.close();
 711		}
 712		catch(IOException io)
 713		{
 714			Log.log(Log.ERROR,PluginJAR.class,io);
 715			try
 716			{
 717				if(dout != null)
 718					dout.close();
 719			}
 720			catch(IOException io2)
 721			{
 722				Log.log(Log.ERROR,PluginJAR.class,io2);
 723			}
 724			new File(jarCachePath).delete();
 725		}
 726	} //}}}
 727
 728	//}}}
 729
 730	//{{{ PluginJAR constructor
 731	PluginJAR(File file)
 732	{
 733		this.path = file.getPath();
 734		String jarCacheDir = jEdit.getJARCacheDirectory();
 735		if(jarCacheDir != null)
 736		{
 737			cachePath = MiscUtilities.constructPath(
 738				jarCacheDir,file.getName() + ".summary");
 739		}
 740		this.file = file;
 741		classLoader = new JARClassLoader(this);
 742		actions = new ActionSet();
 743	} //}}}
 744
 745	//{{{ init() method
 746	void init()
 747	{
 748		boolean initialized = false;
 749
 750		PluginCacheEntry cache = getPluginCache(this);
 751		if(cache != null)
 752		{
 753			loadCache(cache);
 754			classLoader.activate();
 755			initialized = true;
 756		}
 757		else
 758		{
 759			try
 760			{
 761				cache = generateCache();
 762				if(cache != null)
 763				{
 764					setPluginCache(this,cache);
 765					classLoader.activate();
 766					initialized = true;
 767				}
 768			}
 769			catch(IOException io)
 770			{
 771				Log.log(Log.ERROR,this,"Cannot load"
 772					+ " plugin " + path);
 773				Log.log(Log.ERROR,this,io);
 774
 775				String[] args = { io.toString() };
 776				jEdit.pluginError(path,"plugin-error.load-error",args);
 777
 778				uninit(false);
 779			}
 780		}
 781	} //}}}
 782
 783	//{{{ uninit() method
 784	void uninit(boolean exit)
 785	{
 786		deactivatePlugin(exit);
 787
 788		if(!exit)
 789		{
 790			Iterator iter = weRequireThese.iterator();
 791			while(iter.hasNext())
 792			{
 793				String path = (String)iter.next();
 794				PluginJAR jar = jEdit.getPluginJAR(path);
 795				if(jar != null)
 796					jar.theseRequireMe.remove(this.path);
 797			}
 798
 799			classLoader.deactivate();
 800			BeanShell.resetClassManager();
 801
 802			if(actions != null)
 803				jEdit.removeActionSet(actions);
 804			if(browserActions != null)
 805				VFSBrowser.getActionContext().removeActionSet(browserActions);
 806
 807			DockableWindowFactory.getInstance()
 808				.unloadDockableWindows(this);
 809			ServiceManager.unloadServices(this);
 810
 811			jEdit.removePluginProps(properties);
 812
 813			try
 814			{
 815				if(zipFile != null)
 816				{
 817					zipFile.close();
 818					zipFile = null;
 819				}
 820			}
 821			catch(IOException io)
 822			{
 823				Log.log(Log.ERROR,this,io);
 824			}
 825		}
 826	} //}}}
 827
 828	//{{{ getClasses() method
 829	String[] getClasses()
 830	{
 831		return classes;
 832	} //}}}
 833
 834	//}}}
 835
 836	//{{{ Private members
 837
 838	//{{{ Instance variables
 839	private String path;
 840	private String cachePath;
 841	private File file;
 842
 843	private JARClassLoader classLoader;
 844	private ZipFile zipFile;
 845	private Properties properties;
 846	private String[] classes;
 847	private ActionSet actions;
 848	private ActionSet browserActions;
 849
 850	private EditPlugin plugin;
 851
 852	private URL dockablesURI;
 853	private URL servicesURI;
 854
 855	private boolean activated;
 856
 857	private List theseRequireMe = new LinkedList();
 858	private List weRequireThese = new LinkedList();
 859	//}}}
 860
 861	//{{{ actionsPresentButNotCoreClass() method
 862	private void actionsPresentButNotCoreClass()
 863	{
 864		Log.log(Log.WARNING,this,getPath() + " has an actions.xml but no plugin core class");
 865		actions.setLabel("MISSING PLUGIN CORE CLASS");
 866	} //}}}
 867
 868	//{{{ loadCache() method
 869	private void loadCache(PluginCacheEntry cache)
 870	{
 871		classes = cache.classes;
 872
 873		/* this should be before dockables are initialized */
 874		if(cache.cachedProperties != null)
 875		{
 876			properties = cache.cachedProperties;
 877			jEdit.addPluginProps(cache.cachedProperties);
 878		}
 879
 880		if(cache.actionsURI != null
 881			&& cache.cachedActionNames != null)
 882		{
 883			actions = new ActionSet(this,
 884				cache.cachedActionNames,
 885				cache.cachedActionToggleFlags,
 886				cache.actionsURI);
 887		}
 888
 889		if(cache.browserActionsURI != null
 890			&& cache.cachedBrowserActionNames != null)
 891		{
 892			browserActions = new ActionSet(this,
 893				cache.cachedBrowserActionNames,
 894				cache.cachedBrowserActionToggleFlags,
 895				cache.browserActionsURI);
 896			VFSBrowser.getActionContext().addActionSet(browserActions);
 897		}
 898
 899		if(cache.dockablesURI != null
 900			&& cache.cachedDockableNames != null
 901			&& cache.cachedDockableActionFlags != null)
 902		{
 903			dockablesURI = cache.dockablesURI;
 904			DockableWindowFactory.getInstance()
 905				.cacheDockableWindows(this,
 906				cache.cachedDockableNames,
 907				cache.cachedDockableActionFlags);
 908		}
 909
 910		if(actions.size() != 0)
 911			jEdit.addActionSet(actions);
 912
 913		if(cache.servicesURI != null
 914			&& cache.cachedServices != null)
 915		{
 916			servicesURI = cache.servicesURI;
 917			for(int i = 0; i < cache.cachedServices.length;
 918				i++)
 919			{
 920				ServiceManager.Descriptor d
 921					= cache.cachedServices[i];
 922				ServiceManager.registerService(d);
 923			}
 924		}
 925
 926		if(cache.pluginClass != null)
 927		{
 928			// Check if a plugin with the same name
 929			// is already loaded
 930			if(jEdit.getPlugin(cache.pluginClass) != null)
 931			{
 932				jEdit.pluginError(path,
 933					"plugin-error.already-loaded",
 934					null);
 935				uninit(false);
 936			}
 937			else
 938			{
 939				String label = jEdit.getProperty(
 940					"plugin." + cache.pluginClass
 941					+ ".name");
 942				actions.setLabel(jEdit.getProperty(
 943					"action-set.plugin",
 944					new String[] { label }));
 945				plugin = new EditPlugin.Deferred(this,
 946					cache.pluginClass);
 947			}
 948		}
 949		else
 950		{
 951			if(actions.size() != 0)
 952				actionsPresentButNotCoreClass();
 953		}
 954	} //}}}
 955
 956	//{{{ generateCache() method
 957	private PluginCacheEntry generateCache() throws IOException
 958	{
 959		properties = new Properties();
 960
 961		LinkedList classes = new LinkedList();
 962
 963		ZipFile zipFile = getZipFile();
 964
 965		List plugins = new LinkedList();
 966
 967		PluginCacheEntry cache = new PluginCacheEntry();
 968		cache.modTime = file.lastModified();
 969		cache.cachedProperties = new Properties();
 970
 971		Enumeration entries = zipFile.entries();
 972		while(entries.hasMoreElements())
 973		{
 974			ZipEntry entry = (ZipEntry)
 975				entries.nextElement();
 976			String name = entry.getName();
 977			String lname = name.toLowerCase();
 978			if(lname.equals("actions.xml"))
 979			{
 980				cache.actionsURI = classLoader.getResource(name);
 981			}
 982			else if(lname.equals("browser.actions.xml"))
 983			{
 984				cache.browserActionsURI = classLoader.getResource(name);
 985			}
 986			else if(lname.equals("dockables.xml"))
 987			{
 988				dockablesURI = classLoader.getResource(name);
 989				cache.dockablesURI = dockablesURI;
 990			}
 991			else if(lname.equals("services.xml"))
 992			{
 993				servicesURI = classLoader.getResource(name);
 994				cache.servicesURI = servicesURI;
 995			}
 996			else if(lname.endsWith(".props"))
 997			{
 998				InputStream in = classLoader.getResourceAsStream(name);
 999				properties.load(in);
1000				in.close();
1001			}
1002			else if(name.endsWith(".class"))
1003			{
1004				String className = MiscUtilities
1005					.fileToClass(name);
1006				if(className.endsWith("Plugin"))
1007				{
1008					plugins.add(className);
1009				}
1010				classes.add(className);
1011			}
1012		}
1013
1014		cache.cachedProperties = properties;
1015		jEdit.addPluginProps(properties);
1016
1017		this.classes = cache.classes =
1018			(String[])classes.toArray(
1019			new String[classes.size()]);
1020
1021		String label = null;
1022
1023		Iterator iter = plugins.iterator();
1024		while(iter.hasNext())
1025		{
1026			String className = (String)iter.next();
1027
1028			String _label = jEdit.getProperty("plugin."
1029				+ className + ".name");
1030			String version = jEdit.getProperty("plugin."
1031				+ className + ".version");
1032			if(_label == null || version == null)
1033			{
1034				Log.log(Log.WARNING,this,"Ignoring: "
1035					+ className);
1036			}
1037			else
1038			{
1039				cache.pluginClass = className;
1040
1041				// Check if a plugin with the same name
1042				// is already loaded
1043				if(jEdit.getPlugin(className) != null)
1044				{
1045					jEdit.pluginError(path,
1046						"plugin-error.already-loaded",
1047						null);
1048					return null;
1049				}
1050				else
1051				{
1052					plugin = new EditPlugin.Deferred(this,
1053						className);
1054					label = _label;
1055				}
1056
1057				break;
1058			}
1059		}
1060
1061		if(cache.actionsURI != null)
1062		{
1063			actions = new ActionSet(this,null,null,
1064				cache.actionsURI);
1065			actions.load();
1066			cache.cachedActionNames =
1067				actions.getCacheableActionNames();
1068			cache.cachedActionToggleFlags = new boolean[
1069				cache.cachedActionNames.length];
1070			for(int i = 0; i < cache.cachedActionNames.length; i++)
1071			{
1072				 cache.cachedActionToggleFlags[i]
1073				 	= jEdit.getBooleanProperty(
1074					cache.cachedActionNames[i]
1075					+ ".toggle");
1076			}
1077		}
1078
1079		if(cache.browserActionsURI != null)
1080		{
1081			browserActions = new ActionSet(this,null,null,
1082				cache.browserActionsURI);
1083			browserActions.load();
1084			VFSBrowser.getActionContext().addActionSet(browserActions);
1085			cache.cachedBrowserActionNames =
1086				browserActions.getCacheableActionNames();
1087			cache.cachedBrowserActionToggleFlags = new boolean[
1088				cache.cachedBrowserActionNames.length];
1089			for(int i = 0;
1090				i < cache.cachedBrowserActionNames.length;
1091				i++)
1092			{
1093				 cache.cachedBrowserActionToggleFlags[i]
1094				 	= jEdit.getBooleanProperty(
1095					cache.cachedBrowserActionNames[i]
1096					+ ".toggle");
1097			}
1098		}
1099
1100		if(dockablesURI != null)
1101		{
1102			DockableWindowFactory.getInstance()
1103				.loadDockableWindows(this,
1104				dockablesURI,cache);
1105		}
1106
1107		if(actions.size() != 0)
1108		{
1109			if(label != null)
1110			{
1111				actions.setLabel(jEdit.getProperty(
1112					"action-set.plugin",
1113					new String[] { label }));
1114			}
1115			else
1116				actionsPresentButNotCoreClass();
1117
1118			jEdit.addActionSet(actions);
1119		}
1120
1121		if(servicesURI != null)
1122		{
1123			ServiceManager.loadServices(this,servicesURI,cache);
1124		}
1125
1126		return cache;
1127	} //}}}
1128
1129	//{{{ startPlugin() method
1130	private void startPlugin()
1131	{
1132		try
1133		{
1134			plugin.start();
1135		}
1136		catch(Throwable t)
1137		{
1138			breakPlugin();
1139
1140			Log.log(Log.ERROR,PluginJAR.this,
1141				"Error while starting plugin " + plugin.getClassName());
1142			Log.log(Log.ERROR,PluginJAR.this,t);
1143			String[] args = { t.toString() };
1144			jEdit.pluginError(path,"plugin-error.start-error",args);
1145		}
1146
1147		if(plugin instanceof EBPlugin)
1148		{
1149			if(jEdit.getProperty("plugin."
1150				+ plugin.getClassName() + ".activate")
1151				== null)
1152			{
1153				// old plugins expected jEdit 4.1-style
1154				// behavior, where a PropertiesChanged
1155				// was sent after plugins were started
1156				((EBComponent)plugin).handleMessage(
1157					new org.gjt.sp.jedit.msg.PropertiesChanged(null));
1158			}
1159			EditBus.addToBus((EBPlugin)plugin);
1160		}
1161
1162		// buffers retain a reference to the fold handler in
1163		// question... and the easiest way to handle fold
1164		// handler loading is this...
1165		Buffer buffer = jEdit.getFirstBuffer();
1166		while(buffer != null)
1167		{
1168			FoldHandler handler =
1169				FoldHandler.getFoldHandler(
1170				buffer.getStringProperty("folding"));
1171			// == null before loaded
1172			if(buffer.getFoldHandler() != null
1173				&& handler != null
1174				&& handler != buffer.getFoldHandler())
1175			{
1176				buffer.setFoldHandler(handler);
1177			}
1178			buffer = buffer.getNext();
1179		}
1180	} //}}}
1181
1182	//{{{ startPluginLater() method
1183	private void startPluginLater()
1184	{
1185		SwingUtilities.invokeLater(new Runnable()
1186		{
1187			public void run()
1188			{
1189				if(!activated)
1190					return;
1191
1192				startPlugin();
1193			}
1194		});
1195	} //}}}
1196
1197	//{{{ breakPlugin() method
1198	private void breakPlugin()
1199	{
1200		plugin = new EditPlugin.Broken(this,plugin.getClassName());
1201
1202		// remove action sets, dockables, etc so that user doesn't
1203		// see the broken plugin
1204		uninit(false);
1205		// but we want properties to hang around
1206		jEdit.addPluginProps(properties);
1207	} //}}}
1208
1209	//}}}
1210
1211	//{{{ PluginCacheEntry class
1212	/**
1213	 * Used by the <code>DockableWindowManager</code> and
1214	 * <code>ServiceManager</code> to handle caching.
1215	 * @since jEdit 4.2pre1
1216	 */
1217	public static class PluginCacheEntry
1218	{
1219		public static final int MAGIC = 0xB7A2E420;
1220
1221		//{{{ Instance variables
1222		public PluginJAR plugin;
1223		public long modTime;
1224
1225		public String[] classes;
1226		public URL actionsURI;
1227		public String[] cachedActionNames;
1228		public boolean[] cachedActionToggleFlags;
1229		public URL browserActionsURI;
1230		public String[] cachedBrowserActionNames;
1231		public boolean[] cachedBrowserActionToggleFlags;
1232		public URL dockablesURI;
1233		public String[] cachedDockableNames;
1234		public boolean[] cachedDockableActionFlags;
1235		public URL servicesURI;
1236		public ServiceManager.Descriptor[] cachedServices;
1237
1238		public Properties cachedProperties;
1239		public String pluginClass;
1240		//}}}
1241
1242		/* read() and write() must be kept perfectly in sync...
1243		 * its a very simple file format. doing it this way is
1244		 * faster than serializing since serialization calls
1245		 * reflection, etc. */
1246
1247		//{{{ read() method
1248		public boolean read(DataInputStream din) throws IOException
1249		{
1250			int cacheMagic = din.readInt();
1251			if(cacheMagic != MAGIC)
1252				return false;
1253
1254			String cacheBuild = readString(din);
1255			if(!cacheBuild.equals(jEdit.getBuild()))
1256				return false;
1257
1258			long cacheModTime = din.readLong();
1259			if(cacheModTime != modTime)
1260				return false;
1261
1262			actionsURI = readURI(din);
1263			cachedActionNames = readStringArray(din);
1264			cachedActionToggleFlags = readBooleanArray(din);
1265
1266			browserActionsURI = readURI(din);
1267			cachedBrowserActionNames = readStringArray(din);
1268			cachedBrowserActionToggleFlags = readBooleanArray(din);
1269
1270			dockablesURI = readURI(din);
1271			cachedDockableNames = readStringArray(din);
1272			cachedDockableActionFlags = readBooleanArray(din);
1273
1274			servicesURI = readURI(din);
1275			int len = din.readInt();
1276			if(len == 0)
1277				cachedServices = null;
1278			else
1279			{
1280				cachedServices = new ServiceManager.Descriptor[len];
1281				for(int i = 0; i < len; i++)
1282				{
1283					ServiceManager.Descriptor d = new
1284						ServiceManager.Descriptor(
1285						readString(din),
1286						readString(din),
1287						null,
1288						plugin);
1289					cachedServices[i] = d;
1290				}
1291			}
1292
1293			classes = readStringArray(din);
1294
1295			cachedProperties = readMap(din);
1296
1297			pluginClass = readString(din);
1298
1299			return true;
1300		} //}}}
1301
1302		//{{{ write() method
1303		public void write(DataOutputStream dout) throws IOException
1304		{
1305			dout.writeInt(MAGIC);
1306			writeString(dout,jEdit.getBuild());
1307
1308			dout.writeLong(modTime);
1309
1310			writeString(dout,actionsURI);
1311			writeStringArray(dout,cachedActionNames);
1312			writeBooleanArray(dout,cachedActionToggleFlags);
1313
1314			writeString(dout,browserActionsURI);
1315			writeStringArray(dout,cachedBrowserActionNames);
1316			writeBooleanArray(dout,cachedBrowserActionToggleFlags);
1317
1318			writeString(dout,dockablesURI);
1319			writeStringArray(dout,cachedDockableNames);
1320			writeBooleanArray(dout,cachedDockableActionFlags);
1321
1322			writeString(dout,servicesURI);
1323			if(cachedServices == null)
1324				dout.writeInt(0);
1325			else
1326			{
1327				dout.writeInt(cachedServices.length);
1328				for(int i = 0; i < cachedServices.length; i++)
1329				{
1330					writeString(dout,cachedServices[i].clazz);
1331					writeString(dout,cachedServices[i].name);
1332				}
1333			}
1334
1335			writeStringArray(dout,classes);
1336
1337			writeMap(dout,cachedProperties);
1338
1339			writeString(dout,pluginClass);
1340		} //}}}
1341
1342		//{{{ Private members
1343
1344		//{{{ readString() method
1345		private String readString(DataInputStream din)
1346			throws IOException
1347		{
1348			int len = din.readInt();
1349			if(len == 0)
1350				return null;
1351			char[] str = new char[len];
1352			for(int i = 0; i < len; i++)
1353				str[i] = din.readChar();
1354			return new String(str);
1355		} //}}}
1356
1357		//{{{ readURI() method
1358		private URL readURI(DataInputStream din)
1359			throws IOException
1360		{
1361			String str = readString(din);
1362			if(str == null)
1363				return null;
1364			else
1365				return new URL(str);
1366		} //}}}
1367
1368		//{{{ readStringArray() method
1369		private String[] readStringArray(DataInputStream din)
1370			throws IOException
1371		{
1372			int len = din.readInt();
1373			if(len == 0)
1374				return null;
1375			String[] str = new String[len];
1376			for(int i = 0; i < len; i++)
1377			{
1378				str[i] = readString(din);
1379			}
1380			return str;
1381		} //}}}
1382
1383		//{{{ readBooleanArray() method
1384		private boolean[] readBooleanArray(DataInputStream din)
1385			throws IOException
1386		{
1387			int len = din.readInt();
1388			if(len == 0)
1389				return null;
1390			boolean[] bools = new boolean[len];
1391			for(int i = 0; i < len; i++)
1392			{
1393				bools[i] = din.readBoolean();
1394			}
1395			return bools;
1396		} //}}}
1397
1398		//{{{ readMap() method
1399		private Properties readMap(DataInputStream din)
1400			throws IOException
1401		{
1402			Properties returnValue = new Properties();
1403			int count = din.readInt();
1404			for(int i = 0; i < count; i++)
1405			{
1406				String key = readString(din);
1407				String value = readString(din);
1408				if(value == null)
1409					value = "";
1410				returnValue.put(key,value);
1411			}
1412			return returnValue;
1413		} //}}}
1414
1415		//{{{ writeString() method
1416		private void writeString(DataOutputStream dout,
1417			Object obj) throws IOException
1418		{
1419			if(obj == null)
1420			{
1421				dout.writeInt(0);
1422			}
1423			else
1424			{
1425				String str = obj.toString();
1426				dout.writeInt(str.length());
1427				dout.writeChars(str);
1428			}
1429		} //}}}
1430
1431		//{{{ writeStringArray() method
1432		private void writeStringArray(DataOutputStream dout,
1433			String[] str) throws IOException
1434		{
1435			if(str == null)
1436			{
1437				dout.writeInt(0);
1438			}
1439			else
1440			{
1441				dout.writeInt(str.length);
1442				for(int i = 0; i < str.length; i++)
1443				{
1444					writeString(dout,str[i]);
1445				}
1446			}
1447		} //}}}
1448
1449		//{{{ writeBooleanArray() method
1450		private void writeBooleanArray(DataOutputStream dout,
1451			boolean[] bools) throws IOException
1452		{
1453			if(bools == null)
1454			{
1455				dout.writeInt(0);
1456			}
1457			else
1458			{
1459				dout.writeInt(bools.length);
1460				for(int i = 0; i < bools.length; i++)
1461				{
1462					dout.writeBoolean(bools[i]);
1463				}
1464			}
1465		} //}}}
1466
1467		//{{{ writeMap() method
1468		private void writeMap(DataOutputStream dout, Map map)
1469			throws IOException
1470		{
1471			dout.writeInt(map.size());
1472			Iterator iter = map.keySet().iterator();
1473			while(iter.hasNext())
1474			{
1475				String key = (String)iter.next();
1476				writeString(dout,key);
1477				writeString(dout,map.get(key));
1478			}
1479		} //}}}
1480
1481		//}}}
1482	} //}}}
1483}