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    }