Docjar: A Java Source and Docuemnt Enginecom.*    java.*    javax.*    org.*    all    new    plug-in

Quick Search    Search Deep

Source code: org/gjt/sp/jedit/PluginJAR.java


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, 2003 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  
23  package org.gjt.sp.jedit;
24  
25  //{{{ Imports
26  import javax.swing.SwingUtilities;
27  import java.io.*;
28  import java.lang.reflect.Modifier;
29  import java.net.URL;
30  import java.util.*;
31  import java.util.zip.*;
32  import org.gjt.sp.jedit.browser.VFSBrowser;
33  import org.gjt.sp.jedit.buffer.*;
34  import org.gjt.sp.jedit.gui.DockableWindowManager;
35  import org.gjt.sp.jedit.msg.*;
36  import 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,v 1.40 2003/11/16 00:04:08 spestov Exp $
102  * @since jEdit 4.2pre1
103  */
104 public 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 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 
214     String name = plugin.getClassName();
215 
216     String dep;
217     while((dep = jEdit.getProperty("plugin." + name + ".depend." + i++)) != null)
218     {
219       int index = dep.indexOf(' ');
220       if(index == -1)
221       {
222         Log.log(Log.ERROR,this,name + " has an invalid"
223           + " dependency: " + dep);
224         ok = false;
225         continue;
226       }
227 
228       String what = dep.substring(0,index);
229       String arg = dep.substring(index + 1);
230 
231       if(what.equals("jdk"))
232       {
233         if(MiscUtilities.compareStrings(
234           System.getProperty("java.version"),
235           arg,false) < 0)
236         {
237           String[] args = { arg,
238             System.getProperty("java.version") };
239           jEdit.pluginError(path,"plugin-error.dep-jdk",args);
240           ok = false;
241         }
242       }
243       else if(what.equals("jedit"))
244       {
245         if(arg.length() != 11)
246         {
247           Log.log(Log.ERROR,this,"Invalid jEdit version"
248             + " number: " + arg);
249           ok = false;
250         }
251 
252         if(MiscUtilities.compareStrings(
253           jEdit.getBuild(),arg,false) < 0)
254         {
255           String needs = MiscUtilities.buildToVersion(arg);
256           String[] args = { needs,
257             jEdit.getVersion() };
258           jEdit.pluginError(path,
259             "plugin-error.dep-jedit",args);
260           ok = false;
261         }
262       }
263       else if(what.equals("plugin"))
264       {
265         int index2 = arg.indexOf(' ');
266         if(index2 == -1)
267         {
268           Log.log(Log.ERROR,this,name 
269             + " has an invalid dependency: "
270             + dep + " (version is missing)");
271           ok = false;
272           continue;
273         }
274 
275         String pluginName = arg.substring(0,index2);
276         String needVersion = arg.substring(index2 + 1);
277         String currVersion = jEdit.getProperty("plugin." 
278           + pluginName + ".version");
279 
280         EditPlugin plugin = jEdit.getPlugin(pluginName);
281         if(plugin == null)
282         {
283           String[] args = { needVersion,
284             pluginName };
285           jEdit.pluginError(path,
286             "plugin-error.dep-plugin.no-version",
287             args);
288           ok = false;
289         }
290         else if(MiscUtilities.compareStrings(
291           currVersion,needVersion,false) < 0)
292         {
293           String[] args = { needVersion,
294             pluginName, currVersion };
295           jEdit.pluginError(path,
296             "plugin-error.dep-plugin",args);
297           ok = false;
298         }
299         else if(plugin instanceof EditPlugin.Broken)
300         {
301           String[] args = { pluginName };
302           jEdit.pluginError(path,
303             "plugin-error.dep-plugin.broken",args);
304           ok = false;
305         }
306         else
307         {
308           PluginJAR jar = plugin.getPluginJAR();
309           jar.theseRequireMe.add(path);
310           weRequireThese.add(jar.getPath());
311         }
312       }
313       else if(what.equals("class"))
314       {
315         try
316         {
317           classLoader.loadClass(arg,false);
318         }
319         catch(Exception e)
320         {
321           String[] args = { arg };
322           jEdit.pluginError(path,
323             "plugin-error.dep-class",args);
324           ok = false;
325         }
326       }
327       else
328       {
329         Log.log(Log.ERROR,this,name + " has unknown"
330           + " dependency: " + dep);
331         ok = false;
332       }
333     }
334 
335     // each JAR file listed in the plugin's jars property
336     // needs to know that we need them
337     String jars = jEdit.getProperty("plugin."
338       + plugin.getClassName() + ".jars");
339     if(jars != null)
340     {
341       String dir = MiscUtilities.getParentOfPath(path);
342 
343       StringTokenizer st = new StringTokenizer(jars);
344       while(st.hasMoreTokens())
345       {
346         String jarPath = MiscUtilities.constructPath(
347           dir,st.nextToken());
348         PluginJAR jar = jEdit.getPluginJAR(jarPath);
349         if(jar == null)
350         {
351           String[] args = { jarPath };
352           jEdit.pluginError(path,
353             "plugin-error.missing-jar",args);
354           ok = false;
355         }
356         else
357         {
358           weRequireThese.add(jarPath);
359           jar.theseRequireMe.add(path);
360         }
361       }
362     }
363 
364     if(!ok)
365       breakPlugin();
366 
367     return ok;
368   } //}}}
369 
370   //{{{ getDependentPlugins() method
371   /**
372    * Returns an array of all plugins that depend on this one.
373    * @since jEdit 4.2pre2
374    */
375   public String[] getDependentPlugins()
376   {
377     return (String[])theseRequireMe.toArray(
378       new String[theseRequireMe.size()]);
379   } //}}}
380 
381   //{{{ getPlugin() method
382   /**
383    * Returns the plugin core class for this JAR file. Note that if the
384    * plugin has not been activated, this will return an instance of
385    * {@link EditPlugin.Deferred}. If you need the actual plugin core
386    * class instance, call {@link #activatePlugin()} first.
387    *
388    * @since jEdit 4.2pre1
389    */
390   public EditPlugin getPlugin()
391   {
392     return plugin;
393   } //}}}
394 
395   //{{{ activatePlugin() method
396   /**
397    * Loads the plugin core class. Does nothing if the plugin core class
398    * has already been loaded. This method might be called on startup,
399    * depending on what properties are set. See {@link EditPlugin#start()}.
400    * This method is thread-safe.
401    *
402    * @since jEdit 4.2pre1
403    */
404   public void activatePlugin()
405   {
406     synchronized(this)
407     {
408       if(activated)
409       {
410         // recursive call
411         return;
412       }
413 
414       activated = true;
415 
416       if(!(plugin instanceof EditPlugin.Deferred))
417         return;
418 
419       String className = plugin.getClassName();
420 
421       try
422       {
423         Class clazz = classLoader.loadClass(className,false);
424         int modifiers = clazz.getModifiers();
425         if(Modifier.isInterface(modifiers)
426           || Modifier.isAbstract(modifiers)
427           || !EditPlugin.class.isAssignableFrom(clazz))
428         {
429           Log.log(Log.ERROR,this,"Plugin has properties but does not extend EditPlugin: "
430             + className);
431           breakPlugin();
432           return;
433         }
434 
435         plugin = (EditPlugin)clazz.newInstance();
436         plugin.jar = (EditPlugin.JAR)this;
437       }
438       catch(Throwable t)
439       {
440         breakPlugin();
441 
442         Log.log(Log.ERROR,this,"Error while starting plugin " + className);
443         Log.log(Log.ERROR,this,t);
444         String[] args = { t.toString() };
445         jEdit.pluginError(path,"plugin-error.start-error",args);
446 
447         return;
448       }
449     }
450 
451     if(jEdit.isMainThread()
452       || SwingUtilities.isEventDispatchThread())
453     {
454       startPlugin();
455     }
456     else
457     {
458       // for thread safety
459       startPluginLater();
460     }
461 
462     EditBus.send(new PluginUpdate(this,PluginUpdate.ACTIVATED,false));
463   } //}}}
464 
465   //{{{ activateIfNecessary() method
466   /**
467    * Should be called after a new plugin is installed.
468    * @since jEdit 4.2pre2
469    */
470   public void activatePluginIfNecessary()
471   {
472     if(!(plugin instanceof EditPlugin.Deferred && plugin != null))
473       return;
474 
475     String className = plugin.getClassName();
476 
477     // default for plugins that don't specify this property (ie,
478     // 4.1-style plugins) is to load them on startup
479     String activate = jEdit.getProperty("plugin."
480       + className + ".activate");
481 
482     if(activate == null)
483     {
484       // 4.1 plugin
485       if(!jEdit.isMainThread())
486       {
487         breakPlugin();
488 
489         jEdit.pluginError(path,"plugin-error.not-42",null);
490       }
491       else
492         activatePlugin();
493     }
494     else
495     {
496       // 4.2 plugin
497 
498       // if at least one property listed here is true,
499       // load the plugin
500       boolean load = false;
501 
502       StringTokenizer st = new StringTokenizer(activate);
503       while(st.hasMoreTokens())
504       {
505         String prop = st.nextToken();
506         boolean value = jEdit.getBooleanProperty(prop);
507         if(value)
508         {
509           Log.log(Log.DEBUG,this,"Activating "
510             + className + " because of " + prop);
511           load = true;
512           break;
513         }
514       }
515 
516       if(load)
517         activatePlugin();
518     }
519   } //}}}
520 
521   //{{{ deactivatePlugin() method
522   /**
523    * Unloads the plugin core class. Does nothing if the plugin core class
524    * has not been loaded.
525    * This method can only be called from the AWT event dispatch thread!
526    * @see EditPlugin#stop()
527    *
528    * @since jEdit 4.2pre3
529    */
530   public void deactivatePlugin(boolean exit)
531   {
532     synchronized(this)
533     {
534       if(!activated)
535         return;
536 
537       activated = false;
538 
539       if(!exit)
540       {
541         // buffers retain a reference to the fold handler in
542         // question... and the easiest way to handle fold
543         // handler unloading is this...
544         Buffer buffer = jEdit.getFirstBuffer();
545         while(buffer != null)
546         {
547           if(buffer.getFoldHandler() != null
548             && buffer.getFoldHandler().getClass()
549             .getClassLoader() == classLoader)
550           {
551             buffer.setFoldHandler(
552               new DummyFoldHandler());
553           }
554           buffer = buffer.getNext();
555         }
556       }
557 
558       if(plugin != null && !(plugin instanceof EditPlugin.Broken))
559       {
560         if(plugin instanceof EBPlugin)
561           EditBus.removeFromBus((EBPlugin)plugin);
562 
563         try
564         {
565           plugin.stop();
566         }
567         catch(Throwable t)
568         {
569           Log.log(Log.ERROR,this,"Error while "
570             + "stopping plugin:");
571           Log.log(Log.ERROR,this,t);
572         }
573 
574         plugin = new EditPlugin.Deferred(
575           plugin.getClassName());
576         plugin.jar = (EditPlugin.JAR)this;
577 
578         EditBus.send(new PluginUpdate(this,
579           PluginUpdate.DEACTIVATED,exit));
580 
581         if(!exit)
582         {
583           // see if this is a 4.1-style plugin
584           String activate = jEdit.getProperty("plugin."
585             + plugin.getClassName() + ".activate");
586 
587           if(activate == null)
588           {
589             breakPlugin();
590             jEdit.pluginError(path,"plugin-error.not-42",null);
591           }
592         }
593       }
594     }
595   } //}}}
596 
597   //{{{ getDockablesURI() method
598   /**
599    * Returns the location of the plugin's
600    * <code>dockables.xml</code> file.
601    * @since jEdit 4.2pre1
602    */
603   public URL getDockablesURI()
604   {
605     return dockablesURI;
606   } //}}}
607 
608   //{{{ getServicesURI() method
609   /**
610    * Returns the location of the plugin's
611    * <code>services.xml</code> file.
612    * @since jEdit 4.2pre1
613    */
614   public URL getServicesURI()
615   {
616     return servicesURI;
617   } //}}}
618 
619   //{{{ toString() method
620   public String toString()
621   {
622     if(plugin == null)
623       return path;
624     else
625       return path + ",class=" + plugin.getClassName();
626   } //}}}
627 
628   //{{{ Package-private members
629 
630   //{{{ Static methods
631 
632   //{{{ getPluginCache() method
633   static PluginCacheEntry getPluginCache(PluginJAR plugin)
634   {
635     String jarCachePath = plugin.getCachePath();
636     if(jarCachePath == null)
637       return null;
638 
639     DataInputStream din = null;
640     try
641     {
642       PluginCacheEntry cache = new PluginCacheEntry();
643       cache.plugin = plugin;
644       cache.modTime = plugin.getFile().lastModified();
645       din = new DataInputStream(
646         new BufferedInputStream(
647         new FileInputStream(jarCachePath)));
648       if(cache.read(din))
649         return cache;
650       else
651       {
652         // returns false with outdated cache
653         return null;
654       }
655     }
656     catch(FileNotFoundException fnf)
657     {
658       return null;
659     }
660     catch(IOException io)
661     {
662       Log.log(Log.ERROR,PluginJAR.class,io);
663       return null;
664     }
665     finally
666     {
667       try
668       {
669         if(din != null)
670           din.close();
671       }
672       catch(IOException io)
673       {
674         Log.log(Log.ERROR,PluginJAR.class,io);
675       }
676     }
677   } //}}}
678 
679   //{{{ setPluginCache() method
680   static void setPluginCache(PluginJAR plugin, PluginCacheEntry cache)
681   {
682     String jarCachePath = plugin.getCachePath();
683     if(jarCachePath == null)
684       return;
685 
686     Log.log(Log.DEBUG,PluginJAR.class,"Writing " + jarCachePath);
687 
688     DataOutputStream dout = null;
689     try
690     {
691       dout = new DataOutputStream(
692         new BufferedOutputStream(
693         new FileOutputStream(jarCachePath)));
694       cache.write(dout);
695       dout.close();
696     }
697     catch(IOException io)
698     {
699       Log.log(Log.ERROR,PluginJAR.class,io);
700       try
701       {
702         dout.close();
703       }
704       catch(IOException io2)
705       {
706         Log.log(Log.ERROR,PluginJAR.class,io2);
707       }
708       new File(jarCachePath).delete();
709     }
710   } //}}}
711 
712   //}}}
713 
714   //{{{ PluginJAR constructor
715   PluginJAR(File file)
716   {
717     this.path = file.getPath();
718     String jarCacheDir = jEdit.getJARCacheDirectory();
719     if(jarCacheDir != null)
720     {
721       cachePath = MiscUtilities.constructPath(
722         jarCacheDir,file.getName() + ".summary");
723     }
724     this.file = file;
725     classLoader = new JARClassLoader(this);
726     actions = new ActionSet();
727   } //}}}
728 
729   //{{{ init() method
730   void init()
731   {
732     boolean initialized = false;
733 
734     PluginCacheEntry cache = getPluginCache(this);
735     if(cache != null)
736     {
737       loadCache(cache);
738       classLoader.activate();
739       initialized = true;
740     }
741     else
742     {
743       try
744       {
745         cache = generateCache();
746         if(cache != null)
747         {
748           setPluginCache(this,cache);
749           classLoader.activate();
750           initialized = true;
751         }
752       }
753       catch(IOException io)
754       {
755         Log.log(Log.ERROR,this,"Cannot load"
756           + " plugin " + path);
757         Log.log(Log.ERROR,this,io);
758 
759         String[] args = { io.toString() };
760         jEdit.pluginError(path,"plugin-error.load-error",args);
761 
762         uninit(false);
763       }
764     }
765   } //}}}
766 
767   //{{{ uninit() method
768   void uninit(boolean exit)
769   {
770     deactivatePlugin(exit);
771 
772     if(!exit)
773     {
774       Iterator iter = weRequireThese.iterator();
775       while(iter.hasNext())
776       {
777         String path = (String)iter.next();
778         PluginJAR jar = jEdit.getPluginJAR(path);
779         if(jar != null)
780           jar.theseRequireMe.remove(this.path);
781       }
782 
783       classLoader.deactivate();
784       BeanShell.resetClassManager();
785 
786       if(actions != null)
787         jEdit.getActionContext().removeActionSet(actions);
788       if(browserActions != null)
789         VFSBrowser.getActionContext().removeActionSet(browserActions);
790 
791       DockableWindowManager.unloadDockableWindows(this);
792       ServiceManager.unloadServices(this);
793 
794       try
795       {
796         if(zipFile != null)
797         {
798           zipFile.close();
799           zipFile = null;
800         }
801       }
802       catch(IOException io)
803       {
804         Log.log(Log.ERROR,this,io);
805       }
806     }
807   } //}}}
808 
809   //{{{ getClasses() method
810   String[] getClasses()
811   {
812     return classes;
813   } //}}}
814 
815   //}}}
816 
817   //{{{ Private members
818 
819   //{{{ Instance variables
820   private String path;
821   private String cachePath;
822   private File file;
823 
824   private JARClassLoader classLoader;
825   private ZipFile zipFile;
826   private String[] classes;
827   private ActionSet actions;
828   private ActionSet browserActions;
829 
830   private EditPlugin plugin;
831 
832   private URL dockablesURI;
833   private URL servicesURI;
834 
835   private boolean activated;
836 
837   private List theseRequireMe = new LinkedList();
838   private List weRequireThese = new LinkedList();
839   //}}}
840 
841   //{{{ loadCache() method
842   private void loadCache(PluginCacheEntry cache)
843   {
844     classes = cache.classes;
845 
846     /* this should be before dockables are initialized */
847     if(cache.cachedProperties != null)
848       jEdit.addProperties(cache.cachedProperties);
849 
850     if(cache.actionsURI != null
851       && cache.cachedActionNames != null)
852     {
853       actions = new ActionSet(this,
854         cache.cachedActionNames,
855         cache.cachedActionToggleFlags,
856         cache.actionsURI);
857     }
858 
859     if(cache.browserActionsURI != null
860       && cache.cachedBrowserActionNames != null)
861     {
862       browserActions = new ActionSet(this,
863         cache.cachedBrowserActionNames,
864         cache.cachedBrowserActionToggleFlags,
865         cache.browserActionsURI);
866       VFSBrowser.getActionContext().addActionSet(browserActions);
867     }
868 
869     if(cache.dockablesURI != null
870       && cache.cachedDockableNames != null
871       && cache.cachedDockableActionFlags != null)
872     {
873       dockablesURI = cache.dockablesURI;
874       DockableWindowManager.cacheDockableWindows(this,
875         cache.cachedDockableNames,
876         cache.cachedDockableActionFlags);
877     }
878 
879     if(actions.size() != 0)
880       jEdit.addActionSet(actions);
881 
882     if(cache.servicesURI != null
883       && cache.cachedServices != null)
884     {
885       servicesURI = cache.servicesURI;
886       for(int i = 0; i < cache.cachedServices.length;
887         i++)
888       {
889         ServiceManager.Descriptor d
890           = cache.cachedServices[i];
891         ServiceManager.registerService(d);
892       }
893     }
894 
895     if(cache.pluginClass != null)
896     {
897       // Check if a plugin with the same name
898       // is already loaded
899       if(jEdit.getPlugin(cache.pluginClass) != null)
900       {
901         jEdit.pluginError(path,
902           "plugin-error.already-loaded",
903           null);
904         uninit(false);
905       }
906       else
907       {
908         String label = jEdit.getProperty(
909           "plugin." + cache.pluginClass
910           + ".name");
911         actions.setLabel(jEdit.getProperty(
912           "action-set.plugin",
913           new String[] { label }));
914         plugin = new EditPlugin.Deferred(
915           cache.pluginClass);
916         plugin.jar = (EditPlugin.JAR)this;
917       }
918     }
919     else
920     {
921       if(actions.size() != 0)
922       {
923         Log.log(Log.WARNING,this,getPath() + " has an actions.xml but no plugin core class");
924         actions.setLabel("MISSING PLUGIN CORE CLASS");
925       }
926     }
927   } //}}}
928 
929   //{{{ generateCache() method
930   private PluginCacheEntry generateCache() throws IOException
931   {
932     Properties properties = new Properties();
933 
934     LinkedList classes = new LinkedList();
935 
936     ZipFile zipFile = getZipFile();
937 
938     List plugins = new LinkedList();
939 
940     PluginCacheEntry cache = new PluginCacheEntry();
941     cache.modTime = file.lastModified();
942     cache.cachedProperties = new HashMap();
943 
944     Enumeration entries = zipFile.entries();
945     while(entries.hasMoreElements())
946     {
947       ZipEntry entry = (ZipEntry)
948         entries.nextElement();
949       String name = entry.getName();
950       String lname = name.toLowerCase();
951       if(lname.equals("actions.xml"))
952       {
953         cache.actionsURI = classLoader.getResource(name);
954       }
955       else if(lname.equals("browser.actions.xml"))
956       {
957         cache.browserActionsURI = classLoader.getResource(name);
958       }
959       else if(lname.equals("dockables.xml"))
960       {
961         dockablesURI = classLoader.getResource(name);
962         cache.dockablesURI = dockablesURI;
963       }
964       else if(lname.equals("services.xml"))
965       {
966         servicesURI = classLoader.getResource(name);
967         cache.servicesURI = servicesURI;
968       }
969       else if(lname.endsWith(".props"))
970       {
971         InputStream in = classLoader.getResourceAsStream(name);
972         properties.load(in);
973         in.close();
974       }
975       else if(name.endsWith(".class"))
976       {
977         String className = MiscUtilities
978           .fileToClass(name);
979         if(className.endsWith("Plugin"))
980         {
981           plugins.add(className);
982         }
983         classes.add(className);
984       }
985     }
986 
987     cache.cachedProperties = properties;
988     jEdit.addProperties(properties);
989 
990     this.classes = cache.classes =
991       (String[])classes.toArray(
992       new String[classes.size()]);
993 
994     String label = null;
995 
996     Iterator iter = plugins.iterator();
997     while(iter.hasNext())
998     {
999       String className = (String)iter.next();
1000
1001      String _label = jEdit.getProperty("plugin."
1002        + className + ".name");
1003      String version = jEdit.getProperty("plugin."
1004        + className + ".version");
1005      if(_label == null || version == null)
1006      {
1007        Log.log(Log.WARNING,this,"Ignoring: "
1008          + className);
1009      }
1010      else
1011      {
1012        cache.pluginClass = className;
1013
1014        // Check if a plugin with the same name
1015        // is already loaded
1016        if(jEdit.getPlugin(className) != null)
1017        {
1018          jEdit.pluginError(path,
1019            "plugin-error.already-loaded",
1020            null);
1021          return null;
1022        }
1023        else
1024        {
1025          plugin = new EditPlugin.Deferred(
1026            className);
1027          plugin.jar = (EditPlugin.JAR)this;
1028          label = _label;
1029        }
1030
1031        break;
1032      }
1033    }
1034
1035    if(cache.actionsURI != null)
1036    {
1037      actions = new ActionSet(this,null,null,
1038        cache.actionsURI);
1039      actions.load();
1040      cache.cachedActionNames =
1041        actions.getCacheableActionNames();
1042      cache.cachedActionToggleFlags = new boolean[
1043        cache.cachedActionNames.length];
1044      for(int i = 0; i < cache.cachedActionNames.length; i++)
1045      {
1046         cache.cachedActionToggleFlags[i]
1047           = jEdit.getBooleanProperty(
1048          cache.cachedActionNames[i]
1049          + ".toggle");
1050      }
1051    }
1052
1053    if(cache.browserActionsURI != null)
1054    {
1055      browserActions = new ActionSet(this,null,null,
1056        cache.browserActionsURI);
1057      browserActions.load();
1058      VFSBrowser.getActionContext().addActionSet(browserActions);
1059      cache.cachedBrowserActionNames =
1060        browserActions.getCacheableActionNames();
1061      cache.cachedBrowserActionToggleFlags = new boolean[
1062        cache.cachedBrowserActionNames.length];
1063      for(int i = 0;
1064        i < cache.cachedBrowserActionNames.length;
1065        i++)
1066      {
1067         cache.cachedBrowserActionToggleFlags[i]
1068           = jEdit.getBooleanProperty(
1069          cache.cachedBrowserActionNames[i]
1070          + ".toggle");
1071      }
1072    }
1073
1074    if(dockablesURI != null)
1075    {
1076      DockableWindowManager.loadDockableWindows(this,
1077        dockablesURI,cache);
1078    }
1079
1080    if(label != null)
1081    {
1082      actions.setLabel(jEdit.getProperty(
1083        "action-set.plugin",
1084        new String[] { label }));
1085    }
1086
1087    if(actions.size() != 0)
1088      jEdit.addActionSet(actions);
1089
1090    if(servicesURI != null)
1091    {
1092      ServiceManager.loadServices(this,servicesURI,cache);
1093    }
1094
1095    return cache;
1096  } //}}}
1097
1098  //{{{ startPlugin() method
1099  private void startPlugin()
1100  {
1101    try
1102    {
1103      plugin.start();
1104    }
1105    catch(Throwable t)
1106    {
1107      breakPlugin();
1108
1109      Log.log(Log.ERROR,PluginJAR.this,
1110        "Error while starting plugin " + plugin.getClassName());
1111      Log.log(Log.ERROR,PluginJAR.this,t);
1112      String[] args = { t.toString() };
1113      jEdit.pluginError(path,"plugin-error.start-error",args);
1114    }
1115
1116    if(plugin instanceof EBPlugin)
1117    {
1118      if(jEdit.getProperty("plugin."
1119        + plugin.getClassName() + ".activate")
1120        == null)
1121      {
1122        // old plugins expected jEdit 4.1-style
1123        // behavior, where a PropertiesChanged
1124        // was sent after plugins were started
1125        ((EBComponent)plugin).handleMessage(
1126          new org.gjt.sp.jedit.msg.PropertiesChanged(null));
1127      }
1128      EditBus.addToBus((EBPlugin)plugin);
1129    }
1130
1131    // buffers retain a reference to the fold handler in
1132    // question... and the easiest way to handle fold
1133    // handler loading is this...
1134    Buffer buffer = jEdit.getFirstBuffer();
1135    while(buffer != null)
1136    {
1137      FoldHandler handler =
1138        FoldHandler.getFoldHandler(
1139        buffer.getStringProperty("folding"));
1140      // == null before loaded
1141      if(buffer.getFoldHandler() != null
1142        && handler != null
1143        && handler != buffer.getFoldHandler())
1144      {
1145        buffer.setFoldHandler(handler);
1146      }
1147      buffer = buffer.getNext();
1148    }
1149  } //}}}
1150
1151  //{{{ startPluginLater() method
1152  private void startPluginLater()
1153  {
1154    SwingUtilities.invokeLater(new Runnable()
1155    {
1156      public void run()
1157      {
1158        if(!activated)
1159          return;
1160
1161        startPlugin();
1162      }
1163    });
1164  } //}}}
1165
1166  //{{{ breakPlugin() method
1167  private void breakPlugin()
1168  {
1169    plugin = new EditPlugin.Broken(plugin.getClassName());
1170    plugin.jar = (EditPlugin.JAR)this;
1171
1172    // remove action sets, dockables, etc so that user doesn't
1173    // see the broken plugin
1174    uninit(false);
1175  } //}}}
1176
1177  //}}}
1178
1179  //{{{ PluginCacheEntry class
1180  /**
1181   * Used by the <code>DockableWindowManager</code> and
1182   * <code>ServiceManager</code> to handle caching.
1183   * @since jEdit 4.2pre1
1184   */
1185  public static class PluginCacheEntry
1186  {
1187    public static final int MAGIC = 0xB7A2E420;
1188
1189    //{{{ Instance variables
1190    public PluginJAR plugin;
1191    public long modTime;
1192
1193    public String[] classes;
1194    public URL actionsURI;
1195    public String[] cachedActionNames;
1196    public boolean[] cachedActionToggleFlags;
1197    public URL browserActionsURI;
1198    public String[] cachedBrowserActionNames;
1199    public boolean[] cachedBrowserActionToggleFlags;
1200    public URL dockablesURI;
1201    public String[] cachedDockableNames;
1202    public boolean[] cachedDockableActionFlags;
1203    public URL servicesURI;
1204    public ServiceManager.Descriptor[] cachedServices;
1205
1206    public Map cachedProperties;
1207    public String pluginClass;
1208    //}}}
1209
1210    /* read() and write() must be kept perfectly in sync...
1211     * its a very simple file format. doing it this way is
1212     * faster than serializing since serialization calls
1213     * reflection, etc. */
1214
1215    //{{{ read() method
1216    public boolean read(DataInputStream din) throws IOException
1217    {
1218      int cacheMagic = din.readInt();
1219      if(cacheMagic != MAGIC)
1220        return false;
1221
1222      String cacheBuild = readString(din);
1223      if(!cacheBuild.equals(jEdit.getBuild()))
1224        return false;
1225
1226      long cacheModTime = din.readLong();
1227      if(cacheModTime != modTime)
1228        return false;
1229
1230      actionsURI = readURI(din);
1231      cachedActionNames = readStringArray(din);
1232      cachedActionToggleFlags = readBooleanArray(din);
1233
1234      browserActionsURI = readURI(din);
1235      cachedBrowserActionNames = readStringArray(din);
1236      cachedBrowserActionToggleFlags = readBooleanArray(din);
1237
1238      dockablesURI = readURI(din);
1239      cachedDockableNames = readStringArray(din);
1240      cachedDockableActionFlags = readBooleanArray(din);
1241
1242      servicesURI = readURI(din);
1243      int len = din.readInt();
1244      if(len == 0)
1245        cachedServices = null;
1246      else
1247      {
1248        cachedServices = new ServiceManager.Descriptor[len];
1249        for(int i = 0; i < len; i++)
1250        {
1251          ServiceManager.Descriptor d = new
1252            ServiceManager.Descriptor(
1253            readString(din),
1254            readString(din),
1255            null,
1256            plugin);
1257          cachedServices[i] = d;
1258        }
1259      }
1260
1261      classes = readStringArray(din);
1262
1263      cachedProperties = readMap(din);
1264
1265      pluginClass = readString(din);
1266
1267      return true;
1268    } //}}}
1269
1270    //{{{ write() method
1271    public void write(DataOutputStream dout) throws IOException
1272    {
1273      dout.writeInt(MAGIC);
1274      writeString(dout,jEdit.getBuild());
1275
1276      dout.writeLong(modTime);
1277
1278      writeString(dout,actionsURI);
1279      writeStringArray(dout,cachedActionNames);
1280      writeBooleanArray(dout,cachedActionToggleFlags);
1281
1282      writeString(dout,browserActionsURI);
1283      writeStringArray(dout,cachedBrowserActionNames);
1284      writeBooleanArray(dout,cachedBrowserActionToggleFlags);
1285
1286      writeString(dout,dockablesURI);
1287      writeStringArray(dout,cachedDockableNames);
1288      writeBooleanArray(dout,cachedDockableActionFlags);
1289
1290      writeString(dout,servicesURI);
1291      if(cachedServices == null)
1292        dout.writeInt(0);
1293      else
1294      {
1295        dout.writeInt(cachedServices.length);
1296        for(int i = 0; i < cachedServices.length; i++)
1297        {
1298          writeString(dout,cachedServices[i].clazz);
1299          writeString(dout,cachedServices[i].name);
1300        }
1301      }
1302
1303      writeStringArray(dout,classes);
1304
1305      writeMap(dout,cachedProperties);
1306
1307      writeString(dout,pluginClass);
1308    } //}}}
1309
1310    //{{{ Private members
1311
1312    //{{{ readString() method
1313    private String readString(DataInputStream din)
1314      throws IOException
1315    {
1316      int len = din.readInt();
1317      if(len == 0)
1318        return null;
1319      char[] str = new char[len];
1320      for(int i = 0; i < len; i++)
1321        str[i] = din.readChar();
1322      return new String(str);
1323    } //}}}
1324
1325    //{{{ readURI() method
1326    private URL readURI(DataInputStream din)
1327      throws IOException
1328    {
1329      String str = readString(din);
1330      if(str == null)
1331        return null;
1332      else
1333        return new URL(str);
1334    } //}}}
1335
1336    //{{{ readStringArray() method
1337    private String[] readStringArray(DataInputStream din)
1338      throws IOException
1339    {
1340      int len = din.readInt();
1341      if(len == 0)
1342        return null;
1343      String[] str = new String[len];
1344      for(int i = 0; i < len; i++)
1345      {
1346        str[i] = readString(din);
1347      }
1348      return str;
1349    } //}}}
1350
1351    //{{{ readBooleanArray() method
1352    private boolean[] readBooleanArray(DataInputStream din)
1353      throws IOException
1354    {
1355      int len = din.readInt();
1356      if(len == 0)
1357        return null;
1358      boolean[] bools = new boolean[len];
1359      for(int i = 0; i < len; i++)
1360      {
1361        bools[i] = din.readBoolean();
1362      }
1363      return bools;
1364    } //}}}
1365
1366    //{{{ readMap() method
1367    private Map readMap(DataInputStream din) throws IOException
1368    {
1369      HashMap returnValue = new HashMap();
1370      int count = din.readInt();
1371      for(int i = 0; i < count; i++)
1372      {
1373        String key = readString(din);
1374        String value = readString(din);
1375        if(value == null)
1376          value = "";
1377        returnValue.put(key,value);
1378      }
1379      return returnValue;
1380    } //}}}
1381
1382    //{{{ writeString() method
1383    private void writeString(DataOutputStream dout,
1384      Object obj) throws IOException
1385    {
1386      if(obj == null)
1387      {
1388        dout.writeInt(0);
1389      }
1390      else
1391      {
1392        String str = obj.toString();
1393        dout.writeInt(str.length());
1394        dout.writeChars(str);
1395      }
1396    } //}}}
1397
1398    //{{{ writeStringArray() method
1399    private void writeStringArray(DataOutputStream dout,
1400      String[] str) throws IOException
1401    {
1402      if(str == null)
1403      {
1404        dout.writeInt(0);
1405      }
1406      else
1407      {
1408        dout.writeInt(str.length);
1409        for(int i = 0; i < str.length; i++)
1410        {
1411          writeString(dout,str[i]);
1412        }
1413      }
1414    } //}}}
1415
1416    //{{{ writeBooleanArray() method
1417    private void writeBooleanArray(DataOutputStream dout,
1418      boolean[] bools) throws IOException
1419    {
1420      if(bools == null)
1421      {
1422        dout.writeInt(0);
1423      }
1424      else
1425      {
1426        dout.writeInt(bools.length);
1427        for(int i = 0; i < bools.length; i++)
1428        {
1429          dout.writeBoolean(bools[i]);
1430        }
1431      }
1432    } //}}}
1433
1434    //{{{ writeMap() method
1435    private void writeMap(DataOutputStream dout, Map map)
1436      throws IOException
1437    {
1438      dout.writeInt(map.size());
1439      Iterator iter = map.keySet().iterator();
1440      while(iter.hasNext())
1441      {
1442        String key = (String)iter.next();
1443        writeString(dout,key);
1444        writeString(dout,map.get(key));
1445      }
1446    } //}}}
1447
1448    //}}}
1449  } //}}}
1450}