com.boylesoftware.web.Router.java Source code

Java tutorial

Introduction

Here is the source code for com.boylesoftware.web.Router.java

Source

/*
 * Copyright 2013 Boyle Software, Inc.
 *
 * Licensed under the Apache 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.apache.org/licenses/LICENSE-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 com.boylesoftware.web;

import java.io.IOException;
import java.io.UnsupportedEncodingException;
import java.net.URLEncoder;
import java.util.concurrent.ExecutorService;

import javax.servlet.DispatcherType;
import javax.servlet.ServletException;
import javax.servlet.ServletRequest;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;

import org.apache.commons.logging.Log;
import org.apache.commons.logging.LogFactory;

import com.boylesoftware.web.api.Attributes;
import com.boylesoftware.web.api.Authenticator;
import com.boylesoftware.web.spi.Route.SecurityMode;
import com.boylesoftware.web.spi.RouterRequest;
import com.boylesoftware.web.util.LooseCannon;
import com.boylesoftware.web.util.StringUtils;
import com.boylesoftware.web.util.pool.PooledStringBuffer;
import com.boylesoftware.web.util.pool.StringBufferPool;

/**
 * Routing logic implementation.
 *
 * @author Lev Himmelfarb
 */
class Router {

    /**
     * Name of request attribute used to pass exception from the asynchronous
     * transaction to the router.
     */
    private static final String ASYNC_EXCEPTION_ATTNAME = (Router.class).getName() + ".ASYNC_EXCEPTION";

    /**
     * Name of request attribute used to pass authenticated user to the router.
     */
    private static final String AUTHED_USER_ATTNAME = (Router.class).getName() + ".AUTHED_USER";

    /**
     * Plug for anonymous user.
     */
    private static final Object ANONYMOUS_USER = new Object();

    /**
     * The log.
     */
    private final Log log = LogFactory.getLog(this.getClass());

    /**
     * Set asynchronous transaction exception in the request.
     *
     * @param request The request.
     * @param e The exception.
     */
    static void setAsyncException(final ServletRequest request, final Exception e) {

        request.setAttribute(ASYNC_EXCEPTION_ATTNAME, e);
    }

    /**
     * Set authenticated user in the request.
     *
     * @param request The request.
     * @param authedUser The user, or {@code null} if the request is
     * unauthenticated.
     */
    static void setAuthenticatedUser(final HttpServletRequest request, final Object authedUser) {

        request.setAttribute(AUTHED_USER_ATTNAME, (authedUser != null ? authedUser : ANONYMOUS_USER));
    }

    /**
     * Route request.
     *
     * @param webapp The application.
     * @param request The HTTP request.
     * @param response The HTTP response.
     *
     * @return {@code true} if the router took care of the request processing
     * and no further action is required, {@code false} if the router did not
     * process the request because it does not have a route for it.
     *
     * @throws ServletException If an error happens.
     * @throws IOException If an I/O error happens reading the request or
     * sending the response.
     */
    boolean route(final AbstractWebApplication webapp, final HttpServletRequest request,
            final HttpServletResponse response) throws ServletException, IOException {

        if (this.log.isDebugEnabled())
            this.log.debug("received " + request.getDispatcherType() + " " + request.getMethod() + " "
                    + request.getRequestURI() + " (" + request.getClass().getName() + ")");

        // execute corresponding logic
        return (request.getDispatcherType() == DispatcherType.ASYNC ? this.doAsync(webapp, request, response)
                : this.doRequest(webapp, request, response));
    }

