PageRenderTime 160ms CodeModel.GetById 58ms app.highlight 88ms RepoModel.GetById 1ms app.codeStats 1ms

/jEdit/tags/jedit-4-2-pre14/org/gjt/sp/jedit/PluginJAR.java

#
Java | 1484 lines | 1057 code | 160 blank | 267 comment | 198 complexity | 7a8aded660449fc4fd9109f490ee8d19 MD5 | raw file
   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.DockableWindowManager;
  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 5023 2004-04-20 19:58:39Z spestov $
 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 = (EditPlugin.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(
 590				plugin.getClassName());
 591			plugin.jar = (EditPlugin.JAR)this;
 592
 593			EditBus.send(new PluginUpdate(this,
 594				PluginUpdate.DEACTIVATED,exit));
 595
 596			if(!exit)
 597			{
 598				// see if this is a 4.1-style plugin
 599				String activate = jEdit.getProperty("plugin."
 600					+ plugin.getClassName() + ".activate");
 601
 602				if(activate == null)
 603				{
 604					breakPlugin();
 605					jEdit.pluginError(path,"plugin-error.not-42",null);
 606				}
 607			}
 608		}
 609
 610		activated = false;
 611	} //}}}
 612
 613	//{{{ getDockablesURI() method
 614	/**
 615	 * Returns the location of the plugin's
 616	 * <code>dockables.xml</code> file.
 617	 * @since jEdit 4.2pre1
 618	 */
 619	public URL getDockablesURI()
 620	{
 621		return dockablesURI;
 622	} //}}}
 623
 624	//{{{ getServicesURI() method
 625	/**
 626	 * Returns the location of the plugin's
 627	 * <code>services.xml</code> file.
 628	 * @since jEdit 4.2pre1
 629	 */
 630	public URL getServicesURI()
 631	{
 632		return servicesURI;
 633	} //}}}
 634
 635	//{{{ toString() method
 636	public String toString()
 637	{
 638		if(plugin == null)
 639			return path;
 640		else
 641			return path + ",class=" + plugin.getClassName();
 642	} //}}}
 643
 644	//{{{ Package-private members
 645
 646	//{{{ Static methods
 647
 648	//{{{ getPluginCache() method
 649	static PluginCacheEntry getPluginCache(PluginJAR plugin)
 650	{
 651		String jarCachePath = plugin.getCachePath();
 652		if(jarCachePath == null)
 653			return null;
 654
 655		DataInputStream din = null;
 656		try
 657		{
 658			PluginCacheEntry cache = new PluginCacheEntry();
 659			cache.plugin = plugin;
 660			cache.modTime = plugin.getFile().lastModified();
 661			din = new DataInputStream(
 662				new BufferedInputStream(
 663				new FileInputStream(jarCachePath)));
 664			if(cache.read(din))
 665				return cache;
 666			else
 667			{
 668				// returns false with outdated cache
 669				return null;
 670			}
 671		}
 672		catch(FileNotFoundException fnf)
 673		{
 674			return null;
 675		}
 676		catch(IOException io)
 677		{
 678			Log.log(Log.ERROR,PluginJAR.class,io);
 679			return null;
 680		}
 681		finally
 682		{
 683			try
 684			{
 685				if(din != null)
 686					din.close();
 687			}
 688			catch(IOException io)
 689			{
 690				Log.log(Log.ERROR,PluginJAR.class,io);
 691			}
 692		}
 693	} //}}}
 694
 695	//{{{ setPluginCache() method
 696	static void setPluginCache(PluginJAR plugin, PluginCacheEntry cache)
 697	{
 698		String jarCachePath = plugin.getCachePath();
 699		if(jarCachePath == null)
 700			return;
 701
 702		Log.log(Log.DEBUG,PluginJAR.class,"Writing " + jarCachePath);
 703
 704		DataOutputStream dout = null;
 705		try
 706		{
 707			dout = new DataOutputStream(
 708				new BufferedOutputStream(
 709				new FileOutputStream(jarCachePath)));
 710			cache.write(dout);
 711			dout.close();
 712		}
 713		catch(IOException io)
 714		{
 715			Log.log(Log.ERROR,PluginJAR.class,io);
 716			try
 717			{
 718				if(dout != null)
 719					dout.close();
 720			}
 721			catch(IOException io2)
 722			{
 723				Log.log(Log.ERROR,PluginJAR.class,io2);
 724			}
 725			new File(jarCachePath).delete();
 726		}
 727	} //}}}
 728
 729	//}}}
 730
 731	//{{{ PluginJAR constructor
 732	PluginJAR(File file)
 733	{
 734		this.path = file.getPath();
 735		String jarCacheDir = jEdit.getJARCacheDirectory();
 736		if(jarCacheDir != null)
 737		{
 738			cachePath = MiscUtilities.constructPath(
 739				jarCacheDir,file.getName() + ".summary");
 740		}
 741		this.file = file;
 742		classLoader = new JARClassLoader(this);
 743		actions = new ActionSet();
 744	} //}}}
 745
 746	//{{{ init() method
 747	void init()
 748	{
 749		boolean initialized = false;
 750
 751		PluginCacheEntry cache = getPluginCache(this);
 752		if(cache != null)
 753		{
 754			loadCache(cache);
 755			classLoader.activate();
 756			initialized = true;
 757		}
 758		else
 759		{
 760			try
 761			{
 762				cache = generateCache();
 763				if(cache != null)
 764				{
 765					setPluginCache(this,cache);
 766					classLoader.activate();
 767					initialized = true;
 768				}
 769			}
 770			catch(IOException io)
 771			{
 772				Log.log(Log.ERROR,this,"Cannot load"
 773					+ " plugin " + path);
 774				Log.log(Log.ERROR,this,io);
 775
 776				String[] args = { io.toString() };
 777				jEdit.pluginError(path,"plugin-error.load-error",args);
 778
 779				uninit(false);
 780			}
 781		}
 782	} //}}}
 783
 784	//{{{ uninit() method
 785	void uninit(boolean exit)
 786	{
 787		deactivatePlugin(exit);
 788
 789		if(!exit)
 790		{
 791			Iterator iter = weRequireThese.iterator();
 792			while(iter.hasNext())
 793			{
 794				String path = (String)iter.next();
 795				PluginJAR jar = jEdit.getPluginJAR(path);
 796				if(jar != null)
 797					jar.theseRequireMe.remove(this.path);
 798			}
 799
 800			classLoader.deactivate();
 801			BeanShell.resetClassManager();
 802
 803			if(actions != null)
 804				jEdit.getActionContext().removeActionSet(actions);
 805			if(browserActions != null)
 806				VFSBrowser.getActionContext().removeActionSet(browserActions);
 807
 808			DockableWindowManager.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			DockableWindowManager.cacheDockableWindows(this,
 905				cache.cachedDockableNames,
 906				cache.cachedDockableActionFlags);
 907		}
 908
 909		if(actions.size() != 0)
 910			jEdit.addActionSet(actions);
 911
 912		if(cache.servicesURI != null
 913			&& cache.cachedServices != null)
 914		{
 915			servicesURI = cache.servicesURI;
 916			for(int i = 0; i < cache.cachedServices.length;
 917				i++)
 918			{
 919				ServiceManager.Descriptor d
 920					= cache.cachedServices[i];
 921				ServiceManager.registerService(d);
 922			}
 923		}
 924
 925		if(cache.pluginClass != null)
 926		{
 927			// Check if a plugin with the same name
 928			// is already loaded
 929			if(jEdit.getPlugin(cache.pluginClass) != null)
 930			{
 931				jEdit.pluginError(path,
 932					"plugin-error.already-loaded",
 933					null);
 934				uninit(false);
 935			}
 936			else
 937			{
 938				String label = jEdit.getProperty(
 939					"plugin." + cache.pluginClass
 940					+ ".name");
 941				actions.setLabel(jEdit.getProperty(
 942					"action-set.plugin",
 943					new String[] { label }));
 944				plugin = new EditPlugin.Deferred(
 945					cache.pluginClass);
 946				plugin.jar = (EditPlugin.JAR)this;
 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(
1053						className);
1054					plugin.jar = (EditPlugin.JAR)this;
1055					label = _label;
1056				}
1057
1058				break;
1059			}
1060		}
1061
1062		if(cache.actionsURI != null)
1063		{
1064			actions = new ActionSet(this,null,null,
1065				cache.actionsURI);
1066			actions.load();
1067			cache.cachedActionNames =
1068				actions.getCacheableActionNames();
1069			cache.cachedActionToggleFlags = new boolean[
1070				cache.cachedActionNames.length];
1071			for(int i = 0; i < cache.cachedActionNames.length; i++)
1072			{
1073				 cache.cachedActionToggleFlags[i]
1074				 	= jEdit.getBooleanProperty(
1075					cache.cachedActionNames[i]
1076					+ ".toggle");
1077			}
1078		}
1079
1080		if(cache.browserActionsURI != null)
1081		{
1082			browserActions = new ActionSet(this,null,null,
1083				cache.browserActionsURI);
1084			browserActions.load();
1085			VFSBrowser.getActionContext().addActionSet(browserActions);
1086			cache.cachedBrowserActionNames =
1087				browserActions.getCacheableActionNames();
1088			cache.cachedBrowserActionToggleFlags = new boolean[
1089				cache.cachedBrowserActionNames.length];
1090			for(int i = 0;
1091				i < cache.cachedBrowserActionNames.length;
1092				i++)
1093			{
1094				 cache.cachedBrowserActionToggleFlags[i]
1095				 	= jEdit.getBooleanProperty(
1096					cache.cachedBrowserActionNames[i]
1097					+ ".toggle");
1098			}
1099		}
1100
1101		if(dockablesURI != null)
1102		{
1103			DockableWindowManager.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(plugin.getClassName());
1201		plugin.jar = (EditPlugin.JAR)this;
1202
1203		// remove action sets, dockables, etc so that user doesn't
1204		// see the broken plugin
1205		uninit(false);
1206		// but we want properties to hang around
1207		jEdit.addPluginProps(properties);
1208	} //}}}
1209
1210	//}}}
1211
1212	//{{{ PluginCacheEntry class
1213	/**
1214	 * Used by the <code>DockableWindowManager</code> and
1215	 * <code>ServiceManager</code> to handle caching.
1216	 * @since jEdit 4.2pre1
1217	 */
1218	public static class PluginCacheEntry
1219	{
1220		public static final int MAGIC = 0xB7A2E420;
1221
1222		//{{{ Instance variables
1223		public PluginJAR plugin;
1224		public long modTime;
1225
1226		public String[] classes;
1227		public URL actionsURI;
1228		public String[] cachedActionNames;
1229		public boolean[] cachedActionToggleFlags;
1230		public URL browserActionsURI;
1231		public String[] cachedBrowserActionNames;
1232		public boolean[] cachedBrowserActionToggleFlags;
1233		public URL dockablesURI;
1234		public String[] cachedDockableNames;
1235		public boolean[] cachedDockableActionFlags;
1236		public URL servicesURI;
1237		public ServiceManager.Descriptor[] cachedServices;
1238
1239		public Properties cachedProperties;
1240		public String pluginClass;
1241		//}}}
1242
1243		/* read() and write() must be kept perfectly in sync...
1244		 * its a very simple file format. doing it this way is
1245		 * faster than serializing since serialization calls
1246		 * reflection, etc. */
1247
1248		//{{{ read() method
1249		public boolean read(DataInputStream din) throws IOException
1250		{
1251			int cacheMagic = din.readInt();
1252			if(cacheMagic != MAGIC)
1253				return false;
1254
1255			String cacheBuild = readString(din);
1256			if(!cacheBuild.equals(jEdit.getBuild()))
1257				return false;
1258
1259			long cacheModTime = din.readLong();
1260			if(cacheModTime != modTime)
1261				return false;
1262
1263			actionsURI = readURI(din);
1264			cachedActionNames = readStringArray(din);
1265			cachedActionToggleFlags = readBooleanArray(din);
1266
1267			browserActionsURI = readURI(din);
1268			cachedBrowserActionNames = readStringArray(din);
1269			cachedBrowserActionToggleFlags = readBooleanArray(din);
1270
1271			dockablesURI = readURI(din);
1272			cachedDockableNames = readStringArray(din);
1273			cachedDockableActionFlags = readBooleanArray(din);
1274
1275			servicesURI = readURI(din);
1276			int len = din.readInt();
1277			if(len == 0)
1278				cachedServices = null;
1279			else
1280			{
1281				cachedServices = new ServiceManager.Descriptor[len];
1282				for(int i = 0; i < len; i++)
1283				{
1284					ServiceManager.Descriptor d = new
1285						ServiceManager.Descriptor(
1286						readString(din),
1287						readString(din),
1288						null,
1289						plugin);
1290					cachedServices[i] = d;
1291				}
1292			}
1293
1294			classes = readStringArray(din);
1295
1296			cachedProperties = readMap(din);
1297
1298			pluginClass = readString(din);
1299
1300			return true;
1301		} //}}}
1302
1303		//{{{ write() method
1304		public void write(DataOutputStream dout) throws IOException
1305		{
1306			dout.writeInt(MAGIC);
1307			writeString(dout,jEdit.getBuild());
1308
1309			dout.writeLong(modTime);
1310
1311			writeString(dout,actionsURI);
1312			writeStringArray(dout,cachedActionNames);
1313			writeBooleanArray(dout,cachedActionToggleFlags);
1314
1315			writeString(dout,browserActionsURI);
1316			writeStringArray(dout,cachedBrowserActionNames);
1317			writeBooleanArray(dout,cachedBrowserActionToggleFlags);
1318
1319			writeString(dout,dockablesURI);
1320			writeStringArray(dout,cachedDockableNames);
1321			writeBooleanArray(dout,cachedDockableActionFlags);
1322
1323			writeString(dout,servicesURI);
1324			if(cachedServices == null)
1325				dout.writeInt(0);
1326			else
1327			{
1328				dout.writeInt(cachedServices.length);
1329				for(int i = 0; i < cachedServices.length; i++)
1330				{
1331					writeString(dout,cachedServices[i].clazz);
1332					writeString(dout,cachedServices[i].name);
1333				}
1334			}
1335
1336			writeStringArray(dout,classes);
1337
1338			writeMap(dout,cachedProperties);
1339
1340			writeString(dout,pluginClass);
1341		} //}}}
1342
1343		//{{{ Private members
1344
1345		//{{{ readString() method
1346		private String readString(DataInputStream din)
1347			throws IOException
1348		{
1349			int len = din.readInt();
1350			if(len == 0)
1351				return null;
1352			char[] str = new char[len];
1353			for(int i = 0; i < len; i++)
1354				str[i] = din.readChar();
1355			return new String(str);
1356		} //}}}
1357
1358		//{{{ readURI() method
1359		private URL readURI(DataInputStream din)
1360			throws IOException
1361		{
1362			String str = readString(din);
1363			if(str == null)
1364				return null;
1365			else
1366				return new URL(str);
1367		} //}}}
1368
1369		//{{{ readStringArray() method
1370		private String[] readStringArray(DataInputStream din)
1371			throws IOException
1372		{
1373			int len = din.readInt();
1374			if(len == 0)
1375				return null;
1376			String[] str = new String[len];
1377			for(int i = 0; i < len; i++)
1378			{
1379				str[i] = readString(din);
1380			}
1381			return str;
1382		} //}}}
1383
1384		//{{{ readBooleanArray() method
1385		private boolean[] readBooleanArray(DataInputStream din)
1386			throws IOException
1387		{
1388			int len = din.readInt();
1389			if(len == 0)
1390				return null;
1391			boolean[] bools = new boolean[len];
1392			for(int i = 0; i < len; i++)
1393			{
1394				bools[i] = din.readBoolean();
1395			}
1396			return bools;
1397		} //}}}
1398
1399		//{{{ readMap() method
1400		private Properties readMap(DataInputStream din)
1401			throws IOException
1402		{
1403			Properties returnValue = new Properties();
1404			int count = din.readInt();
1405			for(int i = 0; i < count; i++)
1406			{
1407				String key = readString(din);
1408				String value = readString(din);
1409				if(value == null)
1410					value = "";
1411				returnValue.put(key,value);
1412			}
1413			return returnValue;
1414		} //}}}
1415
1416		//{{{ writeString() method
1417		private void writeString(DataOutputStream dout,
1418			Object obj) throws IOException
1419		{
1420			if(obj == null)
1421			{
1422				dout.writeInt(0);
1423			}
1424			else
1425			{
1426				String str = obj.toString();
1427				dout.writeInt(str.length());
1428				dout.writeChars(str);
1429			}
1430		} //}}}
1431
1432		//{{{ writeStringArray() method
1433		private void writeStringArray(DataOutputStream dout,
1434			String[] str) throws IOException
1435		{
1436			if(str == null)
1437			{
1438				dout.writeInt(0);
1439			}
1440			else
1441			{
1442				dout.writeInt(str.length);
1443				for(int i = 0; i < str.length; i++)
1444				{
1445					writeString(dout,str[i]);
1446				}
1447			}
1448		} //}}}
1449
1450		//{{{ writeBooleanArray() method
1451		private void writeBooleanArray(DataOutputStream dout,
1452			boolean[] bools) throws IOException
1453		{
1454			if(bools == null)
1455			{
1456				dout.writeInt(0);
1457			}
1458			else
1459			{
1460				dout.writeInt(bools.length);
1461				for(int i = 0; i < bools.length; i++)
1462				{
1463					dout.writeBoolean(bools[i]);
1464				}
1465			}
1466		} //}}}
1467
1468		//{{{ writeMap() method
1469		private void writeMap(DataOutputStream dout, Map map)
1470			throws IOException
1471		{
1472			dout.writeInt(map.size());
1473			Iterator iter = map.keySet().iterator();
1474			while(iter.hasNext())
1475			{
1476				String key = (String)iter.next();
1477				writeString(dout,key);
1478				writeString(dout,map.get(key));
1479			}
1480		} //}}}
1481
1482		//}}}
1483	} //}}}
1484}