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