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 }