1 /* 2 * PluginManager.java 24 oct. 2008 3 * 4 * Sweet Home 3D, Copyright (c) 2008 Emmanuel PUYBARET / eTeks <info@eteks.com> 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; either version 2 of the License, or 9 * (at your option) any later version. 10 * 11 * This program is distributed in the hope that it will be useful, 12 * but WITHOUT ANY WARRANTY; without even the implied warranty of 13 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 14 * GNU General Public License for more details. 15 * 16 * You should have received a copy of the GNU General Public License 17 * along with this program; if not, write to the Free Software 18 * Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA 19 */ 20 package com.eteks.sweethome3d.plugin; 21 22 import java.io.BufferedInputStream; 23 import java.io.File; 24 import java.io.FileFilter; 25 import java.io.FileInputStream; 26 import java.io.FileOutputStream; 27 import java.io.IOException; 28 import java.io.InputStream; 29 import java.io.OutputStream; 30 import java.lang.reflect.Constructor; 31 import java.lang.reflect.Modifier; 32 import java.net.MalformedURLException; 33 import java.net.URL; 34 import java.net.URLClassLoader; 35 import java.net.URLEncoder; 36 import java.util.ArrayList; 37 import java.util.Arrays; 38 import java.util.Collections; 39 import java.util.Iterator; 40 import java.util.LinkedHashMap; 41 import java.util.List; 42 import java.util.Locale; 43 import java.util.Map; 44 import java.util.MissingResourceException; 45 import java.util.ResourceBundle; 46 import java.util.TreeMap; 47 import java.util.zip.ZipEntry; 48 import java.util.zip.ZipInputStream; 49 50 import javax.swing.undo.UndoableEditSupport; 51 52 import com.eteks.sweethome3d.model.CollectionEvent; 53 import com.eteks.sweethome3d.model.CollectionListener; 54 import com.eteks.sweethome3d.model.Home; 55 import com.eteks.sweethome3d.model.HomeApplication; 56 import com.eteks.sweethome3d.model.Library; 57 import com.eteks.sweethome3d.model.RecorderException; 58 import com.eteks.sweethome3d.model.UserPreferences; 59 import com.eteks.sweethome3d.tools.OperatingSystem; 60 import com.eteks.sweethome3d.viewcontroller.HomeController; 61 62 /** 63 * Sweet Home 3D plug-ins manager. 64 * @author Emmanuel Puybaret 65 */ 66 public class PluginManager { 67 public static final String PLUGIN_LIBRARY_TYPE = "Plugin"; 68 69 private static final String ID = "id"; 70 private static final String NAME = "name"; 71 private static final String CLASS = "class"; 72 private static final String DESCRIPTION = "description"; 73 private static final String VERSION = "version"; 74 private static final String LICENSE = "license"; 75 private static final String PROVIDER = "provider"; 76 private static final String APPLICATION_MINIMUM_VERSION = "applicationMinimumVersion"; 77 private static final String JAVA_MINIMUM_VERSION = "javaMinimumVersion"; 78 79 private static final String APPLICATION_PLUGIN_FAMILY = "ApplicationPlugin"; 80 81 private static final String DEFAULT_APPLICATION_PLUGIN_PROPERTIES_FILE = 82 APPLICATION_PLUGIN_FAMILY + ".properties"; 83 84 private final File [] pluginFolders; 85 private final Map<String, PluginLibrary> pluginLibraries = 86 new TreeMap<String, PluginLibrary>(); 87 private final Map<Home, List<Plugin>> homePlugins = new LinkedHashMap<Home, List<Plugin>>(); 88 89 /** 90 * Reads application plug-ins from resources in the given plug-in folder. 91 */ PluginManager(File pluginFolder)92 public PluginManager(File pluginFolder) { 93 this(new File [] {pluginFolder}); 94 } 95 96 /** 97 * Reads application plug-ins from resources in the given plug-in folders. 98 * @since 3.0 99 */ PluginManager(File [] pluginFolders)100 public PluginManager(File [] pluginFolders) { 101 this.pluginFolders = pluginFolders; 102 if (pluginFolders != null) { 103 for (File pluginFolder : pluginFolders) { 104 // Try to load plugin files from plugin folder 105 File [] pluginFiles = pluginFolder.listFiles(new FileFilter () { 106 public boolean accept(File pathname) { 107 return pathname.isFile(); 108 } 109 }); 110 111 if (pluginFiles != null) { 112 // Treat plug in files in reverse order of their version number 113 Arrays.sort(pluginFiles, Collections.reverseOrder(OperatingSystem.getFileVersionComparator())); 114 for (File pluginFile : pluginFiles) { 115 try { 116 loadPlugins(pluginFile.toURI().toURL(), pluginFile.getAbsolutePath()); 117 } catch (MalformedURLException ex) { 118 // Files are supposed to exist ! 119 } 120 } 121 } 122 } 123 } 124 } 125 126 /** 127 * Reads application plug-ins from resources in the given URLs. 128 */ PluginManager(URL [] pluginUrls)129 public PluginManager(URL [] pluginUrls) { 130 this.pluginFolders = null; 131 for (URL pluginUrl : pluginUrls) { 132 loadPlugins(pluginUrl, pluginUrl.toExternalForm()); 133 } 134 } 135 136 /** 137 * Loads the plug-ins that may be available in the given URL. 138 */ loadPlugins(URL pluginUrl, String pluginLocation)139 private void loadPlugins(URL pluginUrl, String pluginLocation) { 140 ZipInputStream zipIn = null; 141 try { 142 // Open a zip input from pluginUrl 143 zipIn = new ZipInputStream(pluginUrl.openStream()); 144 // Try do find a plugin properties file in current zip stream 145 for (ZipEntry entry; (entry = zipIn.getNextEntry()) != null; ) { 146 String zipEntryName = entry.getName(); 147 int lastIndex = zipEntryName.lastIndexOf(DEFAULT_APPLICATION_PLUGIN_PROPERTIES_FILE); 148 if (lastIndex != -1 149 && (lastIndex == 0 150 || zipEntryName.charAt(lastIndex - 1) == '/')) { 151 try { 152 // Build application plugin family with its package 153 String applicationPluginFamily = zipEntryName.substring(0, lastIndex); 154 applicationPluginFamily += APPLICATION_PLUGIN_FAMILY; 155 ClassLoader classLoader = new URLClassLoader(new URL [] {pluginUrl}, getClass().getClassLoader()); 156 readPlugin(ResourceBundle.getBundle(applicationPluginFamily, Locale.getDefault(), classLoader), 157 pluginLocation, 158 "jar:" + pluginUrl.toString() + "!/" + URLEncoder.encode(zipEntryName, "UTF-8").replace("+", "%20"), 159 classLoader); 160 } catch (MissingResourceException ex) { 161 // Ignore malformed plugins 162 } 163 } 164 } 165 } catch (IOException ex) { 166 // Ignore furniture plugin 167 } finally { 168 if (zipIn != null) { 169 try { 170 zipIn.close(); 171 } catch (IOException ex) { 172 } 173 } 174 } 175 } 176 177 /** 178 * Reads the plug-in properties from the given <code>resource</code>. 179 */ readPlugin(ResourceBundle resource, String pluginLocation, String pluginEntry, ClassLoader pluginClassLoader)180 private void readPlugin(ResourceBundle resource, 181 String pluginLocation, 182 String pluginEntry, 183 ClassLoader pluginClassLoader) { 184 try { 185 String name = resource.getString(NAME); 186 187 // Check Java and application versions 188 String javaMinimumVersion = resource.getString(JAVA_MINIMUM_VERSION); 189 if (!OperatingSystem.isJavaVersionGreaterOrEqual(javaMinimumVersion)) { 190 System.err.println("Invalid plug-in " + pluginEntry + ":\n" 191 + "Not compatible Java version " + System.getProperty("java.version")); 192 return; 193 } 194 195 String applicationMinimumVersion = resource.getString(APPLICATION_MINIMUM_VERSION); 196 if (!isApplicationVersionSuperiorTo(applicationMinimumVersion)) { 197 System.err.println("Invalid plug-in " + pluginEntry + ":\n" 198 + "Not compatible application version"); 199 return; 200 } 201 202 String pluginClassName = resource.getString(CLASS); 203 Class<? extends Plugin> pluginClass = getPluginClass(pluginClassLoader, pluginClassName); 204 205 String id = getOptionalString(resource, ID, null); 206 String description = resource.getString(DESCRIPTION); 207 String version = resource.getString(VERSION); 208 String license = resource.getString(LICENSE); 209 String provider = resource.getString(PROVIDER); 210 211 // Store plug-in properties if they don't exist yet 212 if (this.pluginLibraries.get(name) == null) { 213 this.pluginLibraries.put(name, new PluginLibrary( 214 pluginLocation, id, name, description, version, license, provider, pluginClass, pluginClassLoader)); 215 } 216 } catch (MissingResourceException ex) { 217 System.err.println("Invalid plug-in " + pluginEntry + ":\n" + ex.getMessage()); 218 } catch (IllegalArgumentException ex) { 219 System.err.println("Invalid plug-in " + pluginEntry + ":\n" + ex.getMessage()); 220 } 221 } 222 223 /** 224 * Returns the value of the property with the given <code>key</code> or the default value 225 * if the property isn't defined. 226 */ getOptionalString(ResourceBundle resource, String key, String defaultValue)227 private String getOptionalString(ResourceBundle resource, String key, String defaultValue) { 228 try { 229 return resource.getString(key); 230 } catch (MissingResourceException ex) { 231 return defaultValue; 232 } 233 } 234 235 /** 236 * Returns <code>true</code> if the given version is smaller than the version 237 * of the application. Versions are compared only on their first two parts. 238 */ isApplicationVersionSuperiorTo(String applicationMinimumVersion)239 private boolean isApplicationVersionSuperiorTo(String applicationMinimumVersion) { 240 String [] applicationMinimumVersionParts = applicationMinimumVersion.split("\\.|_|\\s"); 241 if (applicationMinimumVersionParts.length >= 1) { 242 try { 243 // Compare digits in first part 244 int applicationVersionFirstPart = (int)(Home.CURRENT_VERSION / 1000); 245 int applicationMinimumVersionFirstPart = Integer.parseInt(applicationMinimumVersionParts [0]); 246 if (applicationVersionFirstPart > applicationMinimumVersionFirstPart) { 247 return true; 248 } else if (applicationVersionFirstPart == applicationMinimumVersionFirstPart 249 && applicationMinimumVersionParts.length >= 2) { 250 // Compare digits in second part 251 return ((Home.CURRENT_VERSION / 100) % 10) >= Integer.parseInt(applicationMinimumVersionParts [1]); 252 } 253 } catch (NumberFormatException ex) { 254 } 255 } 256 return false; 257 } 258 259 /** 260 * Returns the <code>Class</code> instance of the class named <code>pluginClassName</code>, 261 * after checking plug-in class exists, may be instantiated and has a default public constructor. 262 */ 263 @SuppressWarnings("unchecked") getPluginClass(ClassLoader pluginClassLoader, String pluginClassName)264 private Class<? extends Plugin> getPluginClass(ClassLoader pluginClassLoader, 265 String pluginClassName) { 266 try { 267 Class<? extends Plugin> pluginClass = 268 (Class<? extends Plugin>)pluginClassLoader.loadClass(pluginClassName); 269 if (!Plugin.class.isAssignableFrom(pluginClass)) { 270 throw new IllegalArgumentException( 271 pluginClassName + " not a subclass of " + Plugin.class.getName()); 272 } else if (Modifier.isAbstract(pluginClass.getModifiers()) 273 || !Modifier.isPublic(pluginClass.getModifiers())) { 274 throw new IllegalArgumentException( 275 pluginClassName + " not a public static class"); 276 } 277 Constructor<? extends Plugin> constructor = pluginClass.getConstructor(new Class [0]); 278 if (!Modifier.isPublic(constructor.getModifiers())) { 279 throw new IllegalArgumentException( 280 pluginClassName + " constructor not accessible"); 281 } 282 return pluginClass; 283 } catch (NoClassDefFoundError ex) { 284 throw new IllegalArgumentException(ex.getMessage(), ex); 285 } catch (ClassNotFoundException ex) { 286 throw new IllegalArgumentException(ex.getMessage(), ex); 287 } catch (NoSuchMethodException ex) { 288 throw new IllegalArgumentException(ex.getMessage(), ex); 289 } 290 } 291 292 /** 293 * Returns the available plug-in libraries. 294 * @since 4.0 295 */ getPluginLibraries()296 public List<Library> getPluginLibraries() { 297 return Collections.unmodifiableList(new ArrayList<Library>(this.pluginLibraries.values())); 298 } 299 300 /** 301 * Returns an unmodifiable list of plug-in instances initialized with the 302 * given parameters. 303 */ getPlugins(final HomeApplication application, final Home home, UserPreferences preferences, UndoableEditSupport undoSupport)304 public List<Plugin> getPlugins(final HomeApplication application, 305 final Home home, 306 UserPreferences preferences, 307 UndoableEditSupport undoSupport) { 308 return getPlugins(application, home, preferences, null, undoSupport); 309 } 310 311 /** 312 * Returns an unmodifiable list of plug-in instances initialized with the 313 * given parameters. 314 * @since 3.5 315 */ getPlugins(final HomeApplication application, final Home home, UserPreferences preferences, HomeController homeController, UndoableEditSupport undoSupport)316 List<Plugin> getPlugins(final HomeApplication application, 317 final Home home, 318 UserPreferences preferences, 319 HomeController homeController, 320 UndoableEditSupport undoSupport) { 321 if (application.getHomes().contains(home)) { 322 List<Plugin> plugins = this.homePlugins.get(home); 323 if (plugins == null) { 324 plugins = new ArrayList<Plugin>(); 325 // Instantiate each plug-in class 326 for (PluginLibrary pluginLibrary : this.pluginLibraries.values()) { 327 try { 328 Plugin plugin = pluginLibrary.getPluginClass().newInstance(); 329 plugin.setPluginClassLoader(pluginLibrary.getPluginClassLoader()); 330 plugin.setName(pluginLibrary.getName()); 331 plugin.setDescription(pluginLibrary.getDescription()); 332 plugin.setVersion(pluginLibrary.getVersion()); 333 plugin.setLicense(pluginLibrary.getLicense()); 334 plugin.setProvider(pluginLibrary.getProvider()); 335 plugin.setUserPreferences(preferences); 336 plugin.setHome(home); 337 plugin.setHomeController(homeController); 338 plugin.setUndoableEditSupport(undoSupport); 339 plugins.add(plugin); 340 } catch (InstantiationException ex) { 341 // Shouldn't happen : plug-in class was checked during readPlugin call 342 throw new RuntimeException(ex); 343 } catch (IllegalAccessException ex) { 344 // Shouldn't happen : plug-in class was checked during readPlugin call 345 throw new RuntimeException(ex); 346 } 347 } 348 349 plugins = Collections.unmodifiableList(plugins); 350 this.homePlugins.put(home, plugins); 351 352 // Initialize plug-ins 353 for (Plugin plugin : plugins) { 354 plugin.init(); 355 } 356 357 // Add a listener that will destroy all plug-ins when home is deleted 358 application.addHomesListener(new CollectionListener<Home>() { 359 public void collectionChanged(CollectionEvent<Home> ev) { 360 if (ev.getType() == CollectionEvent.Type.DELETE 361 && ev.getItem() == home) { 362 for (Plugin plugin : homePlugins.get(home)) { 363 plugin.destroy(); 364 } 365 homePlugins.remove(home); 366 application.removeHomesListener(this); 367 } 368 } 369 }); 370 } 371 return plugins; 372 } else { 373 return Collections.emptyList(); 374 } 375 } 376 377 /** 378 * Returns <code>true</code> if a plug-in in the given file name already exists 379 * in the first plug-ins folder. 380 * @throws RecorderException if no plug-ins folder is associated to this manager. 381 */ pluginExists(String pluginLocation)382 public boolean pluginExists(String pluginLocation) throws RecorderException { 383 if (this.pluginFolders == null 384 || this.pluginFolders.length == 0) { 385 throw new RecorderException("Can't access to plugins folder"); 386 } else { 387 String pluginFileName = new File(pluginLocation).getName(); 388 return new File(this.pluginFolders [0], pluginFileName).exists(); 389 } 390 } 391 392 /** 393 * Deletes the given plug-in <code>libraries</code> from managed plug-ins. 394 * @since 4.0 395 */ deletePlugins(List<Library> libraries)396 public void deletePlugins(List<Library> libraries) throws RecorderException { 397 for (Library library : libraries) { 398 for (Iterator<Map.Entry<String, PluginLibrary>> it = this.pluginLibraries.entrySet().iterator(); it.hasNext(); ) { 399 String pluginLocation = it.next().getValue().getLocation(); 400 if (pluginLocation.equals(library.getLocation())) { 401 if (new File(pluginLocation).exists() 402 && !new File(pluginLocation).delete()) { 403 throw new RecorderException("Couldn't delete file " + library.getLocation()); 404 } 405 it.remove(); 406 } 407 } 408 } 409 } 410 411 /** 412 * Adds the file at the given location to the first plug-ins folders if it exists. 413 * Once added, the plug-in will be available at next application start. 414 * @throws RecorderException if no plug-ins folder is associated to this manager. 415 */ addPlugin(String pluginPath)416 public void addPlugin(String pluginPath) throws RecorderException { 417 try { 418 if (this.pluginFolders == null 419 || this.pluginFolders.length == 0) { 420 throw new RecorderException("Can't access to plugins folder"); 421 } 422 String pluginFileName = new File(pluginPath).getName(); 423 File destinationFile = new File(this.pluginFolders [0], pluginFileName); 424 425 // Copy furnitureCatalogFile to furniture plugin folder 426 InputStream tempIn = null; 427 OutputStream tempOut = null; 428 try { 429 tempIn = new BufferedInputStream(new FileInputStream(pluginPath)); 430 this.pluginFolders [0].mkdirs(); 431 tempOut = new FileOutputStream(destinationFile); 432 byte [] buffer = new byte [8192]; 433 int size; 434 while ((size = tempIn.read(buffer)) != -1) { 435 tempOut.write(buffer, 0, size); 436 } 437 } finally { 438 if (tempIn != null) { 439 tempIn.close(); 440 } 441 if (tempOut != null) { 442 tempOut.close(); 443 } 444 } 445 } catch (IOException ex) { 446 throw new RecorderException( 447 "Can't write " + pluginPath + " in plugins folder", ex); 448 } 449 } 450 451 /** 452 * The properties required to instantiate a plug-in. 453 */ 454 private static class PluginLibrary implements Library { 455 private final String location; 456 private final String name; 457 private final String id; 458 private final String description; 459 private final String version; 460 private final String license; 461 private final String provider; 462 private final Class<? extends Plugin> pluginClass; 463 private final ClassLoader pluginClassLoader; 464 465 /** 466 * Creates plug-in properties from parameters. 467 */ PluginLibrary(String location, String id, String name, String description, String version, String license, String provider, Class<? extends Plugin> pluginClass, ClassLoader pluginClassLoader)468 public PluginLibrary(String location, 469 String id, 470 String name, String description, String version, 471 String license, String provider, 472 Class<? extends Plugin> pluginClass, ClassLoader pluginClassLoader) { 473 this.location = location; 474 this.id = id; 475 this.name = name; 476 this.description = description; 477 this.version = version; 478 this.license = license; 479 this.provider = provider; 480 this.pluginClass = pluginClass; 481 this.pluginClassLoader = pluginClassLoader; 482 } 483 getPluginClass()484 public Class<? extends Plugin> getPluginClass() { 485 return this.pluginClass; 486 } 487 getPluginClassLoader()488 public ClassLoader getPluginClassLoader() { 489 return this.pluginClassLoader; 490 } 491 getType()492 public String getType() { 493 return PluginManager.PLUGIN_LIBRARY_TYPE; 494 } 495 getLocation()496 public String getLocation() { 497 return this.location; 498 } 499 getId()500 public String getId() { 501 return this.id; 502 } 503 getName()504 public String getName() { 505 return this.name; 506 } 507 getDescription()508 public String getDescription() { 509 return this.description; 510 } 511 getVersion()512 public String getVersion() { 513 return this.version; 514 } 515 getLicense()516 public String getLicense() { 517 return this.license; 518 } 519 getProvider()520 public String getProvider() { 521 return this.provider; 522 } 523 } 524 } 525