org.eclipse.gyrex.http.jetty.internal.app.ApplicationHandler.java Source code

Java tutorial

Introduction

Here is the source code for org.eclipse.gyrex.http.jetty.internal.app.ApplicationHandler.java

Source

/*******************************************************************************
 * Copyright (c) 2010, 2013 AGETO Service GmbH and others.
 * All rights reserved.
 *
 * This program and the accompanying materials are made available under the
 * terms of the Eclipse Public License v1.0 which accompanies this distribution,
 * and is available at http://www.eclipse.org/legal/epl-v10.html.
 *
 * Contributors:
 *     Gunnar Wagenknecht - initial API and implementation
 *******************************************************************************/
package org.eclipse.gyrex.http.jetty.internal.app;

import java.io.IOException;
import java.net.MalformedURLException;
import java.net.URL;
import java.util.EventListener;
import java.util.concurrent.CopyOnWriteArraySet;

import javax.servlet.DispatcherType;
import javax.servlet.Filter;
import javax.servlet.RequestDispatcher;
import javax.servlet.Servlet;
import javax.servlet.ServletContext;
import javax.servlet.ServletException;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;

import org.eclipse.gyrex.http.application.Application;
import org.eclipse.gyrex.http.application.context.IApplicationContext;
import org.eclipse.gyrex.http.internal.application.manager.ApplicationInstance;
import org.eclipse.gyrex.http.internal.application.manager.ApplicationRegistration;
import org.eclipse.gyrex.http.jetty.internal.JettyDebug;
import org.eclipse.gyrex.http.jetty.internal.JettyEngineApplication;
import org.eclipse.gyrex.server.Platform;

import org.eclipse.core.runtime.FileLocator;
import org.eclipse.jetty.http.PathMap;
import org.eclipse.jetty.http.PathMap.MappedEntry;
import org.eclipse.jetty.security.SecurityHandler;
import org.eclipse.jetty.server.Request;
import org.eclipse.jetty.server.handler.ContextHandler;
import org.eclipse.jetty.server.handler.ErrorHandler;
import org.eclipse.jetty.server.session.HashSessionManager;
import org.eclipse.jetty.server.session.SessionHandler;
import org.eclipse.jetty.servlet.ServletContextHandler;
import org.eclipse.jetty.util.StringUtil;
import org.eclipse.jetty.util.URIUtil;
import org.eclipse.jetty.util.resource.Resource;

import org.apache.commons.lang.StringUtils;
import org.apache.commons.lang.exception.ExceptionUtils;
import org.apache.commons.lang.math.NumberUtils;

import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

/**
 * The {@link Application} handler.
 * <p>
 * This is the main handler for an application. It provides the
 * {@link ServletContext} and implements context request handling.
 * </p>
 */
public class ApplicationHandler extends ServletContextHandler {

    /**
     * {@link ServletContext} implementation used by {@link ApplicationHandler}.
     */
    public class ApplicationServletContext extends Context {

        private <T> T createAndInject(final Class<T> c) throws ServletException {
            try {
                return getApplication().getContext().getInjector().make(c);
            } catch (final Exception | AssertionError | LinkageError e) {
                throw new ServletException(
                        String.format("Error injecting class '%s'. %s", c.getName(), e.getMessage()), e);
            }
        }

        @Override
        public <T extends Filter> T createFilter(final Class<T> c) throws ServletException {
            return createAndInject(c);
        }

        @Override
        public <T extends EventListener> T createListener(final Class<T> c) throws ServletException {
            return createAndInject(c);
        }

        @Override
        public <T extends Servlet> T createServlet(final Class<T> c) throws ServletException {
            return createAndInject(c);
        }

        @Override
        public ServletContext getContext(final String uripath) {
            // cross-application context access not supported
            if (Platform.inDebugMode()) {
                LOG.warn("Illegal cross-application access attempt from {} to {}.", getApplicationId(), uripath);
            }
            return null;
        }

        @Override
        public String getContextPath() {
            final String contextPath = getCurrentContextPath();
            if (URIUtil.SLASH.equals(contextPath))
                return EMPTY_STRING;
            return contextPath;
        }

