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