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