        @Override
        public RequestDispatcher getNamedDispatcher(final String name) {
            // not supported
            return null;
        }

        @Override
        public String getRealPath(final String path) {
            // not supported
            return null;
        }

        @Override
        public Servlet getServlet(final String name) throws ServletException {
            // not supported
            return null;
        }
    }

    public static final String ATTRIBUTE_DEBUG_INFO = ApplicationHandler.class.getName().concat(".debugInfo");

    static final String EMPTY_STRING = "";

    private static final Logger LOG = LoggerFactory.getLogger(ApplicationHandler.class);

    private final ThreadLocal<String> currentContextPath = new ThreadLocal<>();
    private final ApplicationRegistration applicationRegistration;
    private final CopyOnWriteArraySet<String> urls = new CopyOnWriteArraySet<>();
    private final PathMap<ResourceProviderHolder> resourcesMap = new PathMap<>();
    private final boolean showDebugInfo = Platform.inDebugMode() || Platform.inDevelopmentMode();
    private final ApplicationHandlerMetrics metrics;

    private ApplicationContext applicationContext;
    private volatile ApplicationInstance applicationInstance;
    private ApplicationDelegateHandler applicationDelegateHandler;
    private SessionHandler sessionHandler;

    /**
     * Creates a new instance.
     * 
     * @param applicationId
     */
    public ApplicationHandler(final ApplicationRegistration applicationRegistration) {
        super();

        if (null == applicationRegistration)
            throw new IllegalArgumentException("application registration must not be null");

        metrics = new ApplicationHandlerMetrics(applicationRegistration.getApplicationId());
        JettyEngineApplication.registerMetrics(metrics);

        // use "slash" as default context path
        setContextPath(URIUtil.SLASH);

        // initialize context
        _scontext = new ApplicationServletContext();

        // initialize application registration
        this.applicationRegistration = applicationRegistration;

        // set init params
        getInitParams().putAll(applicationRegistration.getInitProperties());

        // set display name
        setDisplayName(applicationRegistration.getProviderId() + "@" + applicationRegistration.getApplicationId()
                + "@" + applicationRegistration.getContext().getContextPath());
    }

    /**
     * Adds a resource to the application.
     * 
     * @param pathSpec
     * @param resourceProviderHolder
     */
    public void addResource(final String pathSpec, final ResourceProviderHolder resourceProviderHolder) {
        resourcesMap.put(pathSpec, resourceProviderHolder);
    }

    public void addUrl(final String url) {
        urls.add(url);
    }

    private SessionHandler createSessionHandler() {
        // make sure the set a proper session inactive interval
        // otherwise Jetty will keep sessions open forever
        final HashSessionManager sessionManager = new HashSessionManager();
        sessionManager
                .setMaxInactiveInterval(NumberUtils.toInt(getInitParameter("session.maxInactiveInterval"), 1800));
        return new SessionHandler(sessionManager);
    }

    @Override
    public void destroy() {
        try {
            super.destroy();
        } finally {
            JettyEngineApplication.unregisterMetrics(metrics);
        }
    }

