001package systems.dmx.core.impl; 002 003import systems.dmx.core.Association; 004import systems.dmx.core.AssociationType; 005import systems.dmx.core.Topic; 006import systems.dmx.core.TopicType; 007import systems.dmx.core.model.SimpleValue; 008import systems.dmx.core.osgi.PluginContext; 009import systems.dmx.core.service.CoreService; 010import systems.dmx.core.service.DMXEvent; 011import systems.dmx.core.service.EventListener; 012import systems.dmx.core.service.Inject; 013import systems.dmx.core.service.ModelFactory; 014import systems.dmx.core.service.Plugin; 015import systems.dmx.core.service.PluginInfo; 016import systems.dmx.core.storage.spi.DMXTransaction; 017 018import org.osgi.framework.Bundle; 019import org.osgi.framework.BundleContext; 020import org.osgi.framework.ServiceReference; 021import org.osgi.service.event.Event; 022import org.osgi.service.event.EventAdmin; 023import org.osgi.service.event.EventConstants; 024import org.osgi.service.event.EventHandler; 025import org.osgi.util.tracker.ServiceTracker; 026 027import java.io.InputStream; 028import java.io.IOException; 029import java.lang.reflect.Field; 030import java.net.URL; 031import java.util.ArrayList; 032import java.util.Enumeration; 033import java.util.HashMap; 034import java.util.Hashtable; 035import java.util.List; 036import java.util.Map; 037import java.util.Properties; 038import java.util.logging.Level; 039import java.util.logging.Logger; 040 041 042 043// ### TODO: refactoring? This class does too much. 044// ### It lies about its dependencies. It depends on dmx but dmx is not passed to constructor. 045public class PluginImpl implements Plugin, EventHandler { 046 047 // ------------------------------------------------------------------------------------------------------- Constants 048 049 private static final String PLUGIN_DEFAULT_PACKAGE = "systems.dmx.core.osgi"; 050 private static final String PLUGIN_CONFIG_FILE = "/plugin.properties"; 051 private static final String PLUGIN_ACTIVATED = "de/deepamehta/core/plugin_activated"; // topic of the OSGi event 052 053 // ---------------------------------------------------------------------------------------------- Instance Variables 054 055 private PluginContext pluginContext; 056 private BundleContext bundleContext; 057 058 private Bundle pluginBundle; 059 private String pluginUri; // This bundle's symbolic name, e.g. "systems.dmx.webclient" 060 061 private Properties pluginProperties; // Read from file "plugin.properties" 062 private String pluginPackage; 063 private PluginInfo pluginInfo; 064 private List<String> pluginDependencies; // plugin URIs as read from "dmx.plugin.activate_after" property 065 private Topic pluginTopic; // Represents this plugin in DB. Holds plugin migration number. 066 067 // Consumed services (DMX Core and OSGi) 068 private CoreServiceImpl dmx; 069 private ModelFactory mf; 070 private EventAdmin eventService; // needed to post the PLUGIN_ACTIVATED OSGi event 071 072 // Consumed services (injected) 073 // key: service interface (a class object), 074 // value: an InjectableService 075 private Map<Class<?>, InjectableService> injectableServices = new HashMap(); 076 077 // Trackers for the consumed services (DMX Core, OSGi, and injected services) 078 private List<ServiceTracker> serviceTrackers = new ArrayList(); 079 080 // Provided OSGi service 081 private String providedServiceInterface; 082 083 // Provided resources 084 private StaticResourcesPublication webResources; 085 private StaticResourcesPublication fileSystemResources; 086 private RestResourcesPublication restResources; 087 088 private Logger logger = Logger.getLogger(getClass().getName()); 089 090 091 092 // ---------------------------------------------------------------------------------------------------- Constructors 093 094 public PluginImpl(PluginContext pluginContext) { 095 this.pluginContext = pluginContext; 096 this.bundleContext = pluginContext.getBundleContext(); 097 // 098 this.pluginBundle = bundleContext.getBundle(); 099 this.pluginUri = pluginBundle.getSymbolicName(); 100 // 101 this.pluginProperties = readConfigFile(); 102 this.pluginPackage = getConfigProperty("dmx.plugin.main_package", pluginContext.getClass().getPackage() 103 .getName()); 104 this.pluginInfo = new PluginInfoImpl(pluginUri, pluginBundle); 105 this.pluginDependencies = pluginDependencies(); 106 // 107 this.providedServiceInterface = providedServiceInterface(); 108 } 109 110 // -------------------------------------------------------------------------------------------------- Public Methods 111 112 public void start() { 113 if (pluginDependencies.size() > 0) { 114 registerPluginActivatedEventListener(); 115 } 116 // 117 createCoreServiceTrackers(); 118 createInjectedServiceTrackers(); 119 // 120 openServiceTrackers(); 121 } 122 123 public void stop() { 124 // Note: plugins often use the shutdown() hook to unregister things at certain services. 125 // So the shutdown() hook must be invoked _before_ the service trackers are closed. 126 invokeShutdownHook(); 127 closeServiceTrackers(); 128 } 129 130 // --- 131 132 /** 133 * Publishes a directory of the server's file system. 134 * 135 * @param path An absolute path to the directory to be published. 136 */ 137 public void publishFileSystem(String uriNamespace, String path) { 138 try { 139 logger.info("### Publishing file system \"" + path + "\" at URI namespace \"" + uriNamespace + "\""); 140 // 141 if (fileSystemResources != null) { 142 throw new RuntimeException(this + " has already published file system resources; " + 143 "only one directory per plugin is supported"); 144 } 145 // 146 fileSystemResources = dmx.wpService.publishFileSystem(uriNamespace, path); 147 } catch (Exception e) { 148 throw new RuntimeException("Publishing file system \"" + path + "\" at URI namespace \"" + uriNamespace + 149 "\" failed", e); 150 } 151 } 152 153 // --- 154 155 public String getUri() { 156 return pluginUri; 157 } 158 159 // --- Plugin Implementation --- 160 161 @Override 162 public InputStream getStaticResource(String name) { 163 try { 164 // We always use the plugin bundle's class loader to access the resource. 165 // getClass().getResource() would fail for generic plugins (plugin bundles not containing a plugin 166 // subclass) because the core bundle's class loader would be used and it has no access. 167 URL url = pluginBundle.getResource(name); 168 // 169 if (url == null) { 170 throw new RuntimeException("Resource \"" + name + "\" not found"); 171 } 172 // 173 return url.openStream(); // throws IOException 174 } catch (Exception e) { 175 throw new RuntimeException("Accessing a static resource of " + this + " failed", e); 176 } 177 } 178 179 @Override 180 public boolean hasStaticResource(String name) { 181 return getBundleEntry(name) != null; 182 } 183 184 // --- 185 186 @Override 187 public String toString() { 188 return pluginContext.toString(); 189 } 190 191 192 193 // ----------------------------------------------------------------------------------------- Package Private Methods 194 195 PluginInfo getInfo() { 196 return pluginInfo; 197 } 198 199 PluginContext getContext() { 200 return pluginContext; 201 } 202 203 Topic getPluginTopic() { 204 return pluginTopic; 205 } 206 207 String getProvidedServiceInterface() { 208 return providedServiceInterface; 209 } 210 211 // --- 212 213 /** 214 * Returns a plugin configuration property (as read from file "plugin.properties") 215 * or <code>null</code> if no such property exists. 216 */ 217 String getConfigProperty(String key) { 218 return getConfigProperty(key, null); 219 } 220 221 String getConfigProperty(String key, String defaultValue) { 222 return pluginProperties.getProperty(key, defaultValue); 223 } 224 225 // --- 226 227 /** 228 * Returns the migration class name for the given migration number. 229 * 230 * @return the fully qualified migration class name, or <code>null</code> if the migration package is unknown. 231 * This is the case if the plugin bundle contains no Plugin subclass and the "dmx.plugin.main_package" 232 * config property is not set. 233 */ 234 String getMigrationClassName(int migrationNr) { 235 if (pluginPackage.equals(PLUGIN_DEFAULT_PACKAGE)) { 236 return null; // migration package is unknown 237 } 238 // 239 return pluginPackage + ".migrations.Migration" + migrationNr; 240 } 241 242 void setMigrationNr(int migrationNr) { 243 pluginTopic.getChildTopics().set("dmx.core.plugin_migration_nr", migrationNr); 244 } 245 246 // --- 247 248 /** 249 * Uses the plugin bundle's class loader to load a class by name. 250 * 251 * @return the class, or <code>null</code> if the class is not found. 252 */ 253 Class loadClass(String className) { 254 try { 255 return pluginBundle.loadClass(className); 256 } catch (ClassNotFoundException e) { 257 return null; 258 } 259 } 260 261 // ------------------------------------------------------------------------------------------------- Private Methods 262 263 264 265 // === Config Properties === 266 267 private Properties readConfigFile() { 268 try { 269 Properties properties = new Properties(); 270 // 271 if (!hasStaticResource(PLUGIN_CONFIG_FILE)) { 272 logger.info("Reading config file \"" + PLUGIN_CONFIG_FILE + "\" for " + this + " SKIPPED " + 273 "-- file does not exist"); 274 return properties; 275 } 276 // 277 logger.info("Reading config file \"" + PLUGIN_CONFIG_FILE + "\" for " + this); 278 properties.load(getStaticResource(PLUGIN_CONFIG_FILE)); 279 return properties; 280 } catch (Exception e) { 281 throw new RuntimeException("Reading config file \"" + PLUGIN_CONFIG_FILE + "\" for " + this + " failed", e); 282 } 283 } 284 285 286 287 // === Service Tracking === 288 289 private void createCoreServiceTrackers() { 290 serviceTrackers.add(createServiceTracker(CoreService.class)); 291 serviceTrackers.add(createServiceTracker(EventAdmin.class)); 292 } 293 294 private void createInjectedServiceTrackers() { 295 List<InjectableService> injectableServices = createInjectableServices(); 296 // 297 if (injectableServices.isEmpty()) { 298 logger.info("Tracking services for " + this + " SKIPPED -- no services consumed"); 299 return; 300 } 301 // 302 logger.info("Tracking " + injectableServices.size() + " services for " + this + " " + injectableServices); 303 for (InjectableService injectableService : injectableServices) { 304 Class<?> serviceInterface = injectableService.getServiceInterface(); 305 this.injectableServices.put(serviceInterface, injectableService); 306 serviceTrackers.add(createServiceTracker(serviceInterface)); 307 } 308 } 309 310 // --- 311 312 private List<InjectableService> createInjectableServices() { 313 List<InjectableService> injectableServices = new ArrayList(); 314 // 315 for (Field field : getInjectableFields(pluginContext.getClass())) { 316 Class<?> serviceInterface = field.getType(); 317 injectableServices.add(new InjectableService(pluginContext, serviceInterface, field)); 318 } 319 return injectableServices; 320 } 321 322 private boolean injectedServicesAvailable() { 323 for (InjectableService injectableService : injectableServices.values()) { 324 if (!injectableService.isServiceAvailable()) { 325 return false; 326 } 327 } 328 return true; 329 } 330 331 // --- 332 333 // called also from MigrationManager 334 static List<Field> getInjectableFields(Class<?> clazz) { 335 List<Field> injectableFields = new ArrayList(); 336 // Note: we use getDeclaredFields() (instead of getFields()) to *not* search the super classes 337 for (Field field : clazz.getDeclaredFields()) { 338 if (field.isAnnotationPresent(Inject.class)) { 339 field.setAccessible(true); // allow injection into private fields 340 injectableFields.add(field); 341 } 342 } 343 return injectableFields; 344 } 345 346 // called from MigrationManager 347 Object getInjectedService(Class<?> serviceInterface) { 348 InjectableService injectableService = injectableServices.get(serviceInterface); 349 if (injectableService == null) { 350 throw new RuntimeException("Service " + serviceInterface.getName() + " is not tracked by " + this); 351 } 352 return injectableService.getService(); 353 } 354 355 // --- 356 357 private ServiceTracker createServiceTracker(final Class serviceInterface) { 358 // 359 return new ServiceTracker(bundleContext, serviceInterface.getName(), null) { 360 361 @Override 362 public Object addingService(ServiceReference serviceRef) { 363 Object service = null; 364 try { 365 service = super.addingService(serviceRef); 366 addService(service, serviceInterface); 367 } catch (Throwable e) { 368 logger.log(Level.SEVERE, "An error occurred while adding service " + serviceInterface.getName() + 369 " to " + pluginContext + ":", e); 370 // Note: here we catch anything, also errors (like NoClassDefFoundError). 371 // If thrown through the OSGi container it would not print out the stacktrace. 372 } 373 return service; 374 } 375 376 @Override 377 public void removedService(ServiceReference ref, Object service) { 378 try { 379 removeService(service, serviceInterface); 380 super.removedService(ref, service); 381 } catch (Throwable e) { 382 logger.log(Level.SEVERE, "An error occurred while removing service " + serviceInterface.getName() + 383 " from " + pluginContext + ":", e); 384 // Note: here we catch anything, also errors (like NoClassDefFoundError). 385 // If thrown through the OSGi container it would not print out the stacktrace. 386 } 387 } 388 }; 389 } 390 391 // --- 392 393 private void openServiceTrackers() { 394 for (ServiceTracker serviceTracker : serviceTrackers) { 395 serviceTracker.open(); 396 } 397 } 398 399 private void closeServiceTrackers() { 400 for (ServiceTracker serviceTracker : serviceTrackers) { 401 serviceTracker.close(); 402 } 403 } 404 405 // --- 406 407 private void addService(Object service, Class serviceInterface) { 408 if (service instanceof CoreService) { 409 logger.info("Adding DMX core service to " + this); 410 setCoreService((CoreServiceImpl) service); 411 publishWebResources(); 412 publishRestResources(); 413 checkRequirementsForActivation(); 414 } else if (service instanceof EventAdmin) { 415 logger.info("Adding Event Admin service to " + this); 416 eventService = (EventAdmin) service; 417 checkRequirementsForActivation(); 418 } else { 419 logger.info("Adding " + serviceInterface.getName() + " to " + this); 420 injectableServices.get(serviceInterface).injectService(service); 421 pluginContext.serviceArrived(service); 422 checkRequirementsForActivation(); 423 } 424 } 425 426 private void removeService(Object service, Class serviceInterface) { 427 if (service == dmx) { 428 logger.info("Removing DMX core service from " + this); 429 unpublishRestResources(); 430 unpublishWebResources(); 431 unpublishFileSystem(); 432 dmx.pluginManager.deactivatePlugin(this); // use plugin manager before core service is removed 433 setCoreService(null); 434 } else if (service == eventService) { 435 logger.info("Removing Event Admin service from " + this); 436 eventService = null; 437 } else { 438 logger.info("Removing " + serviceInterface.getName() + " from " + this); 439 pluginContext.serviceGone(service); 440 injectableServices.get(serviceInterface).injectService(null); 441 } 442 } 443 444 // --- 445 446 private void setCoreService(CoreServiceImpl dmx) { 447 this.dmx = dmx; 448 this.mf = dmx != null ? dmx.mf : null; 449 pluginContext.setCoreService(dmx); 450 } 451 452 // --- 453 454 /** 455 * Checks if this plugin's requirements are met, and if so, activates this plugin. 456 * 457 * The requirements: 458 * - the 2 core services are available (CoreService, EventAdmin). 459 * - the injected services (according to the "Inject" annotation) are available. 460 * - the plugin dependencies (according to the "dmx.plugin.activate_after" config property) are active. 461 */ 462 private void checkRequirementsForActivation() { 463 if (dmx != null && eventService != null && injectedServicesAvailable() && dependenciesAvailable()) { 464 dmx.pluginManager.activatePlugin(this); 465 } 466 } 467 468 469 470 // === Activation === 471 472 /** 473 * Activates this plugin and then posts the PLUGIN_ACTIVATED OSGi event. 474 * 475 * Activation comprises: 476 * - invoke the plugin's preInstall() hook 477 * - install the plugin in the database (includes plugin topic, migrations, type introduction) 478 * - register the plugin's event listeners 479 * - register the plugin's OSGi service 480 * - invoke the plugin's init() hook 481 */ 482 void activate() { 483 try { 484 logger.info("----- Activating " + this + " -----"); 485 // 486 invokePreInstallHook(); 487 installPluginInDB(); 488 registerListeners(); 489 registerProvidedService(); 490 invokeInitHook(); 491 // Note: the event listeners must be registered *after* the plugin is installed in the database (see 492 // installPluginInDB() below). 493 // Consider the Access Control plugin: it can't set a topic's creator before the "admin" user is created. 494 // 495 logger.info("----- Activation of " + this + " complete -----"); 496 // 497 postPluginActivatedEvent(); 498 // 499 } catch (Throwable e) { 500 throw new RuntimeException("Activating " + this + " failed", e); 501 } 502 } 503 504 void deactivate() { 505 unregisterListeners(); 506 } 507 508 509 510 // === Installation === 511 512 /** 513 * Installs the plugin in the database. This comprises: 514 * 1) create "Plugin" topic 515 * 2) run migrations 516 * 3) type introduction (fires the {@link CoreEvent.INTRODUCE_TOPIC_TYPE} and 517 * {@link CoreEvent.INTRODUCE_ASSOCIATION_TYPE} events) 518 */ 519 private void installPluginInDB() { 520 DMXTransaction tx = dmx.beginTx(); 521 try { 522 // 1) create "Plugin" topic 523 boolean isCleanInstall = createPluginTopicIfNotExists(); 524 // 2) run migrations 525 dmx.migrationManager.runPluginMigrations(this, isCleanInstall); 526 // 3) type introduction 527 if (isCleanInstall) { 528 introduceTopicTypesToPlugin(); 529 introduceAssociationTypesToPlugin(); 530 } 531 // 532 tx.success(); 533 } catch (Exception e) { 534 logger.warning("ROLLBACK! (" + this + ")"); 535 throw new RuntimeException("Installing " + this + " in the database failed", e); 536 } finally { 537 tx.finish(); 538 } 539 } 540 541 private boolean createPluginTopicIfNotExists() { 542 pluginTopic = fetchPluginTopic(); 543 // 544 if (pluginTopic != null) { 545 logger.info("Installing " + this + " in the database SKIPPED -- already installed"); 546 return false; 547 } 548 // 549 logger.info("Installing " + this + " in the database"); 550 pluginTopic = createPluginTopic(); 551 return true; 552 } 553 554 /** 555 * Creates a Plugin topic in the DB. 556 * <p> 557 * A Plugin topic represents an installed plugin and is used to track its version. 558 */ 559 private Topic createPluginTopic() { 560 return dmx.createTopic(mf.newTopicModel(pluginUri, "dmx.core.plugin", mf.newChildTopicsModel() 561 .put("dmx.core.plugin_name", pluginName()) 562 .put("dmx.core.plugin_symbolic_name", pluginUri) 563 .put("dmx.core.plugin_migration_nr", 0) 564 )); 565 } 566 567 private Topic fetchPluginTopic() { 568 return dmx.getTopicByUri(pluginUri); 569 } 570 571 // --- 572 573 // ### TODO: move to PersistenceLayer? 574 private void introduceTopicTypesToPlugin() { 575 try { 576 for (TopicType topicType : dmx.getAllTopicTypes()) { 577 dispatchEvent(CoreEvent.INTRODUCE_TOPIC_TYPE, topicType); 578 } 579 } catch (Exception e) { 580 throw new RuntimeException("Introducing topic types to " + this + " failed", e); 581 } 582 } 583 584 // ### TODO: move to PersistenceLayer? 585 private void introduceAssociationTypesToPlugin() { 586 try { 587 for (AssociationType assocType : dmx.getAllAssociationTypes()) { 588 dispatchEvent(CoreEvent.INTRODUCE_ASSOCIATION_TYPE, assocType); 589 } 590 } catch (Exception e) { 591 throw new RuntimeException("Introducing association types to " + this + " failed", e); 592 } 593 } 594 595 596 597 // === Life Cycle === 598 599 private void invokePreInstallHook() { 600 pluginContext.preInstall(); 601 } 602 603 private void invokeInitHook() { 604 pluginContext.init(); 605 } 606 607 private void invokeShutdownHook() { 608 try { 609 pluginContext.shutdown(); 610 } catch (Throwable e) { 611 // Note: we don't throw here. Stopping the plugin must proceed. 612 logger.log(Level.SEVERE, "An error occurred in the shutdown() hook of " + this + ":", e); 613 } 614 } 615 616 617 618 // === Events === 619 620 private void registerListeners() { 621 try { 622 List<DMXEvent> events = getEvents(); 623 // 624 if (events.size() == 0) { 625 logger.info("Registering event listeners of " + this + " SKIPPED -- no event listeners implemented"); 626 return; 627 } 628 // 629 logger.info("Registering " + events.size() + " event listeners of " + this); 630 for (DMXEvent event : events) { 631 dmx.em.addListener(event, (EventListener) pluginContext); 632 } 633 } catch (Exception e) { 634 throw new RuntimeException("Registering event listeners of " + this + " failed", e); 635 } 636 } 637 638 private void unregisterListeners() { 639 List<DMXEvent> events = getEvents(); 640 if (events.size() == 0) { 641 return; 642 } 643 // 644 logger.info("Unregistering event listeners of " + this); 645 for (DMXEvent event : events) { 646 dmx.em.removeListener(event, (EventListener) pluginContext); 647 } 648 } 649 650 // --- 651 652 /** 653 * Returns the events this plugin is listening to. 654 */ 655 private List<DMXEvent> getEvents() { 656 List<DMXEvent> events = new ArrayList(); 657 for (Class interfaze : pluginContext.getClass().getInterfaces()) { 658 if (isListenerInterface(interfaze)) { 659 DMXEvent event = DMXEvent.getEvent(interfaze); 660 logger.fine("### EventListener Interface: " + interfaze + ", event=" + event); 661 events.add(event); 662 } 663 } 664 return events; 665 } 666 667 /** 668 * Checks weather this plugin is a listener for the given event, and if so, dispatches the event to this plugin. 669 * Otherwise nothing is performed. 670 * <p> 671 * Called internally to dispatch the INTRODUCE_TOPIC_TYPE and INTRODUCE_ASSOCIATION_TYPE events. 672 */ 673 private void dispatchEvent(DMXEvent event, Object... params) { 674 dmx.em.dispatchEvent(this, event, params); 675 } 676 677 /** 678 * Returns true if the specified interface is an event listener interface. 679 * A event listener interface is a sub-interface of {@link EventListener}. 680 */ 681 private boolean isListenerInterface(Class interfaze) { 682 return EventListener.class.isAssignableFrom(interfaze); 683 } 684 685 686 687 // === Provided Service === 688 689 /** 690 * Registers the provided service at the OSGi framework. 691 * If this plugin doesn't provide a service nothing is performed. 692 */ 693 private void registerProvidedService() { 694 // Note: "providedServiceInterface" is initialized in constructor. Initializing it here would be 695 // too late as the MigrationManager accesses it. In activate() the MigrationManager is called 696 // *before* registerProvidedService(). 697 try { 698 if (providedServiceInterface == null) { 699 logger.info("Registering OSGi service of " + this + " SKIPPED -- no OSGi service provided"); 700 return; 701 } 702 // 703 logger.info("Registering service \"" + providedServiceInterface + "\" at OSGi framework"); 704 bundleContext.registerService(providedServiceInterface, pluginContext, null); 705 } catch (Exception e) { 706 throw new RuntimeException("Registering service of " + this + " at OSGi framework failed", e); 707 } 708 } 709 710 private String providedServiceInterface() { 711 List<Class<?>> serviceInterfaces = getInterfaces("Service"); 712 switch (serviceInterfaces.size()) { 713 case 0: 714 return null; 715 case 1: 716 return serviceInterfaces.get(0).getName(); 717 default: 718 throw new RuntimeException("Only one service interface per plugin is supported"); 719 } 720 } 721 722 723 724 // === Web Resources === 725 726 /** 727 * Publishes this plugin's web resources (via WebPublishingService). 728 * If the plugin doesn't provide web resources nothing is performed. 729 */ 730 private void publishWebResources() { 731 String uriNamespace = null; 732 try { 733 uriNamespace = getWebResourcesNamespace(); 734 if (uriNamespace == null) { 735 logger.info("Publishing web resources of " + this + " SKIPPED -- no web resources provided"); 736 return; 737 } 738 // 739 logger.info("Publishing web resources of " + this + " at URI namespace \"" + uriNamespace + "\""); 740 webResources = dmx.wpService.publishWebResources(uriNamespace, pluginBundle); 741 } catch (Exception e) { 742 throw new RuntimeException("Publishing web resources of " + this + " failed " + 743 "(URI namespace=\"" + uriNamespace + "\")", e); 744 } 745 } 746 747 private void unpublishWebResources() { 748 if (webResources != null) { 749 logger.info("Unpublishing web resources of " + this); 750 webResources.unpublish(); 751 } 752 } 753 754 // --- 755 756 private String getWebResourcesNamespace() { 757 return getBundleEntry("/web") != null ? "/" + pluginUri : null; 758 } 759 760 private URL getBundleEntry(String path) { 761 return pluginBundle.getEntry(path); 762 } 763 764 765 766 // === File System Resources === 767 768 // Note: publishing is performed by public method publishFileSystem() 769 770 private void unpublishFileSystem() { 771 if (fileSystemResources != null) { 772 logger.info("Unpublishing file system resources of " + this); 773 fileSystemResources.unpublish(); 774 } 775 } 776 777 778 779 // === REST Resources === 780 781 /** 782 * Publishes this plugin's REST resources (via WebPublishingService). 783 * If the plugin doesn't provide REST resources nothing is performed. 784 */ 785 private void publishRestResources() { 786 try { 787 // root resources 788 List<Object> rootResources = getRootResources(); 789 if (rootResources.size() != 0) { 790 String uriNamespace = dmx.wpService.getUriNamespace(pluginContext); 791 logger.info("Publishing REST resources of " + this + " at URI namespace \"" + uriNamespace + "\""); 792 } else { 793 logger.info("Publishing REST resources of " + this + " SKIPPED -- no REST resources provided"); 794 } 795 // provider classes 796 List<Class<?>> providerClasses = getProviderClasses(); 797 if (providerClasses.size() != 0) { 798 logger.info("Registering " + providerClasses.size() + " provider classes of " + this); 799 } else { 800 logger.info("Registering provider classes of " + this + " SKIPPED -- no provider classes found"); 801 } 802 // register 803 if (rootResources.size() != 0 || providerClasses.size() != 0) { 804 restResources = dmx.wpService.publishRestResources(rootResources, providerClasses); 805 } 806 } catch (Exception e) { 807 unpublishWebResources(); 808 throw new RuntimeException("Publishing REST resources (including provider classes) of " + this + 809 " failed", e); 810 } 811 } 812 813 private void unpublishRestResources() { 814 if (restResources != null) { 815 logger.info("Unpublishing REST resources (including provider classes) of " + this); 816 restResources.unpublish(); 817 } 818 } 819 820 // --- 821 822 private List<Object> getRootResources() { 823 List<Object> rootResources = new ArrayList(); 824 if (dmx.wpService.isRootResource(pluginContext)) { 825 rootResources.add(pluginContext); 826 } 827 return rootResources; 828 } 829 830 private List<Class<?>> getProviderClasses() throws IOException { 831 List<Class<?>> providerClasses = new ArrayList(); 832 for (String className : scanPackage("/provider")) { 833 Class clazz = loadClass(className); 834 if (clazz == null) { 835 throw new RuntimeException("Loading provider class \"" + className + "\" failed"); 836 } else if (!dmx.wpService.isProviderClass(clazz)) { 837 // Note: scanPackage() also returns nested classes, so we check explicitly. 838 continue; 839 } 840 // 841 providerClasses.add(clazz); 842 } 843 return providerClasses; 844 } 845 846 847 848 // === Plugin Dependencies === 849 850 private List<String> pluginDependencies() { 851 List<String> pluginDependencies = new ArrayList(); 852 String activateAfter = getConfigProperty("dmx.plugin.activate_after"); 853 if (activateAfter != null) { 854 String[] pluginUris = activateAfter.split(", *"); 855 for (int i = 0; i < pluginUris.length; i++) { 856 pluginDependencies.add(pluginUris[i]); 857 } 858 } 859 // 860 if (!pluginDependencies.isEmpty()) { 861 logger.info("Tracking " + pluginDependencies.size() + " plugins for " + this + " " + pluginDependencies); 862 } else { 863 logger.info("Tracking plugins for " + this + " SKIPPED -- no plugin dependencies declared"); 864 } 865 // 866 return pluginDependencies; 867 } 868 869 private boolean hasDependency(String pluginUri) { 870 return pluginDependencies.contains(pluginUri); 871 } 872 873 private boolean dependenciesAvailable() { 874 for (String pluginUri : pluginDependencies) { 875 if (!isPluginActivated(pluginUri)) { 876 return false; 877 } 878 } 879 return true; 880 } 881 882 private boolean isPluginActivated(String pluginUri) { 883 return dmx.pluginManager.isPluginActivated(pluginUri); 884 } 885 886 // Note: PLUGIN_ACTIVATED is defined as an OSGi event and not as a DMXEvent. 887 // PLUGIN_ACTIVATED is not supposed to be listened by plugins. 888 // It is a solely used internally (to track plugin availability). 889 890 private void registerPluginActivatedEventListener() { 891 String[] topics = new String[] {PLUGIN_ACTIVATED}; 892 Hashtable properties = new Hashtable(); 893 properties.put(EventConstants.EVENT_TOPIC, topics); 894 bundleContext.registerService(EventHandler.class.getName(), this, properties); 895 } 896 897 private void postPluginActivatedEvent() { 898 Map<String, String> properties = new HashMap(); 899 properties.put(EventConstants.BUNDLE_SYMBOLICNAME, pluginUri); 900 eventService.postEvent(new Event(PLUGIN_ACTIVATED, properties)); 901 } 902 903 // --- EventHandler Implementation --- 904 905 @Override 906 public void handleEvent(Event event) { 907 String pluginUri = null; 908 try { 909 if (!event.getTopic().equals(PLUGIN_ACTIVATED)) { 910 throw new RuntimeException("Unexpected event: " + event); 911 } 912 // 913 pluginUri = (String) event.getProperty(EventConstants.BUNDLE_SYMBOLICNAME); 914 if (!hasDependency(pluginUri)) { 915 return; 916 } 917 // 918 logger.info("Handling PLUGIN_ACTIVATED event from \"" + pluginUri + "\" for " + this); 919 checkRequirementsForActivation(); 920 } catch (Throwable e) { 921 logger.log(Level.SEVERE, "An error occurred while handling PLUGIN_ACTIVATED event from \"" + pluginUri + 922 "\" for " + this + ":", e); 923 // Note: here we catch anything, also errors (like NoClassDefFoundError). 924 // If thrown through the OSGi container it would not print out the stacktrace. 925 } 926 } 927 928 929 930 // === Helper === 931 932 private String pluginName() { 933 return pluginContext.getPluginName(); 934 } 935 936 private List<Class<?>> getInterfaces(String suffix) { 937 List<Class<?>> interfaces = new ArrayList(); 938 for (Class<?> interfaze : pluginContext.getClass().getInterfaces()) { 939 if (interfaze.getName().endsWith(suffix)) { 940 interfaces.add(interfaze); 941 } 942 } 943 return interfaces; 944 } 945 946 // --- 947 948 private List<String> scanPackage(String relativePath) { 949 List<String> classNames = new ArrayList(); 950 Enumeration<String> e = getPluginPaths(relativePath); 951 if (e != null) { 952 while (e.hasMoreElements()) { 953 String entryPath = e.nextElement(); 954 String className = entryPathToClassName(entryPath); 955 logger.fine(" # Found class: " + className); 956 classNames.add(className); 957 } 958 } 959 return classNames; 960 } 961 962 private Enumeration<String> getPluginPaths(String relativePath) { 963 String path = "/" + pluginPackage.replace('.', '/') + relativePath; 964 logger.fine("### Scanning path \"" + path + "\""); 965 return pluginBundle.getEntryPaths(path); 966 } 967 968 private String entryPathToClassName(String entryPath) { 969 entryPath = entryPath.substring(0, entryPath.length() - 6); // strip ".class" 970 return entryPath.replace('/', '.'); 971 } 972}