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