View Javadoc

1   /*
2    *  XNap - A P2P framework and client.
3    *
4    *  See the file AUTHORS for copyright information.
5    *
6    *  This program is free software; you can redistribute it and/or modify
7    *  it under the terms of the GNU General Public License as published by
8    *  the Free Software Foundation.
9    *
10   *  This program is distributed in the hope that it will be useful,
11   *  but WITHOUT ANY WARRANTY; without even the implied warranty of
12   *  MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
13   *  GNU General Public License for more details.
14   *
15   *  You should have received a copy of the GNU General Public License
16   *  along with this program; if not, write to the Free Software
17   *  Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA  02111-1307  USA
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      //--- Constant(s) ---
61  
62  	public static final String PLUGIN_FILENAME 
63  		= FileHelper.getHomeDir() + "available_plugins";
64  
65      //--- Data field(s) ---
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      //--- Constructor(s) ---
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  			// read control file from home directory
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     //--- Method(s) ---
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 				// replace by newer info
155 				infoByName.put(info.getName(), info);
156 				return true;
157 			}
158 		}
159 		else {
160 			// add new info
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 		// remove conflicting plugins
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 						// plugin conflicts with base
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 		// save the names of the successfully disabled plugins
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 		forg> (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 	//--- Inner Classes ---
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 }