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