    @Override
    public void doScope(String target, final Request baseRequest, final HttpServletRequest request,
            final HttpServletResponse response) throws IOException, ServletException {

        /*
         * IMPLEMENTATION NOTE
         *
         * This method is overridden to customize the path handling.
         *
         * Normally, a ContextHandler has a context path. However, the ApplicationHandler
         * must inherit the context path from the calling ApplicationHandlerCollection because
         * it changes depending on the incoming url.
         *
         * Additionally, class loader handling has been disabled in order to rely on the Equinox default.
         */

        // note, we do not support requests going through different contexts
        final ContextHandler.Context origContext = baseRequest.getContext();
        if ((origContext != null) && (origContext != _scontext))
            throw new IllegalStateException(
                    "origContext != this context, nesting/cross-application routing not supported!");

        // backup paths
        final String origContextPath = baseRequest.getContextPath();
        final String origServletPath = baseRequest.getServletPath();
        final String origPathInfo = baseRequest.getPathInfo();

        // if the current context is not set, we need to set and restore
        final boolean newContext = (origContext == null) || (currentContextPath.get() == null);

        // set 'current' context path if not already set
        if (newContext) {
            // note, we rely on ApplicationHandlerCollection to pre-set the
            // correct context path on the request
            currentContextPath.set(baseRequest.getContextPath());
        }
        try {
            final String contextPath = currentContextPath.get();
            String pathInfo = null;

            final DispatcherType dispatch = baseRequest.getDispatcherType();

            // Are we already in this context?
            if (newContext) {
                // check the target
                if (DispatcherType.REQUEST.equals(dispatch) || DispatcherType.ASYNC.equals(dispatch)) {

                    // perform checkContext to support unavailable status
                    if (!checkContext(target, baseRequest, response))
                        return;

                    // only accept requests coming through ApplicationHandlerCollection
                    if (contextPath == null)
                        return;

                    // calculate paths
                    if (target.length() > contextPath.length()) {
                        if (contextPath.length() > 1) {
                            target = target.substring(contextPath.length());
                        }
                        pathInfo = target;
                    } else if (contextPath.length() == 1) {
                        target = URIUtil.SLASH;
                        pathInfo = URIUtil.SLASH;
                    } else {
                        // redirect null path infos in order to have context request end with /
                        baseRequest.setHandled(true);
                        if (baseRequest.getQueryString() != null) {
                            response.sendRedirect(URIUtil.addPaths(baseRequest.getRequestURI(), URIUtil.SLASH) + "?"
                                    + baseRequest.getQueryString());
                        } else {
                            response.sendRedirect(URIUtil.addPaths(baseRequest.getRequestURI(), URIUtil.SLASH));
                        }
                        return;
                    }
                }
            } else {
                // calculate paths for forwarded requests
                // (note, handle error dispatches like forwards for custom error handler support)
                if ((DispatcherType.FORWARD.equals(dispatch) || DispatcherType.ERROR.equals(dispatch))
                        && target.startsWith(URIUtil.SLASH)) {
                    pathInfo = target;
                }
            }

            // update the paths
            baseRequest.setContext(_scontext);
            if (!DispatcherType.INCLUDE.equals(dispatch) && target.startsWith(URIUtil.SLASH)) {
                if (contextPath.length() == 1) {
                    baseRequest.setContextPath(EMPTY_STRING);
                } else {
                    baseRequest.setContextPath(contextPath);
                }
                baseRequest.setServletPath(null);
                baseRequest.setPathInfo(pathInfo);
            }

            // set application handler debug info reference
            if (showDebugInfo) {
                final StringBuilder dump = new StringBuilder();
                dump(dump);
                baseRequest.setAttribute(ATTRIBUTE_DEBUG_INFO, dump.toString());
            }

            // next scope
            // start manual inline of nextScope(target,baseRequest,request,response);
            if (never()) {
                nextScope(target, baseRequest, request, response);
            } else if (_nextScope != null) {
                _nextScope.doScope(target, baseRequest, request, response);
            } else if (_outerScope != null) {
                _outerScope.doHandle(target, baseRequest, request, response);
            } else {
                doHandle(target, baseRequest, request, response);
            } // end manual inline (pathetic attempt to reduce stack depth)
        } finally {
            if (newContext) {
                // reset the context and servlet path
                baseRequest.setContext(origContext);
                baseRequest.setContextPath(origContextPath);
                baseRequest.setServletPath(origServletPath);
                baseRequest.setPathInfo(origPathInfo);
                currentContextPath.set(null);
            }
        }

    }

    @Override
    protected void doStart() throws Exception {
        try {
            super.doStart();
            metrics.setStatus("started", "Handler has been started by Jetty");
        } catch (final Exception e) {
            metrics.setStatus("error", ExceptionUtils.getMessage(e));
            throw e;
        }
    }

