1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20 package org.xnap.plugin;
21
22 import java.io.BufferedReader;
23 import java.io.BufferedWriter;
24 import java.io.File;
25 import java.io.FileReader;
26 import java.io.FileWriter;
27 import java.io.IOException;
28 import java.util.Collections;
29 import java.util.Comparator;
30 import java.util.Hashtable;
31 import java.util.Iterator;
32 import java.util.LinkedList;
33 import java.util.List;
34 import java.util.Vector;
35 import java.util.Properties;
36
37 import org.apache.log4j.Logger;
38 import org.xnap.XNap;
39 import org.xnap.gui.SplashWindow;
40 import org.xnap.gui.XNapFrame;
41 import org.xnap.loader.*;
42 import org.xnap.pkg.*;
43 import org.xnap.pkg.PackageInfoReader;
44 import org.xnap.pkg.PackageInfoWriter;
45 import org.xnap.pkg.ParseException;
46 import org.xnap.pkg.UnsatisfiedDependenciesException;
47 import org.xnap.pkg.XNapPackageManager;
48 import org.xnap.event.PluginListener;
49 import org.xnap.util.FileHelper;
50 import org.xnap.util.Preferences;
51 import org.xnap.util.UncaughtExceptionManager;
52 import org.xnap.util.VersionParser;
53
54 /***
55 * This class keeps track of all available and enabled plugins.
56 */
57 public class PluginManager
58 {
59
60
61
62 public static final String PLUGIN_FILENAME
63 = FileHelper.getHomeDir() + "available_plugins";
64
65
66
67 private static Logger logger = Logger.getLogger(PluginManager.class);
68 private static Preferences prefs = Preferences.getInstance();
69 private static PluginManager singleton = new PluginManager();
70
71 private Hashtable infoByName = new Hashtable();
72
73 private List listeners = new Vector();
74
75
76
77 /***
78 * Discovers available plugins. For each plugin that has satisfied
79 * dependencies a {@link PluginInfo} object is created and added to the
80 * plugin manager.
81 */
82 private PluginManager()
83 {
84 if (XNap.clearPackageInfos
85 || Preferences.getInstance().getUpdatePluginsOnStartup()) {
86
87 updateFromPackageManager();
88 }
89
90 File file = new File(PLUGIN_FILENAME);
91 if (!XNap.clearPackageInfos) {
92
93 if (file.exists()) {
94 try {
95 read(file);
96 }
97 catch (IOException e) {
98 logger.info("Error reading list of available plugins", e);
99 }
100 }
101 else {
102 updateFromPackageManager();
103 }
104 }
105
106 removeBaseConflicts();
107 }
108
109
110
111 public static PluginManager getInstance()
112 {
113 return singleton;
114 }
115
116 /***
117 * Constructs a {@link PluginInfo} object from <code>props</code>
118 * and invokes {@link #add(PluginInfo)} if info is valid.
119 *
120 * @see #PluginManager()
121 */
122 public PluginInfo add(Properties props)
123 {
124 PluginInfo info = new PluginInfo(props);
125 if (info.isValid() && add(info)) {
126 return info;
127 }
128 return null;
129 }
130
131 /***
132 * Adds a new plugin information object to the list of available
133 * plugins.
134 *
135 * <p>If the plugin was already known and <code>info</code>
136 * has a lower version or the plugin was already loaded,
137 * <code>info</code> is not added.
138 *
139 * @return true, if <code>info</code> was added; false, otherwise
140 */
141 public boolean add(PluginInfo info)
142 {
143 if (!info.areRequirementsSatisfied()) {
144 return false;
145 }
146
147 PluginInfo existing = getInfoByName(info.getName());
148 if (existing != null) {
149 if (!existing.isLoaded()
150 && existing.isAvailable()
151 && VersionParser.compare(info.getVersion(),
152 existing.getVersion()) > 0) {
153
154
155 infoByName.put(info.getName(), info);
156 return true;
157 }
158 }
159 else {
160
161 infoByName.put(info.getName(), info);
162 firePluginInfoAdded(info);
163 return true;
164 }
165
166 return false;
167 }
168
169 /***
170 * Adds a plugin from package info. The classpath is resolved to
171 * absolute jar file dependencies.
172 *
173 * @return true, if the plugin was added successfully
174 * @see #add(PluginInfo)
175 */
176 public boolean addFromPackage(PackageInfo p)
177 throws ParseException, UnsatisfiedDependenciesException
178 {
179 PluginInfo info = new PluginInfo(p.getProperties());
180 info.setClassPath(PluginManager.resolveClassPath(info));
181 return add(info);
182 }
183
184 /***
185 * Adds the specified plugin listener to receive plugin events.
186 */
187 public void addPluginListener(PluginListener l)
188 {
189 listeners.add(l);
190 }
191
192 /***
193 * Removes the specified plugin listener.
194 */
195 public void removePluginListener(PluginListener l)
196 {
197 listeners.remove(l);
198 }
199
200 /***
201 * Returns the xnap-core package.
202 *
203 * @return null, if package is not found
204 */
205 public PluginInfo getCorePackage()
206 {
207 return getInfoByName("XNap");
208 }
209
210 public static String getCoreVersion()
211 {
212 PluginInfo info = PluginManager.getInstance().getCorePackage();
213 return (info != null) ? info.getVersion() : XNapLoader.VERSION;
214 }
215
216 /***
217 * Returns the number of enabled plugins.
218 */
219 public int getEnabledCount()
220 {
221 int count = 0;
222 for (Iterator i = infos(); i.hasNext();) {
223 PluginInfo info = (PluginInfo)i.next();
224 if (info.isEnabled()) {
225 count++;
226 }
227 }
228 return count;
229 }
230
231 /***
232 * Invoked by {@link org.xnap.gui.XNapFrame}.
233 */
234 public void guiStarted()
235 {
236 int count = getEnabledCount();
237 int increaseBy = (count > 0)
238 ? (95 - SplashWindow.getProgress()) / count
239 : 0;
240
241 for (Iterator i = infos(); i.hasNext();) {
242 PluginInfo info = (PluginInfo)i.next();
243 if (info.isEnabled()) {
244 SplashWindow.incProgress
245 (increaseBy, XNap.tr("Setting up {0}", info.getName()));
246 try {
247 info.getPlugin().startGUI();
248 }
249 catch(Throwable t) {
250 logger.error("could not start plugin gui", t);
251 UncaughtExceptionManager.getInstance().notify(t);
252 }
253 }
254 }
255 }
256
257 /***
258 * Invoked by {@link org.xnap.gui.XNapFrame}.
259 */
260 public void guiStopped()
261 {
262 for (Iterator i = infos(); i.hasNext();) {
263 PluginInfo info = (PluginInfo)i.next();
264 if (info.isEnabled()) {
265 try {
266 info.getPlugin().stopGUI();
267 }
268 catch(Throwable t) {
269 logger.error("could not stop plugin gui", t);
270 UncaughtExceptionManager.getInstance().notify(t);
271 }
272 }
273 }
274 }
275
276 /***
277 * Returns the information record for plugin with name.
278 *
279 * @param name the plugin's name
280 */
281 public PluginInfo getInfoByName(String name)
282 {
283 return (PluginInfo)infoByName.get(name);
284 }
285
286 /***
287 * Return a sorted iterator over a copy of all {@link PluginInfo} objects.
288 * Changes made to the iterator are not reflected.
289 */
290 public Iterator infos()
291 {
292 LinkedList list = new LinkedList(infoByName.values());
293 Collections.sort(list, new PluginInfoComparator());
294 return list.iterator();
295 }
296
297 /***
298 * Loads the plugin described by <code>info</code>. All required jar
299 * files are added to the class loader and the plugin class is
300 * instantiated.
301 *
302 * @param info the plugin to be loaded
303 * @return the instantiated plugin object
304 */
305 public Plugin load(PluginInfo info) throws Exception
306 {
307 if (!info.isLoaded()) {
308 ClassLoader loader = getClass().getClassLoader();
309 if (loader == null) {
310 loader = XNapClassLoader.getInstance();
311 }
312
313 XNapClassLoader.getInstance().add(info.getClassPath());
314
315 Class c = loader.loadClass(info.getClassName());
316 Plugin p = (Plugin)c.newInstance();
317
318 info.setPlugin(p);
319 p.setInfo(info);
320
321 return p;
322 }
323 else {
324 return info.getPlugin();
325 }
326 }
327
328 /***
329 * Reads information about installed plugins from file.
330 *
331 * @param alreadyLoaded if true, the plugin is already installed in
332 * the classpath
333 */
334 private void read(File file) throws IOException
335 {
336 BufferedReader in = null;
337 try {
338 in = new BufferedReader(new FileReader(file));
339 Properties p;
340 while ((p = PackageInfoReader.readNext(in)) != null) {
341 add(p);
342 }
343 }
344 finally {
345 if (in != null) {
346 try {
347 in.close();
348 }
349 catch (IOException e) {
350 }
351 }
352 }
353 }
354
355 /***
356 * Removes all plugins that conflict with the installed base
357 * packages. Currently only xnap-core is considered to be part of
358 * base.
359 */
360 private void removeBaseConflicts()
361 {
362
363 try {
364 PluginInfo baseInfo = getCorePackage();
365 if (baseInfo == null) {
366 logger.warn("Required base package XNap not found!");
367 return;
368 }
369
370 PackageInfo[] conflicts
371 = XNapPackageManager.getInstance().getConflicts(baseInfo);
372 if (conflicts != null) {
373 for (int i = 0; i < conflicts.length; i++) {
374 PluginInfo info = getInfoByName(conflicts[i].getName());
375 if (info != null && info.equals(conflicts[i])) {
376
377 remove(info);
378 }
379 }
380 }
381 }
382 catch (ParseException e) {
383 logger.warn("Error parsing base conflicts");
384 }
385 }
386
387 /***
388 * Restores the state of the plugin manager from the {@link Preferences}.
389 */
390 public void restore()
391 {
392 String[] names = prefs.getEnabledPlugins();
393 for (int i = 0; i < names.length; i++) {
394 PluginInfo info = getInfoByName(names[i]);
395 if (info != null) {
396 info.setEnableOnStartup(true);
397 try {
398 SplashWindow.incProgress(2, XNap.tr("Loading {0} plugin",
399 info.getName()));
400
401 Plugin p = info.getPlugin();
402 if (p == null) {
403 p = load(info);
404 }
405
406 setEnabled(p, true);
407 }
408 catch (PluginInitializeException e) {
409 logger.error("could not restore plugin " + names[i], e);
410 }
411 catch (ClassNotFoundException e) {
412 logger.error("could not restore plugin " + names[i], e);
413 }
414 catch (Throwable t) {
415 logger.error("could not restore plugin " + names[i], t);
416 UncaughtExceptionManager.getInstance().notify(t);
417 }
418 }
419 else {
420 logger.error("plugin not found " + names[i]);
421 }
422 }
423 }
424
425 /***
426 * Disables all plugins and saves the state of the PluginManager.
427 */
428 public void save()
429 {
430 List enabledPlugins = new LinkedList();
431
432 for (Iterator i = infos(); i.hasNext();) {
433 PluginInfo info = (PluginInfo)i.next();
434 if (info.isEnabled()) {
435 try {
436 setEnabled(info.getPlugin(), false, true);
437 if (info.getEnableOnStartup()) {
438 enabledPlugins.add(info.getName());
439 }
440 }
441 catch(Throwable t) {
442 logger.error("could not disable plugin", t);
443 UncaughtExceptionManager.getInstance().notify(t);
444 }
445 }
446 }
447
448
449 prefs.setEnabledPlugins((String[])enabledPlugins.toArray(new String[0]));
450 }
451
452 /***
453 * Enables or disables <code>plugin</code>
454 */
455 private void setEnabled(Plugin plugin, boolean enable, boolean force)
456 throws Exception
457 {
458 if (enable) {
459 if (!plugin.getInfo().isEnabled()) {
460 plugin.start();
461 if (XNapFrame.getInstance() != null) {
462 plugin.startGUI();
463 }
464 plugin.getInfo().setEnabled(true);
465 firePluginEnabled(plugin);
466 }
467 }
468 else {
469 if (!plugin.getInfo().canDisable() && !force) {
470 throw new Exception(XNap.tr("Can not disable plugin. Restart XNap to disable plugin."));
471 }
472
473 if (plugin.getInfo().isEnabled()) {
474 if (XNapFrame.getInstance() != null) {
475 plugin.stopGUI();
476 }
477 plugin.stop();
478 plugin.getInfo().setEnabled(false);
479 firePluginDisabled(plugin);
480 }
481 }
482 }
483
484 public void setEnabled(Plugin plugin, boolean enable)
485 throws Exception
486 {
487 setEnabled(plugin, enable, false);
488 }
489
490 /***
491 * Returns the number of plugins.
492 */
493 public int size()
494 {
495 return infoByName.values().size();
496 }
497
498 public void remove(PluginInfo info)
499 {
500 infoByName.remove(info.getName());
501 firePluginInfoRemoved(info);
502 }
503
504 /***
505 * Resolves the class path of info to absolute filenames.
506 */
507 public static String[] resolveClassPath(PluginInfo info)
508 throws ParseException, UnsatisfiedDependenciesException
509 {
510 List list = new LinkedList();
511 PackageInfo[] depends
512 = XNapPackageManager.getInstance().getDependencies(info);
513 for (int j = 0; j < depends.length; j++) {
514 String classPath[] = depends[j].getClassPath();
515 for (int i = 0; i < classPath.length; i++) {
516 File f = depends[j].getFile(classPath[i].trim());
517 list.add(f.getAbsolutePath());
518 }
519 }
520 return (String[])list.toArray(new String[0]);
521 }
522
523 /***
524 * Updates the list of available plugins from {@link
525 * XNapPackageManager}.
526 */
527 public void updateFromPackageManager()
528 {
529 XNapPackageManager.getInstance().initialize();
530 for (Iterator i = XNapPackageManager/getInstance()/packages()/package-summary.html">g> (Iterator i = XNapPackageManager.getInstance().packages();
531 i.hasNext();) {
532
533 PackageInfo p = (PackageInfo)i.next();
534 try {
535 if (p.isPlugin() || p.isBase()) {
536 addFromPackage(p);
537 }
538 }
539 catch (ParseException e) {
540 logger.debug("Could not resolve dependencies for "
541 + p.getPackage() + " " + p.getVersion()
542 + ": " + e.getMessage());
543 continue;
544 }
545 catch (UnsatisfiedDependenciesException e) {
546 logger.debug("Unsatisfied dependency for "
547 + p.getPackage() + " " + p.getVersion()
548 + ": " + e.getMessage());
549 continue;
550 }
551 }
552
553 write();
554 }
555
556 public void write()
557 {
558 try {
559 write(new File(PLUGIN_FILENAME));
560 }
561 catch (IOException e) {
562 logger.debug("Error writing list of available plugins", e);
563 }
564 }
565
566 private void write(File file) throws IOException
567 {
568 BufferedWriter out = new BufferedWriter(new FileWriter(file));
569 try {
570 for (Iterator i = infoByName.values().iterator(); i.hasNext();) {
571 PluginInfo info = (PluginInfo)i.next();
572 PackageInfoWriter.write(out, info.getProperties());
573 }
574 }
575 finally {
576 try {
577 out.close();
578 }
579 catch (IOException e) {
580 }
581 }
582 }
583
584 private void firePluginEnabled(Plugin plugin)
585 {
586 Object[] l = listeners.toArray();
587 if (l != null) {
588 for (int i = l.length - 1; i >= 0; i--) {
589 ((PluginListener)l[i]).pluginEnabled(plugin);
590 }
591 }
592 }
593
594 private void firePluginDisabled(Plugin plugin)
595 {
596 Object[] l = listeners.toArray();
597 if (l != null) {
598 for (int i = l.length - 1; i >= 0; i--) {
599 ((PluginListener)l[i]).pluginDisabled(plugin);
600 }
601 }
602 }
603
604
605 private void firePluginInfoAdded(PluginInfo info)
606 {
607 Object[] l = listeners.toArray();
608 if (l != null) {
609 for (int i = l.length - 1; i >= 0; i--) {
610 ((PluginListener)l[i]).pluginInfoAdded(info);
611 }
612 }
613 }
614
615 private void firePluginInfoRemoved(PluginInfo info)
616 {
617 Object[] l = listeners.toArray();
618 if (l != null) {
619 for (int i = l.length - 1; i >= 0; i--) {
620 ((PluginListener)l[i]).pluginInfoRemoved(info);
621 }
622 }
623 }
624
625
626
627 /***
628 * Compares {@link PluginInfo} objects by name.
629 */
630 public static class PluginInfoComparator implements Comparator
631 {
632
633 public int compare(Object o1, Object o2)
634 {
635 return ((PluginInfo)o1).getName().compareToIgnoreCase
636 (((PluginInfo)o2).getName());
637 }
638
639 }
640
641 }