ch.entwine.weblounge.kernel.publisher.EndpointPublishingService.java Source code

Java tutorial

Introduction

Here is the source code for ch.entwine.weblounge.kernel.publisher.EndpointPublishingService.java

Source

/*
 *  Weblounge: Web Content Management System
 *  Copyright (c) 2003 - 2011 The Weblounge Team
 *  http://entwinemedia.com/weblounge
 *
 *  This program is free software; you can redistribute it and/or
 *  modify it under the terms of the GNU Lesser General Public License
 *  as published by the Free Software Foundation; either version 2
 *  of the License, or (at your option) any later version.
 *
 *  This program is distributed in the hope that it will be useful,
 *  but WITHOUT ANY WARRANTY; without even the implied warranty of
 *  MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
 *  GNU Lesser General Public License for more details.
 *
 *  You should have received a copy of the GNU Lesser General Public License
 *  along with this program; if not, write to the Free Software Foundation
 *  Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
 */

package ch.entwine.weblounge.kernel.publisher;

import ch.entwine.weblounge.common.site.Environment;
import ch.entwine.weblounge.common.site.Site;
import ch.entwine.weblounge.common.url.UrlUtils;
import ch.entwine.weblounge.dispatcher.SharedHttpContext;
import ch.entwine.weblounge.kernel.site.SiteManager;
import ch.entwine.weblounge.kernel.site.SiteServiceListener;

import org.apache.commons.lang.StringUtils;
import org.osgi.framework.Bundle;
import org.osgi.framework.BundleContext;
import org.osgi.framework.ServiceEvent;
import org.osgi.framework.ServiceListener;
import org.osgi.framework.ServiceReference;
import org.osgi.framework.ServiceRegistration;
import org.osgi.service.cm.ConfigurationAdmin;
import org.osgi.service.cm.ConfigurationException;
import org.osgi.service.cm.ManagedService;
import org.osgi.service.component.ComponentContext;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

import java.util.Dictionary;
import java.util.HashMap;
import java.util.Hashtable;
import java.util.Map;
import java.util.concurrent.ConcurrentHashMap;

import javax.servlet.Servlet;
import javax.ws.rs.Path;

/**
 * Listens for JAX-RS annotated services and publishes them to the global URL
 * space using a single shared HttpContext.
 */
public class EndpointPublishingService implements ManagedService, SiteServiceListener {

    /** Logging facility */
    static final Logger logger = LoggerFactory.getLogger(EndpointPublishingService.class);

    /** Service pid, used to look up the service configuration */
    public static final String SERVICE_PID = "ch.entwine.weblounge.restpublisher";

    /** Configuration key prefix for rest publisher configuration */
    public static final String OPT_PREFIX = "restpublisher";

    /** Configuration key for the rest services path prefix */
    public static final String OPT_PATH = OPT_PREFIX + ".path";

    /** Configuration key used to configure the endpoint's alias */
    public static final String DEFAULT_PATH = "/system/weblounge";

    /** The context path option used to override the default context path */
    public static final String OPT_CONTEXTPATH = "rest.path";

    /** Context of this component */
    protected ComponentContext componentContext = null;

    /** The bundle context */
    protected BundleContext bundleContext = null;

    /** The JSR 311 service listener */
    protected ServiceListener jsr311ServiceListener = null;

    /** The mountpoint for REST services */
    protected String defaultContextPathPrefix = DEFAULT_PATH;

    /** Mapping of registered endpoints */
    protected Map<String, ServiceRegistration> endpointRegistrations = null;

    /** Mapping of registered servlets */
    protected Map<String, JAXRSServlet> endpointServlets = null;

    /** The site manager */
    protected SiteManager sites = null;

    /** The environment */
    protected Environment environment = null;

    /**
     * Creates a new publishing service for JSR 311 annotated classes.
     */
    public EndpointPublishingService() {
        endpointRegistrations = new ConcurrentHashMap<String, ServiceRegistration>();
        endpointServlets = new ConcurrentHashMap<String, JAXRSServlet>();
        jsr311ServiceListener = new JSR311AnnotatedServiceListener();
    }