    @Override
    protected void doStop() throws Exception {
        try {
            // stop all the Jetty stuff
            super.doStop();
            metrics.setStatus("stopped", "Handler has been stopped by Jetty");
        } catch (final Exception e) {
            metrics.setStatus("error", ExceptionUtils.getMessage(e));
            throw e;
        } finally {
            // destroy the application instance
            final ApplicationContext context = applicationContext;
            if (null != context) {
                applicationContext = null;
                applicationInstance = null;
                applicationDelegateHandler = null;
                applicationRegistration.destroyApplication(context);
            }
            sessionHandler = null;
        }
    }

    @Override
    public void dump(final Appendable out, final String indent) throws IOException {
        super.dump(out, indent);
        out.append(indent).append(" |").append('\n');
        out.append(indent).append(" +- mounted urls").append('\n');
        for (final String url : urls) {
            out.append(indent).append(" | ").append(indent).append(" +- ").append(url).append('\n');
        }
    }

    /**
     * Returns the application
     * 
     * @return the application
     */
    public Application getApplication() {
        final ApplicationInstance instance = applicationInstance;
        final Application app = instance != null ? instance.getApplication() : null;
        if (app == null)
            throw new IllegalStateException("inactive");
        return app;
    }

    /**
     * Returns the application id.
     * 
     * @return the application id
     */
    public String getApplicationId() {
        return applicationRegistration.getApplicationId();
    }

    /**
     * Returns the application registration.
     * 
     * @return the application registration
     */
    public ApplicationRegistration getApplicationRegistration() {
        return applicationRegistration;
    }

    @Override
    public String getContextPath() {
        return getCurrentContextPath();
    }

    /**
     * Returns the current context path.
     * <p>
     * The path returned may vary depending on an active request. If no request
     * is active, i.e. if called outside a request scope, <code>null</code> will
     * be returned.
     * </p>
     * 
     * @return the context path (maybe <code>null</code> if unable to determine)
     */
    public String getCurrentContextPath() {
        return currentContextPath.get();
    }

    /**
     * Returns the handler that is responsible for routing requests to
     * {@link Application#handleRequest(HttpServletRequest, HttpServletResponse)}
     * .
     * 
     * @return the application delegate handler
     */
    public ApplicationDelegateHandler getDelegateHandler() {
        return applicationDelegateHandler;
    }

    public ApplicationHandlerMetrics getMetrics() {
        return metrics;
    }

    /**
     * Looks up and returns a resource for the specified path.
     * 
     * @param path
     * @return the found resource (maybe <code>null</code> if non is registered
     *         for the specified path)
     * @throws MalformedURLException
     */
    @Override
    public Resource getResource(String path) throws MalformedURLException {
        // data structure which maps a request to a resource provider; first-best match wins
        // { path =>  resource provider holder }
        if (resourcesMap.isEmpty() || (null == path) || !path.startsWith(URIUtil.SLASH))
            return null;

        path = URIUtil.canonicalPath(path);
        final MappedEntry<ResourceProviderHolder> entry = resourcesMap.getMatch(path);
        if (null == entry)
            return null;
        final ResourceProviderHolder provider = entry.getValue();
        if (null == provider)
            return null;

        final String pathSpec = entry.getKey();
        final String pathInfo = PathMap.pathInfo(pathSpec, path);
        final URL resourceUrl = provider.getResource(pathInfo);
        if (null == resourceUrl)
            return null;
        try {
            // resolve bundle/Eclipse URLs
            final URL fileURL = FileLocator.toFileURL(resourceUrl);
            return Resource.newResource(fileURL);
        } catch (final IOException e) {
            LOG.warn("Error resolving url {} to file based resource. {}", resourceUrl.toExternalForm(),
                    e.getMessage());
            return null;
        }
    }

    @Override
    public SecurityHandler getSecurityHandler() {
        // no security handler
        return null;
    }

    /**
     * Returns the servlet handler.
     * 
     * @return the servlet handler
     */
    @Override
    public ApplicationServletHandler getServletHandler() {
        // out servlet handler
        return (ApplicationServletHandler) _servletHandler;
    }

