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