    /**
     * Callback for OSGi's declarative services component activation.
     * 
     * @param context
     *          the component context
     * @throws Exception
     *           if component activation fails
     */
    void activate(ComponentContext componentContext) throws Exception {
        this.componentContext = componentContext;
        this.bundleContext = componentContext.getBundleContext();

        sites.addSiteListener(this);

        logger.info("Starting rest publishing service");

        // Try to get hold of the service configuration
        ServiceReference configAdminRef = bundleContext.getServiceReference(ConfigurationAdmin.class.getName());
        if (configAdminRef != null) {
            ConfigurationAdmin configAdmin = (ConfigurationAdmin) bundleContext.getService(configAdminRef);
            Dictionary<?, ?> config = configAdmin.getConfiguration(SERVICE_PID).getProperties();
            if (config != null) {
                configure(config);
            } else {
                logger.debug("No customized configuration for rest publisher found");
            }
        } else {
            logger.debug("No configuration admin service found while looking for rest publisher configuration");
        }

        // Make sure we are notified in case of new services
        bundleContext.addServiceListener(jsr311ServiceListener);

        // Register JAX-RS services that have already been loaded
        for (Bundle bundle : bundleContext.getBundles()) {

            // Skip bundles that are not active
            if (Bundle.ACTIVE != bundle.getState()) {
                logger.trace("Skipping bundle '{}' in state {} while looking for JAXRS endpoints", bundle,
                        bundle.getState());
                continue;
            }

            // Skip bundles that don't have any services registered
            ServiceReference[] refs = bundle.getRegisteredServices();
            if (refs == null)
                continue;

            // Explicitly register the JAXB service by crafting a manual ServiceEvent
            for (ServiceReference ref : refs) {
                try {
                    ServiceEvent event = new ServiceEvent(ServiceEvent.REGISTERED, ref);
                    jsr311ServiceListener.serviceChanged(event);
                } catch (Throwable t) {
                    logger.error("Error registering JAXRS annotated service {} : {}", ref);
                }
            }
        }

    }

    /**
     * Callback for OSGi's declarative services component inactivation.
     * 
     * @param context
     *          the component context
     * @throws Exception
     *           if component inactivation fails
     */
    void deactivate(ComponentContext componentContext) throws Exception {
        if (jsr311ServiceListener != null) {
            bundleContext.removeServiceListener(jsr311ServiceListener);
        }

        // Unregister the current jsr311 servlets
        for (String path : endpointRegistrations.keySet()) {
            unregisterEndpoint(path);
        }
        endpointRegistrations.clear();

        // Stop listening to sites
        sites.removeSiteListener(this);
    }

    /**
     * {@inheritDoc}
     * 
     * @see org.osgi.service.cm.ManagedService#updated(java.util.Dictionary)
     */
    public void updated(Dictionary properties) throws ConfigurationException {
        if (properties == null)
            return;

        boolean changed = configure(properties);
        if (!changed)
            return;

        // Unregister all current endpoints
        for (String path : endpointRegistrations.keySet()) {
            unregisterEndpoint(path);
        }

        // Register any existing JAX-RS services that have already been loaded
        for (Bundle bundle : componentContext.getBundleContext().getBundles()) {
            ServiceReference[] refs = bundle.getRegisteredServices();
            if (refs == null)
                continue;
            for (ServiceReference ref : refs) {
                ServiceEvent event = new ServiceEvent(ServiceEvent.REGISTERED, ref);
                jsr311ServiceListener.serviceChanged(event);
            }
        }

    }

    /**
     * Returns a list of all service endpoints along with their paths.
     * 
     * @return the service paths
     */
    public Map<String, ServiceRegistration> getEndpoints() {
        Map<String, ServiceRegistration> services = new HashMap<String, ServiceRegistration>(
                endpointRegistrations.size());
        services.putAll(endpointRegistrations);
        return services;
    }

    /**
     * Configures this service using the given configuration properties.
     * 
     * @param config
     *          the service configuration
     * @throws ConfigurationException
     *           if configuration fails
     */
    private synchronized boolean configure(Dictionary<?, ?> config) throws ConfigurationException {

        boolean changed = false;

        // context path
        String updatedRestMountpoint = StringUtils.trim((String) config.get(OPT_PATH));
        if (updatedRestMountpoint != null) {
            if (!updatedRestMountpoint.startsWith("/"))
                throw new IllegalArgumentException("Context path (" + OPT_PATH + ") must start with a '/'");
            changed |= !updatedRestMountpoint.equals(defaultContextPathPrefix);
            defaultContextPathPrefix = updatedRestMountpoint;
        }

        return changed;
    }

    /**
     * Creates a REST endpoint for the JAX-RS annotated service.
     * 
     * @param service
     *          The jsr311 annotated service
     * @param contextPath
     *          the http context
     * @param bundle
     *          the registering bundle
     * @param the
     *          endpoint's path
     */
    protected void registerEndpoint(Object service, String contextPath, String endpointPath, Bundle bundle) {
        try {
            JAXRSServlet servlet = new JAXRSServlet(endpointPath, service, bundle);
            servlet.setSite(sites.findSiteByBundle(bundle));
            servlet.setEnvironment(environment);
            Dictionary<String, String> initParams = new Hashtable<String, String>();
            initParams.put(SharedHttpContext.ALIAS, contextPath);
            initParams.put(SharedHttpContext.SERVLET_NAME, service.toString());
            initParams.put(SharedHttpContext.CONTEXT_ID, SharedHttpContext.WEBLOUNGE_CONTEXT_ID);
            ServiceRegistration reg = bundleContext.registerService(Servlet.class.getName(), servlet, initParams);
            endpointRegistrations.put(contextPath, reg);
            endpointServlets.put(contextPath, servlet);
            logger.debug("Registering {} at {}", service, contextPath);
        } catch (Throwable t) {
            logger.error("Error registering rest service at " + contextPath, t);
            return;
        }
    }