    /**
     * Process direct request.
     *
     * @param webapp The application.
     * @param request The request.
     * @param response The response.
     *
     * @return {@code true} if request processed.
     *
     * @throws ServletException If an error happens.
     * @throws IOException If an I/O error happens reading the request or
     * sending the response.
     */
    private boolean doRequest(final AbstractWebApplication webapp, final HttpServletRequest request,
            final HttpServletResponse response) throws ServletException, IOException {

        final boolean debug = this.log.isDebugEnabled();

        // find matching route
        RouterRequest q;
        try {
            q = webapp.getRouterConfiguration().findRoute(request, response);
            if (q == null) {
                if (debug)
                    this.log.debug("no route for request URI " + request.getRequestURI());
                return false;
            }
            if (debug)
                this.log.debug("found route for request URI " + request.getRequestURI());
        } catch (final MethodNotAllowedException e) {
            if (debug)
                this.log.debug("request method " + request.getMethod() + " is not allowed for request URI "
                        + request.getRequestURI(), e);
            response.sendError(e.getHTTPErrorCode());
            return true;
        }
        final RouterRequest routerReq = q;
        RouterRequestLifecycle.associate(routerReq);

        // process request
        boolean recycleReq = true;
        try {

            // check if SSL is required
            final SecurityMode securityMode = routerReq.getRoute().getSecurityMode();
            final boolean requireAuthedUser = ((securityMode == SecurityMode.FORCE_REQUIRE_AUTH)
                    || routerReq.isAuthenticationRequired());
            if ((requireAuthedUser || (securityMode == SecurityMode.FORCE_SSL)) && !routerReq.isSecure()) {
                if (debug)
                    this.log.debug("non-SSL request using mapping with" + " security mode " + securityMode
                            + " and require auth: " + requireAuthedUser);
                this.sendRedirectToSecureURI(webapp, routerReq, response);
                return true;
            }

            // fix request character encoding
            if (routerReq.getCharacterEncoding() == null)
                routerReq.setCharacterEncoding("UTF-8");

            // get authenticated user
            final AuthenticatorExecutor exec = AuthenticatorExecutor.getExecutor();
            boolean recycleExec = true;
            try {
                exec.init(webapp, routerReq);
                recycleReq = recycleExec = false;
                final ExecutorService execPool = webapp.getExecutorService();
                if (debug)
                    this.log.debug("executor service status: " + execPool);
                execPool.execute(exec);
            } finally {
                if (recycleExec)
                    exec.recycle();
            }

        } finally {
            if (recycleReq) {
                if (debug)
                    this.log.debug("recycling router request " + routerReq);
                RouterRequestLifecycle.recycle(routerReq);
            }
        }

        // done
        return true;
    }

    /**
     * Process asynchronous dispatch.
     *
     * @param webapp The application.
     * @param request The request.
     * @param response The response.
     *
     * @return {@code true} if request processed.
     *
     * @throws ServletException If an error happens.
     * @throws IOException If an I/O error happens reading the request or
     * sending the response.
     */
    private boolean doAsync(final AbstractWebApplication webapp, final HttpServletRequest request,
            final HttpServletResponse response) throws ServletException, IOException {

        final boolean debug = this.log.isDebugEnabled();

        // check if asynchronous executor error
        final Exception asyncError = (Exception) request.getAttribute(ASYNC_EXCEPTION_ATTNAME);
        if (asyncError != null) {
            if (debug)
                this.log.debug("received error from async transaction", asyncError);
            if (asyncError instanceof RequestedResourceException) {
                if (asyncError instanceof ServiceUnavailableException)
                    this.log.error("service unavailable error", asyncError);
                response.sendError(((RequestedResourceException) asyncError).getHTTPErrorCode());
                return true;
            }
            if (asyncError instanceof ServletException)
                throw (ServletException) asyncError;
            if (asyncError instanceof IOException)
                throw (IOException) asyncError;
            if (asyncError instanceof RuntimeException)
                throw (RuntimeException) asyncError;
            throw new ServletException(asyncError);
        }

        // restore the route
        final RouterRequest routerReq = RouterRequestLifecycle.restore(request);
        if (routerReq == null) {
            if (debug)
                this.log.debug("request is not a router request");
            return false;
        }

        // re-wrap the router request
        routerReq.rewrap(request, response);

        // check if route requires authenticated user
        final Object authedUser = routerReq.getAttribute(AUTHED_USER_ATTNAME);
        if ((authedUser != null) && (authedUser != ANONYMOUS_USER)) {
            routerReq.setAttribute(Attributes.AUTHED_USER, authedUser);
        } else if ((routerReq.getRoute().getSecurityMode() == SecurityMode.FORCE_REQUIRE_AUTH)
                || routerReq.isAuthenticationRequired()) {
            this.sendRequestForAuthentication(webapp, routerReq, response);
            return true;
        }

        // execute request processing logic
        final RequestTransactionExecutor exec = RequestTransactionExecutor.getExecutor();
        boolean recycleExec = true;
        try {
            if (exec.prepare(webapp, routerReq)) {
                recycleExec = false;
                final ExecutorService execPool = webapp.getExecutorService();
                if (debug)
                    this.log.debug("executor service status: " + execPool);
                execPool.execute(exec);
            }
        } finally {
            if (recycleExec)
                exec.recycle();
        }

        // done
        return true;
    }

