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}