org.opencastproject.kernel.rest.RestPublisher.java Source code

Java tutorial

Introduction

Here is the source code for org.opencastproject.kernel.rest.RestPublisher.java

Source

/**
 *  Copyright 2009, 2010 The Regents of the University of California
 *  Licensed under the Educational Community License, Version 2.0
 *  (the "License"); you may not use this file except in compliance
 *  with the License. You may obtain a copy of the License at
 *
 *  http://www.osedu.org/licenses/ECL-2.0
 *
 *  Unless required by applicable law or agreed to in writing,
 *  software distributed under the License is distributed on an "AS IS"
 *  BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express
 *  or implied. See the License for the specific language governing
 *  permissions and limitations under the License.
 *
 */
package org.opencastproject.kernel.rest;

import org.opencastproject.rest.RestConstants;
import org.opencastproject.rest.StaticResource;
import org.opencastproject.security.api.UnauthorizedException;
import org.opencastproject.util.NotFoundException;

import org.apache.commons.httpclient.HttpStatus;
import org.apache.cxf.Bus;
import org.apache.cxf.jaxrs.JAXRSServerFactoryBean;
import org.apache.cxf.jaxrs.ext.RequestHandler;
import org.apache.cxf.jaxrs.lifecycle.SingletonResourceProvider;
import org.apache.cxf.jaxrs.model.ClassResourceInfo;
import org.apache.cxf.jaxrs.provider.JSONProvider;
import org.apache.cxf.message.Message;
import org.apache.cxf.transport.servlet.CXFNonSpringServlet;
import org.codehaus.jettison.mapped.Configuration;
import org.codehaus.jettison.mapped.MappedNamespaceConvention;
import org.codehaus.jettison.mapped.MappedXMLStreamWriter;
import org.osgi.framework.Bundle;
import org.osgi.framework.BundleContext;
import org.osgi.framework.BundleEvent;
import org.osgi.framework.InvalidSyntaxException;
import org.osgi.framework.ServiceReference;
import org.osgi.framework.ServiceRegistration;
import org.osgi.service.component.ComponentContext;
import org.osgi.util.tracker.BundleTracker;
import org.osgi.util.tracker.ServiceTracker;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

import java.io.OutputStream;
import java.io.OutputStreamWriter;
import java.lang.reflect.Type;
import java.net.URL;
import java.nio.charset.Charset;
import java.util.ArrayList;
import java.util.Dictionary;
import java.util.Hashtable;
import java.util.List;
import java.util.Map;
import java.util.concurrent.ConcurrentHashMap;

