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