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                // ### TODO: explain
578                if (topicType.getUri().equals("dm4.core.meta_meta_type")) {
579                    continue;
580                }
581                //
582                dispatchEvent(CoreEvent.INTRODUCE_TOPIC_TYPE, topicType);
583            }
584        } catch (Exception e) {
585            throw new RuntimeException("Introducing topic types to " + this + " failed", e);
586        }
587    }
588
589    // ### TODO: move to PersistenceLayer?
590    private void introduceAssociationTypesToPlugin() {
591        try {
592            for (AssociationType assocType : dm4.getAllAssociationTypes()) {
593                dispatchEvent(CoreEvent.INTRODUCE_ASSOCIATION_TYPE, assocType);
594            }
595        } catch (Exception e) {
596            throw new RuntimeException("Introducing association types to " + this + " failed", e);
597        }
598    }
599
600
601
602    // === Life Cycle ===
603
604    private void invokePreInstallHook() {
605        pluginContext.preInstall();
606    }
607
608    private void invokeInitHook() {
609        pluginContext.init();
610    }
611
612    private void invokeShutdownHook() {
613        try {
614            pluginContext.shutdown();
615        } catch (Throwable e) {
616            // Note: we don't throw here. Stopping the plugin must proceed.
617            logger.log(Level.SEVERE, "An error occurred in the shutdown() hook of " + this + ":", e);
618        }
619    }
620
621
622
623    // === Events ===
624
625    private void registerListeners() {
626        try {
627            List<DeepaMehtaEvent> events = getEvents();
628            //
629            if (events.size() == 0) {
630                logger.info("Registering event listeners of " + this + " ABORTED -- no event listeners implemented");
631                return;
632            }
633            //
634            logger.info("Registering " + events.size() + " event listeners of " + this);
635            for (DeepaMehtaEvent event : events) {
636                dm4.em.addListener(event, (EventListener) pluginContext);
637            }
638        } catch (Exception e) {
639            throw new RuntimeException("Registering event listeners of " + this + " failed", e);
640        }
641    }
642
643    private void unregisterListeners() {
644        List<DeepaMehtaEvent> events = getEvents();
645        if (events.size() == 0) {
646            return;
647        }
648        //
649        logger.info("Unregistering event listeners of " + this);
650        for (DeepaMehtaEvent event : events) {
651            dm4.em.removeListener(event, (EventListener) pluginContext);
652        }
653    }
654
655    // ---
656
657    /**
658     * Returns the events this plugin is listening to.
659     */
660    private List<DeepaMehtaEvent> getEvents() {
661        List<DeepaMehtaEvent> events = new ArrayList();
662        for (Class interfaze : pluginContext.getClass().getInterfaces()) {
663            if (isListenerInterface(interfaze)) {
664                DeepaMehtaEvent event = DeepaMehtaEvent.getEvent(interfaze);
665                logger.fine("### EventListener Interface: " + interfaze + ", event=" + event);
666                events.add(event);
667            }
668        }
669        return events;
670    }
671
672    /**
673     * Checks weather this plugin is a listener for the given event, and if so, dispatches the event to this plugin.
674     * Otherwise nothing is performed.
675     * <p>
676     * Called internally to dispatch the INTRODUCE_TOPIC_TYPE and INTRODUCE_ASSOCIATION_TYPE events.
677     */
678    private void dispatchEvent(DeepaMehtaEvent event, Object... params) {
679        dm4.em.dispatchEvent(this, event, params);
680    }
681
682    /**
683     * Returns true if the specified interface is an event listener interface.
684     * A event listener interface is a sub-interface of {@link EventListener}.
685     */
686    private boolean isListenerInterface(Class interfaze) {
687        return EventListener.class.isAssignableFrom(interfaze);
688    }
689
690
691
692    // === Provided Service ===
693
694    /**
695     * Registers the provided service at the OSGi framework.
696     * If this plugin doesn't provide a service nothing is performed.
697     */
698    private void registerProvidedService() {
699        // Note: "providedServiceInterface" is initialized in constructor. Initializing it here would be
700        // too late as the MigrationManager accesses it. In activate() the MigrationManager is called
701        // *before* registerProvidedService().
702        try {
703            if (providedServiceInterface == null) {
704                logger.info("Registering OSGi service of " + this + " ABORTED -- no OSGi service provided");
705                return;
706            }
707            //
708            logger.info("Registering service \"" + providedServiceInterface + "\" at OSGi framework");
709            bundleContext.registerService(providedServiceInterface, pluginContext, null);
710        } catch (Exception e) {
711            throw new RuntimeException("Registering service of " + this + " at OSGi framework failed", e);
712        }
713    }
714
715    private String providedServiceInterface() {
716        List<Class<?>> serviceInterfaces = getInterfaces("Service");
717        switch (serviceInterfaces.size()) {
718        case 0:
719            return null;
720        case 1:
721            return serviceInterfaces.get(0).getName();
722        default:
723            throw new RuntimeException("Only one service interface per plugin is supported");
724        }
725    }
726
727
728
729    // === Web Resources ===
730
731    /**
732     * Publishes this plugin's web resources (via WebPublishingService).
733     * If the plugin doesn't provide web resources nothing is performed.
734     */
735    private void publishWebResources() {
736        String uriNamespace = null;
737        try {
738            uriNamespace = getWebResourcesNamespace();
739            if (uriNamespace == null) {
740                logger.info("Publishing web resources of " + this + " ABORTED -- no web resources provided");
741                return;
742            }
743            //
744            logger.info("Publishing web resources of " + this + " at URI namespace \"" + uriNamespace + "\"");
745            webResources = dm4.wpService.publishWebResources(uriNamespace, pluginBundle);
746        } catch (Exception e) {
747            throw new RuntimeException("Publishing web resources of " + this + " failed " +
748                "(URI namespace=\"" + uriNamespace + "\")", e);
749        }
750    }
751
752    private void unpublishWebResources() {
753        if (webResources != null) {
754            logger.info("Unpublishing web resources of " + this);
755            webResources.unpublish();
756        }
757    }
758
759    // ---
760
761    private String getWebResourcesNamespace() {
762        return getBundleEntry("/web") != null ? "/" + pluginUri : null;
763    }
764
765    private URL getBundleEntry(String path) {
766        return pluginBundle.getEntry(path);
767    }
768
769
770
771    // === File System Resources ===
772
773    // Note: publishing is performed by public method publishFileSystem()
774
775    private void unpublishFileSystem() {
776        if (fileSystemResources != null) {
777            logger.info("Unpublishing file system resources of " + this);
778            fileSystemResources.unpublish();
779        }
780    }
781
782
783
784    // === REST Resources ===
785
786    /**
787     * Publishes this plugin's REST resources (via WebPublishingService).
788     * If the plugin doesn't provide REST resources nothing is performed.
789     */
790    private void publishRestResources() {
791        try {
792            // root resources
793            List<Object> rootResources = getRootResources();
794            if (rootResources.size() != 0) {
795                String uriNamespace = dm4.wpService.getUriNamespace(pluginContext);
796                logger.info("Publishing REST resources of " + this + " at URI namespace \"" + uriNamespace + "\"");
797            } else {
798                logger.info("Publishing REST resources of " + this + " ABORTED -- no REST resources provided");
799            }
800            // provider classes
801            List<Class<?>> providerClasses = getProviderClasses();
802            if (providerClasses.size() != 0) {
803                logger.info("Registering " + providerClasses.size() + " provider classes of " + this);
804            } else {
805                logger.info("Registering provider classes of " + this + " ABORTED -- no provider classes provided");
806            }
807            // register
808            if (rootResources.size() != 0 || providerClasses.size() != 0) {
809                restResources = dm4.wpService.publishRestResources(rootResources, providerClasses);
810            }
811        } catch (Exception e) {
812            unpublishWebResources();
813            throw new RuntimeException("Publishing REST resources (including provider classes) of " + this +
814                " failed", e);
815        }
816    }
817
818    private void unpublishRestResources() {
819        if (restResources != null) {
820            logger.info("Unpublishing REST resources (including provider classes) of " + this);
821            restResources.unpublish();
822        }
823    }
824
825    // ---
826
827    private List<Object> getRootResources() {
828        List<Object> rootResources = new ArrayList();
829        if (dm4.wpService.isRootResource(pluginContext)) {
830            rootResources.add(pluginContext);
831        }
832        return rootResources;
833    }
834
835    private List<Class<?>> getProviderClasses() throws IOException {
836        List<Class<?>> providerClasses = new ArrayList();
837        for (String className : scanPackage("/provider")) {
838            Class clazz = loadClass(className);
839            if (clazz == null) {
840                throw new RuntimeException("Loading provider class \"" + className + "\" failed");
841            } else if (!dm4.wpService.isProviderClass(clazz)) {
842                // Note: scanPackage() also returns nested classes, so we check explicitly.
843                continue;
844            }
845            //
846            providerClasses.add(clazz);
847        }
848        return providerClasses;
849    }
850
851
852
853    // === Plugin Dependencies ===
854
855    private List<String> pluginDependencies() {
856        List<String> pluginDependencies = new ArrayList();
857        String activateAfter = getConfigProperty("dm4.plugin.activate_after");
858        if (activateAfter != null) {
859            String[] pluginUris = activateAfter.split(", *");
860            for (int i = 0; i < pluginUris.length; i++) {
861                pluginDependencies.add(pluginUris[i]);
862            }
863        }
864        //
865        if (!pluginDependencies.isEmpty()) {
866            logger.info("Tracking " + pluginDependencies.size() + " plugins for " + this + " " + pluginDependencies);
867        } else {
868            logger.info("Tracking plugins for " + this + " ABORTED -- no plugin dependencies declared");
869        }
870        //
871        return pluginDependencies;
872    }
873
874    private boolean hasDependency(String pluginUri) {
875        return pluginDependencies.contains(pluginUri);
876    }
877
878    private boolean dependenciesAvailable() {
879        for (String pluginUri : pluginDependencies) {
880            if (!isPluginActivated(pluginUri)) {
881                return false;
882            }
883        }
884        return true;
885    }
886
887    private boolean isPluginActivated(String pluginUri) {
888        return dm4.pluginManager.isPluginActivated(pluginUri);
889    }
890
891    // Note: PLUGIN_ACTIVATED is defined as an OSGi event and not as a DeepaMehtaEvent.
892    // PLUGIN_ACTIVATED is not supposed to be listened by plugins.
893    // It is a solely used internally (to track plugin availability).
894
895    private void registerPluginActivatedEventListener() {
896        String[] topics = new String[] {PLUGIN_ACTIVATED};
897        Hashtable properties = new Hashtable();
898        properties.put(EventConstants.EVENT_TOPIC, topics);
899        bundleContext.registerService(EventHandler.class.getName(), this, properties);
900    }
901
902    private void postPluginActivatedEvent() {
903        Map<String, String> properties = new HashMap();
904        properties.put(EventConstants.BUNDLE_SYMBOLICNAME, pluginUri);
905        eventService.postEvent(new Event(PLUGIN_ACTIVATED, properties));
906    }
907
908    // --- EventHandler Implementation ---
909
910    @Override
911    public void handleEvent(Event event) {
912        String pluginUri = null;
913        try {
914            if (!event.getTopic().equals(PLUGIN_ACTIVATED)) {
915                throw new RuntimeException("Unexpected event: " + event);
916            }
917            //
918            pluginUri = (String) event.getProperty(EventConstants.BUNDLE_SYMBOLICNAME);
919            if (!hasDependency(pluginUri)) {
920                return;
921            }
922            //
923            logger.info("Handling PLUGIN_ACTIVATED event from \"" + pluginUri + "\" for " + this);
924            checkRequirementsForActivation();
925        } catch (Throwable e) {
926            logger.log(Level.SEVERE, "An error occurred while handling PLUGIN_ACTIVATED event from \"" + pluginUri +
927                "\" for " + this + ":", e);
928            // Note: here we catch anything, also errors (like NoClassDefFoundError).
929            // If thrown through the OSGi container it would not print out the stacktrace.
930        }
931    }
932
933
934
935    // === Helper ===
936
937    private String pluginName() {
938        return pluginContext.getPluginName();
939    }
940
941    private List<Class<?>> getInterfaces(String suffix) {
942        List<Class<?>> interfaces = new ArrayList();
943        for (Class<?> interfaze : pluginContext.getClass().getInterfaces()) {
944            if (interfaze.getName().endsWith(suffix)) {
945                interfaces.add(interfaze);
946            }
947        }
948        return interfaces;
949    }
950
951    // ---
952
953    private List<String> scanPackage(String relativePath) {
954        List<String> classNames = new ArrayList();
955        Enumeration<String> e = getPluginPaths(relativePath);
956        if (e != null) {
957            while (e.hasMoreElements()) {
958                String entryPath = e.nextElement();
959                String className = entryPathToClassName(entryPath);
960                logger.fine("  # Found class: " + className);
961                classNames.add(className);
962            }
963        }
964        return classNames;
965    }
966
967    private Enumeration<String> getPluginPaths(String relativePath) {
968        String path = "/" + pluginPackage.replace('.', '/') + relativePath;
969        logger.fine("### Scanning path \"" + path + "\"");
970        return pluginBundle.getEntryPaths(path);
971    }
972
973    private String entryPathToClassName(String entryPath) {
974        entryPath = entryPath.substring(0, entryPath.length() - 6);     // strip ".class"
975        return entryPath.replace('/', '.');
976    }
977}