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