    @Override
    public SessionHandler getSessionHandler() {
        // our session handler
        return sessionHandler;
    }

    /**
     * Returns all mount urls.
     * 
     * @return the urls
     */
    public String[] getUrls() {
        return urls.toArray(new String[urls.size()]);
    }

    public boolean hasUrls() {
        return urls.size() > 0;
    }

    @Override
    public boolean isProtectedTarget(String target) {
        // support custom protected targets
        if (super.isProtectedTarget(target))
            return true;

        if (null == target)
            return false;

        while (target.startsWith("//")) {
            target = URIUtil.compactPath(target);
        }

        // protect certain OSGi constructs
        return StringUtil.startsWithIgnoreCase(target, "/web-inf")
                || StringUtil.startsWithIgnoreCase(target, "/meta-inf")
                || StringUtil.startsWithIgnoreCase(target, "/osgi-inf")
                || StringUtil.startsWithIgnoreCase(target, "/osgi-opt");
    }

    /**
     * Removes a resource from the application.
     * 
     * @param pathSpec
     */
    public void removeResource(final String pathSpec) {
        resourcesMap.remove(pathSpec);
    }

    public void removeUrl(final String url) {
        urls.remove(url);
    }

    @Override
    protected void startContext() throws Exception {
        if (JettyDebug.handlers) {
            LOG.debug("Starting {}", this);
        }

        // create the application context
        applicationContext = new ApplicationContext(this);

        // create servlet handler early
        _servletHandler = new ApplicationServletHandler(this);

        // create remaining the handlers
        applicationDelegateHandler = new ApplicationDelegateHandler(this);
        sessionHandler = createSessionHandler();

        // setup the handler chain before super initialization
        // session -> application -> registered servlets/resources
        setHandler(sessionHandler);
        sessionHandler.setHandler(applicationDelegateHandler);
        applicationDelegateHandler.setHandler(_servletHandler);

        // get the application instance now but initialize it later!
        // this is tricky, we need to defer this till the handlers have all
        // been initialized, initializing the application will trigger servlet registrations
        // this requires the ServletHandler to be setup properly; thus, we likely should
        // setup the handler chain first, start all the handlers and _then_ initialize
        // the application
        try {
            applicationInstance = applicationRegistration.getApplication(applicationContext);
            if (applicationInstance == null)
                throw new IllegalStateException("no application instance for " + applicationRegistration);
        } catch (final Exception e) {
            throw new IllegalStateException(
                    String.format("Error creating application '%s': %s",
                            StringUtils.trimToEmpty(getApplicationId()), StringUtils.trimToEmpty(e.getMessage())),
                    e);
        }

        // get application
        final Application application = applicationInstance.getApplication();
        if (application == null)
            throw new IllegalStateException(
                    "no application object returned from instance for " + applicationRegistration);

        // set important attributes
        setAttribute(IApplicationContext.SERVLET_CONTEXT_ATTRIBUTE_APPLICATION, application);
        setAttribute(IApplicationContext.SERVLET_CONTEXT_ATTRIBUTE_CONTEXT, application.getContext());

        // initialize custom error handler if available
        final Object errorHandler = application.getAdapter(ErrorHandler.class);
        if (errorHandler instanceof ErrorHandler) {
            setErrorHandler((ErrorHandler) errorHandler);
        }

        // perform super initialization
        super.startContext();

        // support welcome files
        setWelcomeFiles(new String[] { "index.jsp", "index.html", "index.htm" });

        // initialize the application
        applicationInstance.initialize();
        if (JettyDebug.handlers) {
            LOG.debug("Application {} initialized", application);
        }
    }

    @Override
    public String toString() {
        final StringBuilder toString = new StringBuilder();
        toString.append("ApplicationHandler@").append(Integer.toHexString(hashCode()));
        toString.append('(').append(applicationRegistration.getApplicationId()).append('[')
                .append(applicationRegistration.getProviderId()).append('@')
                .append(applicationRegistration.getContext().getContextPath()).append("])");
        return toString.toString();
    }
}