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