    /**
     * Removes an endpoint from the OSGi http service.
     * 
     * @param contextPath
     *          The endpoint's url space
     */
    protected void unregisterEndpoint(String contextPath) {
        logger.debug("Unregistering rest endpoint {}", contextPath);

        // Remove the servlet from the http service
        try {
            ServiceRegistration reg = endpointRegistrations.get(contextPath);
            reg.unregister();
        } catch (IllegalStateException e) {
            // Never mind, the service has been unregistered already
        } catch (Throwable t) {
            logger.error("Unregistering endpoint at '{}' failed: {}", contextPath, t.getMessage());
        }

        // Unregister the servlet
        endpointRegistrations.remove(contextPath);
        endpointServlets.remove(contextPath);
    }

    /**
     * Implementation of a service listener which looks for services featuring
     * JSR311 <code>@Path</code> annotations.
     */
    class JSR311AnnotatedServiceListener implements ServiceListener {

        /**
         * {@inheritDoc}
         * 
         * @see org.osgi.framework.ServiceListener#serviceChanged(org.osgi.framework.ServiceEvent)
         */
        public void serviceChanged(ServiceEvent event) {
            ServiceReference ref = event.getServiceReference();
            Object service = null;

            try {
                service = bundleContext.getService(ref);
            } catch (IllegalStateException e) {
                // This is happening when the system is going down and the referenced
                // bundle context has already become invalid
                logger.debug("Endpoint publishing service is already down");
                return;
            }

            // Sometimes, there is a service reference without a service
            if (service == null)
                return;

            // Is this a JSR 311 annotated class?
            Path pathAnnotation = service.getClass().getAnnotation(Path.class);
            if (pathAnnotation == null)
                return;

            // Is there a context path?
            Object contextPathProperty = ref.getProperty(OPT_CONTEXTPATH);
            if (contextPathProperty == null)
                return;

            // Adjust relative context paths
            String contextPath = contextPathProperty.toString();
            if (!contextPath.startsWith("/")) {
                contextPath = UrlUtils.concat(defaultContextPathPrefix, contextPath);
            }

            // Find the registering bundle
            Bundle bundle = ref.getBundle();

            // Process the event
            switch (event.getType()) {
            case ServiceEvent.REGISTERED:
                logger.debug("Registering JAX-RS service {} at {}", service, contextPath);

                // Make sure there is no clash in context paths
                ServiceRegistration existingRef = endpointRegistrations.get(contextPath);
                if (existingRef != null) {
                    Object s = bundleContext.getService(existingRef.getReference());
                    logger.error(
                            "Endpoint {} cannot be registered since the context path {} has already been claimed",
                            new Object[] { service, s, contextPath });
                    return;
                }

                registerEndpoint(service, contextPath, pathAnnotation.value(), bundle);
                break;
            case ServiceEvent.MODIFIED:
                logger.debug("JAX-RS service {} modified", service);
                break;
            case ServiceEvent.UNREGISTERING:
                logger.debug("Unregistering JAX-RS service {} from {}", service, contextPath);
                unregisterEndpoint(contextPath);
                break;
            default:
                // We don't care about these cases
                break;
            }
        }

    }

    /**
     * OSGi callback to set the sites manager.
     * 
     * @param sites
     *          the sites manager
     */
    void setSites(SiteManager sites) {
        this.sites = sites;
    }

    /**
     * OSGi callback to set the environment.
     * 
     * @param environment
     *          the environment
     */
    void setEnvironment(Environment environment) {
        this.environment = environment;
    }

    /**
     * {@inheritDoc}
     * 
     * @see ch.entwine.weblounge.kernel.site.SiteServiceListener#siteAppeared(ch.entwine.weblounge.common.site.Site,
     *      org.osgi.framework.ServiceReference)
     */
    @Override
    public void siteAppeared(Site site, ServiceReference reference) {
        Bundle siteBundle = reference.getBundle();
        if (siteBundle == null)
            return;
        for (Map.Entry<String, JAXRSServlet> r : endpointServlets.entrySet()) {
            JAXRSServlet servlet = r.getValue();
            if (siteBundle.equals(servlet.getBundle())) {
                servlet.setSite(site);
            }
        }
    }

    /**
     * {@inheritDoc}
     * 
     * @see ch.entwine.weblounge.kernel.site.SiteServiceListener#siteDisappeared(ch.entwine.weblounge.common.site.Site)
     */
    @Override
    public void siteDisappeared(Site site) {
        // Nothing to do, the associated endpoints will soon be gone, too
    }

}