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