import javax.servlet.Servlet;
import javax.servlet.ServletConfig;
import javax.servlet.ServletException;
import javax.ws.rs.Path;
import javax.ws.rs.core.MediaType;
import javax.ws.rs.core.Response;
import javax.ws.rs.core.Response.Status;
import javax.ws.rs.ext.ExceptionMapper;
import javax.xml.stream.XMLStreamException;
import javax.xml.stream.XMLStreamWriter;

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

    /** The logger **/
    protected static final Logger logger = LoggerFactory.getLogger(RestPublisher.class);

    /** The rest publisher looks for any non-servlet with the 'opencast.service.path' property */
    public static final String JAX_RS_SERVICE_FILTER = "(&(!(objectClass=javax.servlet.Servlet))("
            + SERVICE_PATH_PROPERTY + "=*))";

    /** A map that sets default xml namespaces in {@link XMLStreamWriter}s */
    protected static final ConcurrentHashMap<String, String> NAMESPACE_MAP;

    /** The 404 Error page */
    protected String fourOhFour = null;

    @SuppressWarnings("unchecked")
    protected List providers = null;

    static {
        NAMESPACE_MAP = new ConcurrentHashMap<String, String>();
        NAMESPACE_MAP.put("http://www.w3.org/2001/XMLSchema-instance", "");
    }

    /** The rest publisher's OSGI declarative services component context */
    protected ComponentContext componentContext;

    /** A service tracker that monitors JAX-RS annotated services, (un)publishing servlets as they (dis)appear */
    protected ServiceTracker jaxRsTracker = null;

    /**
     * A bundle tracker that registers StaticResource servlets for bundles with the right headers.
     */
    protected BundleTracker bundleTracker = null;

    /** The base URL for this server */
    protected String baseServerUri;

    /** Holds references to servlets that this class publishes, so they can be unpublished later */
    protected Map<String, ServiceRegistration> servletRegistrationMap;

    /** Activates this rest publisher */
    @SuppressWarnings("unchecked")
    protected void activate(ComponentContext componentContext) {
        logger.debug("activate()");
        this.baseServerUri = componentContext.getBundleContext().getProperty("org.opencastproject.server.url");
        this.componentContext = componentContext;
        this.fourOhFour = "The resource you requested does not exist."; // TODO: Replace this with something a little nicer
        this.servletRegistrationMap = new ConcurrentHashMap<String, ServiceRegistration>();
        this.providers = new ArrayList();

        JSONProvider jsonProvider = new MatterhornJSONProvider();
        jsonProvider.setIgnoreNamespaces(true);
        jsonProvider.setNamespaceMap(NAMESPACE_MAP);

        providers.add(jsonProvider);
        providers.add(new ExceptionMapper<NotFoundException>() {
            public Response toResponse(NotFoundException e) {
                return Response.status(404).entity(fourOhFour).type(MediaType.TEXT_PLAIN).build();
            }
        });
        providers.add(new ExceptionMapper<UnauthorizedException>() {
            public Response toResponse(UnauthorizedException e) {
                return Response.status(HttpStatus.SC_UNAUTHORIZED).entity("unauthorized").type(MediaType.TEXT_PLAIN)
                        .build();
            };
        });
        providers.add(new RestDocRedirector());

        try {
            jaxRsTracker = new JaxRsServiceTracker();
            bundleTracker = new StaticResourceBundleTracker(componentContext.getBundleContext());
        } catch (InvalidSyntaxException e) {
            throw new IllegalStateException(e);
        }
        jaxRsTracker.open();
        bundleTracker.open();
    }

    /**
     * Deactivates the rest publisher
     */
    protected void deactivate() {
        logger.debug("deactivate()");
        jaxRsTracker.close();
        bundleTracker.close();
    }

    /**
     * Creates a REST endpoint for the JAX-RS annotated service.
     * 
     * @param ref
     *          the osgi service reference
     * @param service
     *          The service itself
     */
    @SuppressWarnings("unchecked")
    protected void createEndpoint(ServiceReference ref, Object service) {
        RestServlet cxf = new RestServlet();
        ServiceRegistration reg = null;
        String serviceType = (String) ref.getProperty(SERVICE_TYPE_PROPERTY);
        String servicePath = (String) ref.getProperty(SERVICE_PATH_PROPERTY);
        boolean jobProducer = Boolean.parseBoolean((String) ref.getProperty(SERVICE_JOBPRODUCER_PROPERTY));
        try {
            Dictionary<String, Object> props = new Hashtable<String, Object>();
            props.put("contextId", RestConstants.HTTP_CONTEXT_ID);
            props.put("alias", servicePath);
            props.put(SERVICE_TYPE_PROPERTY, serviceType);
            props.put(SERVICE_PATH_PROPERTY, servicePath);
            props.put(SERVICE_JOBPRODUCER_PROPERTY, jobProducer);
            reg = componentContext.getBundleContext().registerService(Servlet.class.getName(), cxf, props);
        } catch (Exception e) {
            logger.info("Problem registering REST endpoint {} : {}", servicePath, e.getMessage());
            return;
        }
        servletRegistrationMap.put(servicePath, reg);

        // Wait for the servlet to be initialized. Since the servlet is published via the whiteboard, this may happen
        // asynchronously
        while (!cxf.isInitialized()) {
            logger.debug("Waiting for the servlet at '{}' to be initialized", servicePath);
            try {
                Thread.sleep(100);
            } catch (InterruptedException e) {
                logger.warn("Interrupt while waiting for RestServlet initialization");
                break;
            }
        }

        // Set up cxf
        Bus bus = cxf.getBus();
        JAXRSServerFactoryBean factory = new JAXRSServerFactoryBean();
        factory.setBus(bus);
        factory.setProviders(providers);

        // Set the service class
        factory.setServiceClass(service.getClass());
        factory.setResourceProvider(service.getClass(), new SingletonResourceProvider(service));

        // Set the address to '/', which will force the use of the http service
        factory.setAddress("/");

        // Use the cxf classloader itself to create the cxf server
        ClassLoader bundleClassLoader = Thread.currentThread().getContextClassLoader();
        ClassLoader delegateClassLoader = JAXRSServerFactoryBean.class.getClassLoader();
        try {
            Thread.currentThread().setContextClassLoader(delegateClassLoader);
            factory.create();
        } finally {
            Thread.currentThread().setContextClassLoader(bundleClassLoader);
        }
        logger.info("Registered REST endpoint at " + servicePath);
    }

    /**
     * Removes an endpoint
     * 
     * @param alias
     *          The URL space to reclaim
     */
    protected void destroyEndpoint(String alias) {
        ServiceRegistration reg = servletRegistrationMap.remove(alias);
        if (reg != null) {
            reg.unregister();
        }
    }

    /**
     * Extends the CXF JSONProvider for the grand purpose of removing '@' symbols from json and padded jsonp.
     */
    protected static class MatterhornJSONProvider extends JSONProvider {
        private static final Charset UTF8 = Charset.forName("utf-8");

        /**
         * {@inheritDoc}
         * 
         * @see org.apache.cxf.jaxrs.provider.JSONProvider#createWriter(java.lang.Object, java.lang.Class,
         *      java.lang.reflect.Type, java.lang.String, java.io.OutputStream, boolean)
         */
        protected XMLStreamWriter createWriter(Object actualObject, Class<?> actualClass, Type genericType,
                String enc, OutputStream os, boolean isCollection) throws Exception {
            Configuration c = new Configuration(NAMESPACE_MAP);
            c.setSupressAtAttributes(true);
            MappedNamespaceConvention convention = new MappedNamespaceConvention(c);
            return new MappedXMLStreamWriter(convention, new OutputStreamWriter(os, UTF8)) {
                @Override
                public void writeStartElement(String prefix, String local, String uri) throws XMLStreamException {
                    super.writeStartElement("", local, "");
                }

                @Override
                public void writeStartElement(String uri, String local) throws XMLStreamException {
                    super.writeStartElement("", local, "");
                }

                @Override
                public void setPrefix(String pfx, String uri) throws XMLStreamException {
                }

                @Override
                public void setDefaultNamespace(String uri) throws XMLStreamException {
                }
            };
        }
    }

    /**
     * A custom ServiceTracker that published JAX-RS annotated services with the {@link RestPublisher#SERVICE_PROPERTY}
     * property set to some non-null value.
     */
    public class JaxRsServiceTracker extends ServiceTracker {

        JaxRsServiceTracker() throws InvalidSyntaxException {
            super(componentContext.getBundleContext(),
                    componentContext.getBundleContext().createFilter(JAX_RS_SERVICE_FILTER), null);
        }

        @Override
        public void removedService(ServiceReference reference, Object service) {
            String servicePath = (String) reference.getProperty(SERVICE_PATH_PROPERTY);
            destroyEndpoint(servicePath);
            super.removedService(reference, service);
        }

        @Override
        public Object addingService(ServiceReference reference) {
            Object service = componentContext.getBundleContext().getService(reference);
            if (service == null) {
                logger.info(
                        "JAX-RS service {} has not been instantiated yet, or has already been unregistered. Skipping "
                                + "endpoint creation.",
                        reference);
            } else {
                Path pathAnnotation = service.getClass().getAnnotation(Path.class);
                if (pathAnnotation == null) {
                    logger.warn(
                            "{} was registered with '{}={}', but the service is not annotated with the JAX-RS "
                                    + "@Path annotation",
                            new Object[] { service, SERVICE_PATH_PROPERTY,
                                    reference.getProperty(SERVICE_PATH_PROPERTY) });
                } else {
                    createEndpoint(reference, service);
                }
            }
            return super.addingService(reference);
        }
    }

    /**
     * A classloader that delegates to an OSGI bundle for loading resources.
     */
    class StaticResourceClassLoader extends ClassLoader {
        private Bundle bundle = null;

        public StaticResourceClassLoader(Bundle bundle) {
            super();
            this.bundle = bundle;
        }

        @Override
        public URL getResource(String name) {
            URL url = bundle.getResource(name);
            logger.debug("{} found resource {} from name {}", new Object[] { this, url, name });
            return url;
        }
    }

    /**
     * Tracks bundles containing static resources to be exposed via HTTP URLs.
     */
    class StaticResourceBundleTracker extends BundleTracker {

        /**
         * Creates a new StaticResourceBundleTracker.
         * 
         * @param context
         *          the bundle context
         */
        StaticResourceBundleTracker(BundleContext context) {
            super(context, Bundle.ACTIVE, null);
        }

        /**
         * {@inheritDoc}
         * 
         * @see org.osgi.util.tracker.BundleTracker#addingBundle(org.osgi.framework.Bundle, org.osgi.framework.BundleEvent)
         */
        @Override
        public Object addingBundle(Bundle bundle, BundleEvent event) {
            String classpath = (String) bundle.getHeaders().get(RestConstants.HTTP_CLASSPATH);
            String alias = (String) bundle.getHeaders().get(RestConstants.HTTP_ALIAS);
            String welcomeFile = (String) bundle.getHeaders().get(RestConstants.HTTP_WELCOME);

            if (classpath != null && alias != null) {
                Dictionary<String, String> props = new Hashtable<String, String>();
                props.put("alias", alias);
                props.put("contextId", RestConstants.HTTP_CONTEXT_ID);

                StaticResource servlet = new StaticResource(new StaticResourceClassLoader(bundle), classpath, alias,
                        welcomeFile);

                // We use the newly added bundle's context to register this service, so when that bundle shuts down, it brings
                // down this servlet with it
                logger.debug("Registering servlet with alias {}", alias);
                bundle.getBundleContext().registerService(Servlet.class.getName(), servlet, props);
            }

            return super.addingBundle(bundle, event);
        }
    }

    /**
     * An HttpServlet that uses a JAX-RS service to handle requests.
     */
    public class RestServlet extends CXFNonSpringServlet {
        /** Serialization UID */
        private static final long serialVersionUID = -8963338160276371426L;

        /** Whether this servlet has been initialized by the http service */
        private boolean initialized = false;

        /**
         * Whether the http service has initialized this servlet.
         * 
         * @return the initialization state
         */
        public boolean isInitialized() {
            return initialized;
        }

        @Override
        public void init(ServletConfig servletConfig) throws ServletException {
            super.init(servletConfig);
            initialized = true;
        }
    }

    public class RestDocRedirector implements RequestHandler {
        /**
         * {@inheritDoc}
         * 
         * @see org.apache.cxf.jaxrs.ext.RequestHandler#handleRequest(org.apache.cxf.message.Message,
         *      org.apache.cxf.jaxrs.model.ClassResourceInfo)
         */
        @Override
        public Response handleRequest(Message m, ClassResourceInfo resourceClass) {
            String uri = (String) m.get(Message.REQUEST_URI);
            if (uri.endsWith("/docs")) {
                String[] pathSegments = uri.split("/");
                String path = "";
                for (int i = 1; i < pathSegments.length - 1; i++) {
                    path += "/" + pathSegments[i].replace("/", "");
                }
                return Response.status(Status.MOVED_PERMANENTLY).type(MediaType.TEXT_PLAIN)
                        .header("Location", "/docs.html?path=" + path).build();
            }
            return null;
        }
    }
}