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 }