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