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