View Javadoc
1   package com.github.celldynamics.quimp.plugin.engine;
2   
3   import java.io.File;
4   import java.io.FilenameFilter;
5   import java.io.IOException;
6   import java.lang.reflect.InvocationTargetException;
7   import java.net.MalformedURLException;
8   import java.net.URL;
9   import java.net.URLClassLoader;
10  import java.nio.file.Files;
11  import java.nio.file.Path;
12  import java.nio.file.Paths;
13  import java.util.ArrayList;
14  import java.util.Arrays;
15  import java.util.Collections;
16  import java.util.Enumeration;
17  import java.util.HashMap;
18  import java.util.Iterator;
19  import java.util.Map;
20  import java.util.jar.JarEntry;
21  import java.util.jar.JarFile;
22  
23  import org.slf4j.Logger;
24  import org.slf4j.LoggerFactory;
25  
26  import com.github.celldynamics.quimp.QuimP;
27  import com.github.celldynamics.quimp.plugin.IQuimpCorePlugin;
28  
29  /*
30   * //!>
31   * @startuml doc-files/PluginFactory_1_UML.png
32   * actor user 
33   * participant PluginFactory as PF
34   * participant Plugin as PL 
35   * == Create instance of PluginFactory == 
36   * user -> PF : //<<create>>// 
37   * activate PF 
38   * PF -> PF : init ""availPlugins"" 
39   * PF -> PF : scanDirectory() 
40   * activate PF 
41   * PF -> PF : discover qname getClassName
42   * PF -> PF : getPluginType()
43   * activate PF
44   * PF -> PL : //<<getPluginInstance>>//
45   * activate PL
46   * PF -> PL : getPluginType()
47   * PL --> PF : ""type""
48   * PF -> PL : getPluginVersion()
49   * PL --> PF : ""version""
50   * destroy PL
51   * PF -> PF : store at ""availPlugins""
52   * deactivate PF
53   * deactivate PF
54   * == Get names ==
55   * user -> PF : getPluginNames(type)
56   * loop ""availPlugins""
57   * PF -> PF : check ""type""
58   * end
59   * PF --> user : List
60   * == Get Instance ==
61   * user -> PF : getInstance(name)
62   * PF -> PF : find plugin
63   * PF -> PL : //<<getPluginInstance>>//
64   * activate PL
65   * PF --> user : ""instance""
66   * note left
67   * Only one instance of requested
68   * plugin is created.
69   * On next requests previous reference
70   * is returned
71   * endnote
72   * @enduml
73   * 
74   * @startuml doc-files/PluginFactory_2_UML.png
75   * partition PluginFactory(directory) {
76   * (*) --> if "plugin directory\n exists" then
77   * -->[true] init ""availPlugins""
78   * --> "scanDirectory()"
79   * -right-> (*)
80   * else 
81   * -->[false] "throw QuimpPluginException"
82   * --> (*)
83   * endif
84   * }
85   * @enduml
86   * 
87   * @startuml doc-files/PluginFactory_3_UML.png
88   * partition scanDirectory() { 
89   * (*) --> Get file \nfrom ""root""
90   * if "file contains\n**-quimp.jar**" then
91   * -->[true] Discover qualified name
92   * --> getPluginType()
93   * --> if Type valid\njar valid\nreadable then
94   * -->[true] Store at ""availPlugins""
95   * --> Get file \nfrom ""root""
96   * else
97   * -->[false] log error
98   * --> Get file \nfrom ""root""
99   * endif
100  * else
101  * -->[false] Get file \nfrom ""root""
102  * endif
103  * }
104  * @enduml
105  * 
106  * @startuml doc-files/PluginFactory_4_UML.png
107  * start 
108  * :call ""setup()"" from jar;
109  * if (valid plugin type?) then (true)
110  * :Return plugin type;
111  * stop
112  * else (false)
113  * :throw Exception;
114  * endif
115  * end
116  * @enduml
117  * 
118  * @startuml doc-files/PluginFactory_5_UML.png
119  * start
120  * :Load jar;
121  * :Create instance;
122  * end
123  * @enduml
124  * 
125  * @startuml doc-files/PluginFactory_6_UML.png
126  * start
127  * if (name is not empty) then (yes)
128  *   :Build qualified name\nfrom ""getClassName()"";
129  *   if (get plugin data from\n ""availPlugins"") then (null)
130  *     :log error; 
131  *     ->Return null;
132  *     stop
133  *   else
134  *     if (was plugin used\nbefore) then (yes)
135  *       :Restore instance;
136  *       ->Return instance;
137  *       stop
138  *     else
139  *        :""getPluginInstance"";
140  *        :Store instance;
141  *     endif   
142  *   endif
143  * else
144  *   stop  
145  * endif
146  * stop
147  * @enduml
148  * //!<
149  */
150 /**
151  * Plugin jar loader.
152  * 
153  * <p>Created object is connected with directory where plugins exist. This directory is scanned for
154  * jar
155  * files that meet given below naming conventions. Every file that meets naming convention is loaded
156  * and asked for method IQuimpPlugin.setup(). On success the plugin is registered in availPlugins
157  * database:
158  * 
159  * {@code <Name, <File, Type, ClassName>>}
160  * 
161  * <p>Where Name is name of plugin extracted form file name (see below required naming conventions),
162  * File is handle to file on disk, Type is type of plugin according to types defined in
163  * warwick.wsbc.plugin.IQuimpPlugin and ClassName is qualified name of class of plugin. The
164  * ClassName is extracted from the plugin jar file assuming that plugin class contains underscore _
165  * in its name. If more classes underscored is found in jar, only the first discovered is loaded.
166  * Thus the following conventions are required:
167  * <ol>
168  * <li>Plugin name must contain <b>-quimp</b> to be considered as plugin (see PATTERN field)
169  * <li>Class name in plugin must end with underscore to be considered as plugin main class
170  * </ol>
171  *
172  * <p>Simplified sequence diagrams are as follows: <br>
173  * <img src="doc-files/PluginFactory_1_UML.png"/><br>
174  * 
175  * <p>This class try to hide all exceptions that can be thrown during loading plugins from user. In
176  * general only when user pass wrong path to plugins directory exception is thrown. In all other
177  * cases class returns null pointers or empty lists. Error handling:
178  * <ol>
179  * <li>Given directory exists but there is no plugins inside
180  * <ol>
181  * <li>getPluginNames(int) returns empty list (length 0)
182  * </ol>
183  * <li>Given directory exists but plugins are corrupted - they fulfil naming criterion but they are
184  * not valid QuimP plugins
185  * <ol>
186  * <li>getInstance(final String) returns <tt>null</tt> when correct name is given. It means that
187  * plugin has
188  * been registered by scanDirectory() so it had correct name and supported
189  * wsbc.plugin.IQuimpPlugin.setup() method
190  * </ol>
191  * <li>Given directory does not exist
192  * <ol>
193  * <li>Constructor throws QuimpPluginException
194  * </ol>
195  * <li>User asked for unknown name in getInstance(final String)
196  * <ol>
197  * <li>getInstance(final String) return null
198  * </ol>
199  * </ol>
200  * Internally getPluginType(final File, final String) and getInstance(final String) throw exceptions
201  * around class loading and running methods from them. Additionally getPluginType(final File, final
202  * String) throws exception when unknown type is returned from valid plugin. These exceptions are
203  * caught preventing adding that plugin into availPlugins database (scanDirectory()) or hidden in
204  * getInstance that returns null in this case. All exceptions are masked besides scanDirectory()
205  * that can throw checked PluginException that must be handled by caller. It usually means that
206  * given plugin directory does not exist.
207  * 
208  * <p>Each jar is loaded only once on first request. Next requests get the same instance.
209  * 
210  * @author p.baniukiewicz
211  */
212 public class PluginFactory {
213 
214   /**
215    * The Constant LOGGER.
216    */
217   static final Logger LOGGER = LoggerFactory.getLogger(PluginFactory.class.getName());
218   /**
219    * Name pattern of plugins.
220    */
221   private static final String PATTERN = "-quimp";
222 
223   /**
224    * List of plugins found in initial directory path passed to constructor.
225    * 
226    * <p>Plugins are organized in list [name, [path, qname, type]] where:
227    * <ol>
228    * <li>name is the name of plugin extracted from plugin jar filename. Name is always encoded as
229    * Name - starts with capital letter
230    * <li>path is full path with jar filename
231    * <li>qname is qualified name of plugin class obtained from jar name
232    * <li>type is type of plugin read from IQuimpPlugin.setup() method
233    * </ol>
234    * 
235    * <p>This field is set by scanDirectory() method -> getPluginType()
236    */
237   private HashMap<String, PluginProperties> availPlugins;
238   private Path root;
239 
240   /**
241    * Accessor to internal database of loaded plugins.
242    * 
243    * @return Non-modifiable database of loaded plugins
244    */
245   public Map<String, PluginProperties> getRegisterdPlugins() {
246 
247     return Collections.unmodifiableMap(availPlugins);
248   }
249 
250   /**
251    * Build object connected to plugin directory.
252    * 
253    * <p>Can throw exception if there is no directory path. <br>
254    * <img src="doc-files/PluginFactory_2_UML.png"/><br>
255    * 
256    * @param path
257    * 
258    */
259   public PluginFactory(final Path path) {
260     if (QuimP.SUPER_DEBUG) {
261       getSystemClassPath();
262     }
263     LOGGER.debug("Attached " + path.toString());
264     availPlugins = new HashMap<String, PluginProperties>();
265     // check if dir exists
266     if (Files.notExists(path)) {
267       LOGGER.warn("Plugin directory can not be read");
268       root = Paths.get("/");
269     } else {
270       root = path;
271       scanDirectory();
272     }
273   }
274 
275   /**
276    * Scan path for files that match PATTERN name and end with .jar.
277    * 
278    * <p>Fill availPlugins field. Field name is filled as Name without dependency how original
279    * filename was written. It is converted to small letters and then first char is upper-case
280    * written. <br>
281    * <img src="doc-files/PluginFactory_3_UML.png"/><br>
282    * 
283    * @return table of files that fulfill criterion:
284    *         <ol>
285    *         <li>have extension
286    *         <li>extension is .jar or .JAR
287    *         <li>contain PATTERN in name
288    *         </ol>
289    *         If there is no plugins in directory it returns 0 length array
290    */
291   private File[] scanDirectory() {
292     File fi = new File(root.toString());
293     File[] listFiles = fi.listFiles(new FilenameFilter() {
294 
295       @Override
296       public boolean accept(File dir, final String name) {
297         String sname = name.toLowerCase();
298         if (sname.lastIndexOf('.') <= 0) {
299           return false; // no extension
300         }
301         int lastIndex = sname.lastIndexOf('.');
302         // get extension
303         String ext = sname.substring(lastIndex);
304         if (!ext.equals(".jar")) {
305           return false; // no jar extension
306         }
307         // now we have .jar file, check name pattern
308         if (sname.contains(PATTERN)) {
309           return true;
310         } else {
311           return false;
312         }
313       }
314     });
315     if (listFiles == null) {
316       return new File[0]; // but if yes return empty array
317     }
318     // decode names from listFiles and fill availPlugins names and paths
319     for (File f : listFiles) {
320       // build plugin name from file name
321       String filename = f.getName().toLowerCase();
322       int lastindex = filename.lastIndexOf(PATTERN);
323       // cut from beginning to -quimp
324       String pluginName = filename.substring(0, lastindex);
325       // change first letter to upper to match class-naming convention
326       pluginName = pluginName.substring(0, 1).toUpperCase() + pluginName.substring(1);
327       // check plugin type
328       try {
329         // ask for class names in jar
330         String cname = getClassName(f);
331         // make temporary instance
332         Object inst = getPluginInstance(f, cname);
333         // get type of path.classname plugin
334         int type = getPluginType(inst);
335         // get version of path.classname plugin
336         String ver = getPluginVersion(inst);
337         // create entry with classname and path
338         availPlugins.put(pluginName, new PluginProperties(f, cname, type, ver));
339         LOGGER.debug(
340                 "Registered plugin: " + pluginName + " " + availPlugins.get(pluginName).toString());
341         // catch any error in plugin services - plugin is not stored
342       } catch (ClassNotFoundException | NoSuchMethodException | SecurityException
343               | InstantiationException | IllegalAccessException | IllegalArgumentException
344               | InvocationTargetException | ClassCastException | IOException
345               | NoClassDefFoundError e) {
346         LOGGER.error("Type of plugin " + pluginName + " in jar: " + f.getPath()
347                 + " can not be obtained. Ignoring this plugin");
348         LOGGER.debug(e.getMessage(), e);
349       }
350 
351     }
352     return Arrays.copyOf(listFiles, listFiles.length);
353   }
354 
355   /**
356    * Extracts qualified name of classes in jar file. Class name must contain underscore.
357    * 
358    * @param pathToJar path to jar file
359    * @return Name of first discovered class with underscore
360    * @throws IOException When jar can not be opened
361    * @throws IllegalArgumentException when there is no classes in jar
362    * @see <a href=
363    *      "link">http://stackoverflow.com/questions/11016092/how-to-load-classes-at-runtime-from-a-folder-or-jar</a>
364    */
365   private String getClassName(File pathToJar) throws IOException {
366     ArrayList<String> names = new ArrayList<>(); // all discovered names
367     JarFile jarFile = new JarFile(pathToJar);
368     Enumeration<JarEntry> e = jarFile.entries();
369 
370     while (e.hasMoreElements()) {
371       JarEntry je = (JarEntry) e.nextElement();
372       String entryname = je.getName();
373       if (je.isDirectory() || !entryname.endsWith("_.class")) {
374         continue;
375       }
376       // -6 because of .class
377       String className = je.getName().substring(0, je.getName().length() - 6);
378       className = className.replace('/', '.');
379       names.add(className);
380       LOGGER.debug("In " + pathToJar.toString() + " found class " + entryname);
381     }
382     jarFile.close();
383     if (names.isEmpty()) {
384       throw new IllegalArgumentException("getClassName: There is no underscored classes in jar");
385     }
386     if (names.size() > 1) {
387       LOGGER.warn("More than one underscored class in jar " + pathToJar.toString()
388               + " Take first one " + names.get(0));
389     }
390     return names.get(0);
391   }
392 
393   /**
394    * Gets type of plugin.
395    * 
396    * <p>Calls IQuimpPlugin.setup() method from plugin. <br>
397    * <img src="doc-files/PluginFactory_4_UML.png"/><br>
398    * 
399    * @param instance Instance of plugin
400    * @return Codes of types from IQuimpPlugin
401    * @throws IllegalArgumentException When returned type is unknown
402    * @throws NoSuchMethodException wrong plugin
403    * @throws InvocationTargetException wrong plugin
404    * @see com.github.celldynamics.quimp.plugin.IQuimpCorePlugin
405    */
406   private int getPluginType(Object instance)
407           throws IllegalArgumentException, NoSuchMethodException, InvocationTargetException {
408 
409     int result = (int) ((IQuimpCorePlugin) instance).setup();
410     // decode returned result for plugin type
411     if ((result & IQuimpCorePlugin.DOES_SNAKES) == IQuimpCorePlugin.DOES_SNAKES) {
412       return IQuimpCorePlugin.DOES_SNAKES;
413     } else {
414       throw new IllegalArgumentException("Plugin returned unknown type");
415     }
416   }
417 
418   /**
419    * Gets version of plugin.
420    * 
421    * <p>Calls IQuimpPlugin.getVersion() method from plugin
422    * 
423    * @param instance Instance of plugin
424    * @return String representing version of plugin or null if plugin does not support versioning
425    * @throws NoSuchMethodException wrong plugin
426    * @throws InvocationTargetException wrong plugin
427    */
428   private String getPluginVersion(Object instance)
429           throws NoSuchMethodException, InvocationTargetException {
430     return ((IQuimpCorePlugin) instance).getVersion();
431   }
432 
433   /**
434    * Creates instance of plugin. <br>
435    * <img src="doc-files/PluginFactory_5_UML.png"/><br>
436    * 
437    * @param plugin plugin File handler to plugin
438    * @param className full class name
439    * @return className Formatted fully qualified class name
440    * @throws InstantiationException wrong plugin
441    * @throws IllegalAccessException wrong plugin
442    * @throws ClassNotFoundException wrong plugin
443    * @throws MalformedURLException wrong plugin
444    */
445   private Object getPluginInstance(final File plugin, final String className)
446           throws InstantiationException, IllegalAccessException, ClassNotFoundException,
447           MalformedURLException {
448     URL[] url = new URL[] { plugin.toURI().toURL() };
449     ClassLoader child = new URLClassLoader(url);
450     LOGGER.trace("Trying to load class: " + className + " from " + plugin.toString());
451     Class<?> classToLoad = Class.forName(className, true, child);
452     Object instance = classToLoad.newInstance();
453     return instance;
454   }
455 
456   /**
457    * Return list of plugins of given types.
458    * 
459    * @param type Type defined in com.github.celldynamics.plugin.IQuimpPlugin
460    * @return List of names of plugins of type type. If there is no plugins in directory (this type
461    *         or any) returned list has length 0
462    */
463   public ArrayList<String> getPluginNames(int type) {
464     ArrayList<String> ret = new ArrayList<String>();
465     // Iterate over our collection
466     Iterator<Map.Entry<String, PluginProperties>> it = availPlugins.entrySet().iterator();
467     while (it.hasNext()) {
468       Map.Entry<String, PluginProperties> me = it.next();
469       if (me.getValue().getType() == type) {
470         ret.add(me.getKey()); // add to list of plugins of this type
471       }
472     }
473     if (ret.isEmpty()) {
474       LOGGER.warn("No plugins found");
475     }
476     return ret;
477   }
478 
479   /**
480    * Return instance of named plugin.
481    * 
482    * <p><br>
483    * <img src="doc-files/PluginFactory_6_UML.png"/><br>
484    * 
485    * <p>JAR is loaded only once on first request. Then its reference is stored in
486    * {@link PluginProperties} and served on next demand. Thus, if plugin is used on different frames
487    * in stack it is the same instance.
488    * 
489    * @param name Name of plugin compatible with general rules
490    * @return reference to plugin of name or null when there is any problem with creating instance
491    *         or given name does not exist in availPlugins base
492    */
493   public IQuimpCorePlugin getInstance(final String name) {
494     try {
495       if (name.isEmpty()) {
496         throw new IllegalArgumentException("Plugin of name: " + name + " is not loaded");
497       }
498       // usually name of plugin is spelled with Capital letter first
499       // make sure that name is in correct format
500       String qname = name.substring(0, 1).toUpperCase() + name.substring(1);
501       // find name in database
502       PluginProperties pp = availPlugins.get(qname);
503       if (pp == null) {
504         throw new IllegalArgumentException("Plugin of name: " + name + " is not loaded");
505       }
506       // load class and create instance
507       IQuimpCorePlugin instance;
508       if (pp.getRef() == null) { // not used yet
509         instance = (IQuimpCorePlugin) getPluginInstance(pp.getFile(), pp.getClassName());
510         pp.setRef(instance); // store for next request
511       } else {
512         instance = pp.getRef(); // return same instance as previous for "name"
513       }
514       return instance;
515     } catch (MalformedURLException | ClassNotFoundException | InstantiationException
516             | IllegalAccessException | IllegalArgumentException e) {
517       LOGGER.error("Plugin " + name + " can not be instanced (reason: " + e.getMessage() + ")");
518       LOGGER.debug(e.getMessage(), e);
519       return null;
520     }
521 
522   }
523 
524   /**
525    * Prints system classpath.
526    */
527   public static void getSystemClassPath() {
528     LOGGER.trace("--- CLASPATH ---");
529     ClassLoader cl = ClassLoader.getSystemClassLoader();
530     URL[] urls = ((URLClassLoader) cl).getURLs();
531     for (URL urll : urls) {
532       LOGGER.trace(urll.getFile());
533     }
534     LOGGER.trace("--- CLASPATH ---");
535   }
536 }