001package de.deepamehta.core.impl; 002 003import de.deepamehta.core.Association; 004import de.deepamehta.core.AssociationType; 005import de.deepamehta.core.Topic; 006import de.deepamehta.core.TopicType; 007import de.deepamehta.core.model.SimpleValue; 008import de.deepamehta.core.osgi.PluginContext; 009import de.deepamehta.core.service.CoreService; 010import de.deepamehta.core.service.DeepaMehtaEvent; 011import de.deepamehta.core.service.EventListener; 012import de.deepamehta.core.service.Inject; 013import de.deepamehta.core.service.ModelFactory; 014import de.deepamehta.core.service.Plugin; 015import de.deepamehta.core.service.PluginInfo; 016import de.deepamehta.core.storage.spi.DeepaMehtaTransaction; 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 dm4 but dm4 is not passed to constructor. 045public class PluginImpl implements Plugin, EventHandler { 046 047 // ------------------------------------------------------------------------------------------------------- Constants 048 049 private static final String PLUGIN_DEFAULT_PACKAGE = "de.deepamehta.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. "de.deepamehta.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 "dm4.plugin.activate_after" property 065 private Topic pluginTopic; // Represents this plugin in DB. Holds plugin migration number. 066 067 // Consumed services (DeepaMehta Core and OSGi) 068 private CoreServiceImpl dm4; 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 (DeepaMehta 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("dm4.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 = dm4.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 "dm4.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("dm4.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 + " ABORTED " + 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 + " ABORTED -- 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 DeepaMehta 4 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 == dm4) { 428 logger.info("Removing DeepaMehta 4 core service from " + this); 429 unpublishRestResources(); 430 unpublishWebResources(); 431 unpublishFileSystem(); 432 dm4.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 dm4) { 447 this.dm4 = dm4; 448 this.mf = dm4 != null ? dm4.mf : null; 449 pluginContext.setCoreService(dm4); 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 "dm4.plugin.activate_after" config property) are active. 461 */ 462 private void checkRequirementsForActivation() { 463 if (dm4 != null && eventService != null && injectedServicesAvailable() && dependenciesAvailable()) { 464 dm4.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("Activation of " + 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 DeepaMehtaTransaction tx = dm4.beginTx(); 521 try { 522 // 1) create "Plugin" topic 523 boolean isCleanInstall = createPluginTopicIfNotExists(); 524 // 2) run migrations 525 dm4.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 ABORTED -- 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 dm4.createTopic(mf.newTopicModel(pluginUri, "dm4.core.plugin", mf.newChildTopicsModel() 561 .put("dm4.core.plugin_name", pluginName()) 562 .put("dm4.core.plugin_symbolic_name", pluginUri) 563 .put("dm4.core.plugin_migration_nr", 0) 564 )); 565 } 566 567 private Topic fetchPluginTopic() { 568 return dm4.getTopicByUri(pluginUri); 569 } 570 571 // --- 572 573 // ### TODO: move to PersistenceLayer? 574 private void introduceTopicTypesToPlugin() { 575 try { 576 for (TopicType topicType : dm4.getAllTopicTypes()) { 577 // ### TODO: explain 578 if (topicType.getUri().equals("dm4.core.meta_meta_type")) { 579 continue; 580 } 581 // 582 dispatchEvent(CoreEvent.INTRODUCE_TOPIC_TYPE, topicType); 583 } 584 } catch (Exception e) { 585 throw new RuntimeException("Introducing topic types to " + this + " failed", e); 586 } 587 } 588 589 // ### TODO: move to PersistenceLayer? 590 private void introduceAssociationTypesToPlugin() { 591 try { 592 for (AssociationType assocType : dm4.getAllAssociationTypes()) { 593 dispatchEvent(CoreEvent.INTRODUCE_ASSOCIATION_TYPE, assocType); 594 } 595 } catch (Exception e) { 596 throw new RuntimeException("Introducing association types to " + this + " failed", e); 597 } 598 } 599 600 601 602 // === Life Cycle === 603 604 private void invokePreInstallHook() { 605 pluginContext.preInstall(); 606 } 607 608 private void invokeInitHook() { 609 pluginContext.init(); 610 } 611 612 private void invokeShutdownHook() { 613 try { 614 pluginContext.shutdown(); 615 } catch (Throwable e) { 616 // Note: we don't throw here. Stopping the plugin must proceed. 617 logger.log(Level.SEVERE, "An error occurred in the shutdown() hook of " + this + ":", e); 618 } 619 } 620 621 622 623 // === Events === 624 625 private void registerListeners() { 626 try { 627 List<DeepaMehtaEvent> events = getEvents(); 628 // 629 if (events.size() == 0) { 630 logger.info("Registering event listeners of " + this + " ABORTED -- no event listeners implemented"); 631 return; 632 } 633 // 634 logger.info("Registering " + events.size() + " event listeners of " + this); 635 for (DeepaMehtaEvent event : events) { 636 dm4.em.addListener(event, (EventListener) pluginContext); 637 } 638 } catch (Exception e) { 639 throw new RuntimeException("Registering event listeners of " + this + " failed", e); 640 } 641 } 642 643 private void unregisterListeners() { 644 List<DeepaMehtaEvent> events = getEvents(); 645 if (events.size() == 0) { 646 return; 647 } 648 // 649 logger.info("Unregistering event listeners of " + this); 650 for (DeepaMehtaEvent event : events) { 651 dm4.em.removeListener(event, (EventListener) pluginContext); 652 } 653 } 654 655 // --- 656 657 /** 658 * Returns the events this plugin is listening to. 659 */ 660 private List<DeepaMehtaEvent> getEvents() { 661 List<DeepaMehtaEvent> events = new ArrayList(); 662 for (Class interfaze : pluginContext.getClass().getInterfaces()) { 663 if (isListenerInterface(interfaze)) { 664 DeepaMehtaEvent event = DeepaMehtaEvent.getEvent(interfaze); 665 logger.fine("### EventListener Interface: " + interfaze + ", event=" + event); 666 events.add(event); 667 } 668 } 669 return events; 670 } 671 672 /** 673 * Checks weather this plugin is a listener for the given event, and if so, dispatches the event to this plugin. 674 * Otherwise nothing is performed. 675 * <p> 676 * Called internally to dispatch the INTRODUCE_TOPIC_TYPE and INTRODUCE_ASSOCIATION_TYPE events. 677 */ 678 private void dispatchEvent(DeepaMehtaEvent event, Object... params) { 679 dm4.em.dispatchEvent(this, event, params); 680 } 681 682 /** 683 * Returns true if the specified interface is an event listener interface. 684 * A event listener interface is a sub-interface of {@link EventListener}. 685 */ 686 private boolean isListenerInterface(Class interfaze) { 687 return EventListener.class.isAssignableFrom(interfaze); 688 } 689 690 691 692 // === Provided Service === 693 694 /** 695 * Registers the provided service at the OSGi framework. 696 * If this plugin doesn't provide a service nothing is performed. 697 */ 698 private void registerProvidedService() { 699 // Note: "providedServiceInterface" is initialized in constructor. Initializing it here would be 700 // too late as the MigrationManager accesses it. In activate() the MigrationManager is called 701 // *before* registerProvidedService(). 702 try { 703 if (providedServiceInterface == null) { 704 logger.info("Registering OSGi service of " + this + " ABORTED -- no OSGi service provided"); 705 return; 706 } 707 // 708 logger.info("Registering service \"" + providedServiceInterface + "\" at OSGi framework"); 709 bundleContext.registerService(providedServiceInterface, pluginContext, null); 710 } catch (Exception e) { 711 throw new RuntimeException("Registering service of " + this + " at OSGi framework failed", e); 712 } 713 } 714 715 private String providedServiceInterface() { 716 List<Class<?>> serviceInterfaces = getInterfaces("Service"); 717 switch (serviceInterfaces.size()) { 718 case 0: 719 return null; 720 case 1: 721 return serviceInterfaces.get(0).getName(); 722 default: 723 throw new RuntimeException("Only one service interface per plugin is supported"); 724 } 725 } 726 727 728 729 // === Web Resources === 730 731 /** 732 * Publishes this plugin's web resources (via WebPublishingService). 733 * If the plugin doesn't provide web resources nothing is performed. 734 */ 735 private void publishWebResources() { 736 String uriNamespace = null; 737 try { 738 uriNamespace = getWebResourcesNamespace(); 739 if (uriNamespace == null) { 740 logger.info("Publishing web resources of " + this + " ABORTED -- no web resources provided"); 741 return; 742 } 743 // 744 logger.info("Publishing web resources of " + this + " at URI namespace \"" + uriNamespace + "\""); 745 webResources = dm4.wpService.publishWebResources(uriNamespace, pluginBundle); 746 } catch (Exception e) { 747 throw new RuntimeException("Publishing web resources of " + this + " failed " + 748 "(URI namespace=\"" + uriNamespace + "\")", e); 749 } 750 } 751 752 private void unpublishWebResources() { 753 if (webResources != null) { 754 logger.info("Unpublishing web resources of " + this); 755 webResources.unpublish(); 756 } 757 } 758 759 // --- 760 761 private String getWebResourcesNamespace() { 762 return getBundleEntry("/web") != null ? "/" + pluginUri : null; 763 } 764 765 private URL getBundleEntry(String path) { 766 return pluginBundle.getEntry(path); 767 } 768 769 770 771 // === File System Resources === 772 773 // Note: publishing is performed by public method publishFileSystem() 774 775 private void unpublishFileSystem() { 776 if (fileSystemResources != null) { 777 logger.info("Unpublishing file system resources of " + this); 778 fileSystemResources.unpublish(); 779 } 780 } 781 782 783 784 // === REST Resources === 785 786 /** 787 * Publishes this plugin's REST resources (via WebPublishingService). 788 * If the plugin doesn't provide REST resources nothing is performed. 789 */ 790 private void publishRestResources() { 791 try { 792 // root resources 793 List<Object> rootResources = getRootResources(); 794 if (rootResources.size() != 0) { 795 String uriNamespace = dm4.wpService.getUriNamespace(pluginContext); 796 logger.info("Publishing REST resources of " + this + " at URI namespace \"" + uriNamespace + "\""); 797 } else { 798 logger.info("Publishing REST resources of " + this + " ABORTED -- no REST resources provided"); 799 } 800 // provider classes 801 List<Class<?>> providerClasses = getProviderClasses(); 802 if (providerClasses.size() != 0) { 803 logger.info("Registering " + providerClasses.size() + " provider classes of " + this); 804 } else { 805 logger.info("Registering provider classes of " + this + " ABORTED -- no provider classes provided"); 806 } 807 // register 808 if (rootResources.size() != 0 || providerClasses.size() != 0) { 809 restResources = dm4.wpService.publishRestResources(rootResources, providerClasses); 810 } 811 } catch (Exception e) { 812 unpublishWebResources(); 813 throw new RuntimeException("Publishing REST resources (including provider classes) of " + this + 814 " failed", e); 815 } 816 } 817 818 private void unpublishRestResources() { 819 if (restResources != null) { 820 logger.info("Unpublishing REST resources (including provider classes) of " + this); 821 restResources.unpublish(); 822 } 823 } 824 825 // --- 826 827 private List<Object> getRootResources() { 828 List<Object> rootResources = new ArrayList(); 829 if (dm4.wpService.isRootResource(pluginContext)) { 830 rootResources.add(pluginContext); 831 } 832 return rootResources; 833 } 834 835 private List<Class<?>> getProviderClasses() throws IOException { 836 List<Class<?>> providerClasses = new ArrayList(); 837 for (String className : scanPackage("/provider")) { 838 Class clazz = loadClass(className); 839 if (clazz == null) { 840 throw new RuntimeException("Loading provider class \"" + className + "\" failed"); 841 } else if (!dm4.wpService.isProviderClass(clazz)) { 842 // Note: scanPackage() also returns nested classes, so we check explicitly. 843 continue; 844 } 845 // 846 providerClasses.add(clazz); 847 } 848 return providerClasses; 849 } 850 851 852 853 // === Plugin Dependencies === 854 855 private List<String> pluginDependencies() { 856 List<String> pluginDependencies = new ArrayList(); 857 String activateAfter = getConfigProperty("dm4.plugin.activate_after"); 858 if (activateAfter != null) { 859 String[] pluginUris = activateAfter.split(", *"); 860 for (int i = 0; i < pluginUris.length; i++) { 861 pluginDependencies.add(pluginUris[i]); 862 } 863 } 864 // 865 if (!pluginDependencies.isEmpty()) { 866 logger.info("Tracking " + pluginDependencies.size() + " plugins for " + this + " " + pluginDependencies); 867 } else { 868 logger.info("Tracking plugins for " + this + " ABORTED -- no plugin dependencies declared"); 869 } 870 // 871 return pluginDependencies; 872 } 873 874 private boolean hasDependency(String pluginUri) { 875 return pluginDependencies.contains(pluginUri); 876 } 877 878 private boolean dependenciesAvailable() { 879 for (String pluginUri : pluginDependencies) { 880 if (!isPluginActivated(pluginUri)) { 881 return false; 882 } 883 } 884 return true; 885 } 886 887 private boolean isPluginActivated(String pluginUri) { 888 return dm4.pluginManager.isPluginActivated(pluginUri); 889 } 890 891 // Note: PLUGIN_ACTIVATED is defined as an OSGi event and not as a DeepaMehtaEvent. 892 // PLUGIN_ACTIVATED is not supposed to be listened by plugins. 893 // It is a solely used internally (to track plugin availability). 894 895 private void registerPluginActivatedEventListener() { 896 String[] topics = new String[] {PLUGIN_ACTIVATED}; 897 Hashtable properties = new Hashtable(); 898 properties.put(EventConstants.EVENT_TOPIC, topics); 899 bundleContext.registerService(EventHandler.class.getName(), this, properties); 900 } 901 902 private void postPluginActivatedEvent() { 903 Map<String, String> properties = new HashMap(); 904 properties.put(EventConstants.BUNDLE_SYMBOLICNAME, pluginUri); 905 eventService.postEvent(new Event(PLUGIN_ACTIVATED, properties)); 906 } 907 908 // --- EventHandler Implementation --- 909 910 @Override 911 public void handleEvent(Event event) { 912 String pluginUri = null; 913 try { 914 if (!event.getTopic().equals(PLUGIN_ACTIVATED)) { 915 throw new RuntimeException("Unexpected event: " + event); 916 } 917 // 918 pluginUri = (String) event.getProperty(EventConstants.BUNDLE_SYMBOLICNAME); 919 if (!hasDependency(pluginUri)) { 920 return; 921 } 922 // 923 logger.info("Handling PLUGIN_ACTIVATED event from \"" + pluginUri + "\" for " + this); 924 checkRequirementsForActivation(); 925 } catch (Throwable e) { 926 logger.log(Level.SEVERE, "An error occurred while handling PLUGIN_ACTIVATED event from \"" + pluginUri + 927 "\" for " + this + ":", e); 928 // Note: here we catch anything, also errors (like NoClassDefFoundError). 929 // If thrown through the OSGi container it would not print out the stacktrace. 930 } 931 } 932 933 934 935 // === Helper === 936 937 private String pluginName() { 938 return pluginContext.getPluginName(); 939 } 940 941 private List<Class<?>> getInterfaces(String suffix) { 942 List<Class<?>> interfaces = new ArrayList(); 943 for (Class<?> interfaze : pluginContext.getClass().getInterfaces()) { 944 if (interfaze.getName().endsWith(suffix)) { 945 interfaces.add(interfaze); 946 } 947 } 948 return interfaces; 949 } 950 951 // --- 952 953 private List<String> scanPackage(String relativePath) { 954 List<String> classNames = new ArrayList(); 955 Enumeration<String> e = getPluginPaths(relativePath); 956 if (e != null) { 957 while (e.hasMoreElements()) { 958 String entryPath = e.nextElement(); 959 String className = entryPathToClassName(entryPath); 960 logger.fine(" # Found class: " + className); 961 classNames.add(className); 962 } 963 } 964 return classNames; 965 } 966 967 private Enumeration<String> getPluginPaths(String relativePath) { 968 String path = "/" + pluginPackage.replace('.', '/') + relativePath; 969 logger.fine("### Scanning path \"" + path + "\""); 970 return pluginBundle.getEntryPaths(path); 971 } 972 973 private String entryPathToClassName(String entryPath) { 974 entryPath = entryPath.substring(0, entryPath.length() - 6); // strip ".class" 975 return entryPath.replace('/', '.'); 976 } 977}