    /**
     * Send request for user authentication, which can be a redirect to the
     * login page, or the 401 HTTP code.
     *
     * @param webapp The web-application.
     * @param request The HTTP request.
     * @param response The HTTP response.
     *
     * @throws ServletException If an error happens.
     */
    private void sendRequestForAuthentication(final AbstractWebApplication webapp, final HttpServletRequest request,
            final HttpServletResponse response) throws ServletException {

        LooseCannon.heel();

        final String loginPageURI = webapp.getRouterConfiguration().getLoginPageURI();
        if (loginPageURI != null) {

            try (final PooledStringBuffer buf = StringBufferPool.get()) {
                final StringBuilder sb = buf.getStringBuilder();

                sb.append(request.getRequestURI());
                final String queryString = request.getQueryString();
                if (queryString != null)
                    sb.append('?').append(queryString);
                final String targetURI = sb.toString();
                sb.setLength(0);

                sb.append("https://").append(request.getServerName());

                final int httpsPort = webapp.getHTTPSPort();
                if (httpsPort != 443)
                    sb.append(':').append(httpsPort);

                sb.append(StringUtils.emptyIfNull(request.getContextPath())).append(loginPageURI).append('?')
                        .append(Authenticator.TARGET_URI).append('=');

                try {
                    sb.append(URLEncoder.encode(targetURI, "UTF-8"));
                } catch (final UnsupportedEncodingException e) {
                    throw new ServletException("UTF-8 is unsupported.", e);
                }

                response.setStatus(HttpServletResponse.SC_SEE_OTHER);
                response.setHeader("Location", sb.toString());
            }

        } else {
            response.setStatus(HttpServletResponse.SC_UNAUTHORIZED);
            // TODO: configure realm in the application configuration
            response.setHeader("WWW-Authenticate", "Basic realm=\"Application\"");
        }
    }

    /**
     * Send redirect to the same request URI (including the query string), but
     * over HTTPS.
     *
     * @param webapp The web-application.
     * @param request The HTTP request.
     * @param response The HTTP response.
     */
    private void sendRedirectToSecureURI(final AbstractWebApplication webapp, final HttpServletRequest request,
            final HttpServletResponse response) {

        LooseCannon.heel();

        try (final PooledStringBuffer buf = StringBufferPool.get()) {
            final StringBuilder redirectURL = buf.getStringBuilder();

            redirectURL.append("https://").append(request.getServerName());

            final int httpsPort = webapp.getHTTPSPort();
            if (httpsPort != 443)
                redirectURL.append(':').append(httpsPort);

            redirectURL.append(request.getRequestURI());

            final String queryString = request.getQueryString();
            if (queryString != null)
                redirectURL.append('?').append(queryString);

            response.setStatus(HttpServletResponse.SC_MOVED_PERMANENTLY);
            response.setHeader("Location", redirectURL.toString());
        }
    }
}