org.gluewine.rest_server.RESTServlet.java Source code

Java tutorial

Introduction

Here is the source code for org.gluewine.rest_server.RESTServlet.java

Source

/**************************************************************************
 *
 * Gluewine REST Server Module
 *
 * Copyright (C) 2013 FKS bvba               http://www.fks.be/
 *
 * 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 org.gluewine.rest_server;

import java.io.File;
import java.io.IOException;
import java.io.InputStream;
import java.io.OutputStream;
import java.lang.reflect.InvocationTargetException;
import java.lang.reflect.Method;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.List;
import java.util.Map;

import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;

import org.apache.commons.fileupload.FileItem;
import org.apache.commons.fileupload.FileItemFactory;
import org.apache.commons.fileupload.FileUploadException;
import org.apache.commons.fileupload.disk.DiskFileItemFactory;
import org.apache.commons.fileupload.servlet.ServletFileUpload;
import org.apache.commons.io.IOUtils;
import org.apache.log4j.Logger;
import org.gluewine.authentication.AuthenticationException;
import org.gluewine.core.RepositoryListener;
import org.gluewine.core.ContextInitializer;
import org.gluewine.jetty.GluewineServlet;
import org.gluewine.rest.REST;
import org.gluewine.rest.RESTID;
import org.gluewine.rest.RESTMethod;
import org.gluewine.rest.RESTSerializer;
import org.gluewine.sessions.SessionManager;
import org.gluewine.sessions.Unsecured;
import org.gluewine.utils.AnnotationUtility;
import org.gluewine.utils.ErrorLogger;

/**
 * Handles all REST requests, and dispatches them to the correct objects.
 *
 * @author fks/Serge de Schaetzen
 *
 */
public class RESTServlet extends GluewineServlet implements RepositoryListener<Object> {
    // ===========================================================================
    /**
     * The serial uid.
     */
    private static final long serialVersionUID = -5416068629723455281L;

    /**
     * The map of all registered serializers.
     */
    private Map<String, RESTSerializer> serializers = new HashMap<String, RESTSerializer>();

    /**
     * Map of registerd, annotated, methods.
     */
    private Map<String, RESTMethod> methods = new HashMap<String, RESTMethod>();

    /**
     * The list of available authenticators.
     */
    private List<RESTAuthenticator> authenticators = new ArrayList<RESTAuthenticator>();

    /**
     * The session manager to use.
     */
    private SessionManager sessionManager;

    /**
     * The logger instance to use.
     */
    private Logger logger = Logger.getLogger(getClass());

    /**
     * Bean holding the current request and response.
     */
    private static class RESTBean {
        /**
         * The current request.
         */
        private HttpServletRequest request;
        /**
         * The current response.
         */
        private HttpServletResponse response;
    }

    /**
     * The map of beans bound with the current thread.
     */
    private static Map<Thread, RESTBean> beans = new HashMap<Thread, RESTBean>();

    // ===========================================================================
    @Override
    public String getContextPath() {
        return "REST";
    }

    // ===========================================================================
    /**
     * Returns the JSon context parsed from the request uri.
     *
     * @param req The request to process.
     * @return The json context.
     */
    private String getRESTPath(HttpServletRequest req) {
        String base = "/REST/";

        // Check the uri:
        String uri = req.getRequestURI();
        if (uri.length() > base.length()) {
            String path = uri.substring(base.length());
            int i = path.indexOf('?');
            if (i > -1)
                path = path.substring(0, i);
            if (path.endsWith("/"))
                path = path.substring(0, path.length() - 1);
            return path;
        } else
            return "";
    }

    // ===========================================================================
    /**
     * Performs an authentication. If all available authenticators fail to authenticate
     * the request, a 401 (unauthorized) is send with a header WWW-Authenticate set to
     * Basic.
     *
     * @param req The current request.
     * @param resp The current response.
     * @param service The service where the method is called.
     * @throws AuthenticationException Throw if none of the available authenticators succeeded.
     */
    private void authenticate(HttpServletRequest req, HttpServletResponse resp, Object service)
            throws AuthenticationException {
        req.setAttribute(RESTAuthenticator.CALLED_SERVICE, service);

        for (RESTAuthenticator auth : authenticators) {
            try {
                String session = auth.authenticate(req, resp);
                if (sessionManager != null)
                    sessionManager.setCurrentSessionId(session);
                return;
            } catch (AuthenticationException e) {
                // Ignore, as we will check ALL available authenticators.
            }
        }
        if (authenticators.size() > 0)
            throw new AuthenticationException("Authentication Required");
    }

    // ===========================================================================
    /**
     * Parses the form stored in the given request, and returns a map containing
     * all FileItems indexed on their name.
     *
     * @param req The request to parse.
     * @return The map of items.
     * @throws IOException If an error occurs.
     */
    private Map<String, FileItem> parseForm(HttpServletRequest req) throws IOException {
        Map<String, FileItem> formFields = new HashMap<String, FileItem>();
        try {
            FileItemFactory factory = new DiskFileItemFactory();
            ServletFileUpload upload = new ServletFileUpload(factory);
            @SuppressWarnings("unchecked")
            List<FileItem> items = upload.parseRequest(req);
            for (FileItem item : items)
                formFields.put(item.getFieldName(), item);
        } catch (FileUploadException e) {
            throw new IOException(e.getMessage());
        }

        return formFields;
    }

    // ===========================================================================
    /**
     * Initialises the parameter values for the given method parsed from the request.
     *
     * @param rm The method to process.
     * @param req The request containing the parameters.
     * @param resp The servlet response.
     * @param params the parameter value array to fill in.
     * @param paramTypes the parameter types array.
     * @throws IOException If an error occurs.
     */
    private void initParamValues(RESTMethod rm, HttpServletRequest req, HttpServletResponse resp, Object[] params,
            Class<?>[] paramTypes) throws IOException {
        for (int i = 0; i < params.length; i++) {
            params[i] = null;
            RESTID id = AnnotationUtility.getAnnotations(RESTID.class, rm.getObject(), rm.getMethod(), i);
            if (id != null) {
                if (id.header()) {
                    params[i] = req.getHeader(id.id());
                } else if (id.method()) {
                    params[i] = req.getMethod();
                }
            } else if (HttpServletResponse.class.isAssignableFrom(paramTypes[i])) {
                params[i] = resp;
            } else if (HttpServletRequest.class.isAssignableFrom(paramTypes[i])) {
                params[i] = req;
            }
        }
    }

    /**
     * Returns the parameter values for the given method parsed from the request.
     *
     * @param rm The method to process.
     * @param req The request containing the parameters.
     * @param serializer The serializer to use.
     * @param params the parameter value array to fill in.
     * @param paramTypes the parameter types array.
     * @throws IOException If an error occurs.
     */
    private void fillParamValuesFromRequest(RESTMethod rm, HttpServletRequest req, RESTSerializer serializer,
            Object[] params, Class<?>[] paramTypes) throws IOException {
        for (int i = 0; i < params.length; i++) {
            if (params[i] != null)
                continue;

            RESTID id = AnnotationUtility.getAnnotations(RESTID.class, rm.getObject(), rm.getMethod(), i);
            if (id != null) {
                String[] val;
                if (id.body()) {
                    val = new String[1];
                    if (InputStream.class.isAssignableFrom(paramTypes[i])) {
                        params[i] = req.getInputStream();
                    } else {
                        byte[] bytes = IOUtils.toByteArray(req.getInputStream());
                        val[0] = new String(bytes, "UTF-8");
                    }
                } else {
                    val = req.getParameterValues(id.id());
                }
                if (logger.isTraceEnabled())
                    traceParameter(id.id(), val);
                if (params[i] == null && val != null && val.length > 0)
                    params[i] = serializer.deserialize(paramTypes[i], val);
            }
        }
    }

    // ===========================================================================
    /**
     * Parses the parameters from the form stored in the given map.
     *
     * @param rm The method to process.
     * @param formFields The fields to parse.
     * @param serializer The serializer to use.
     * @param params the parameter value array to fill in.
     * @param paramTypes the parameter types array.
     * @throws IOException If an error occurs.
     */
    private void fillParamValuesFromForm(RESTMethod rm, Map<String, FileItem> formFields, RESTSerializer serializer,
            Object[] params, Class<?>[] paramTypes) throws IOException {
        for (int i = 0; i < params.length; i++) {
            if (params[i] != null)
                continue;

            RESTID id = AnnotationUtility.getAnnotations(RESTID.class, rm.getObject(), rm.getMethod(), i);
            if (id != null) {
                FileItem item = formFields.get(id.id());
                if (item != null) {
                    String[] val = null;
                    if (item.isFormField()) {
                        val = new String[] { item.getString("UTF-8") };
                        params[i] = serializer.deserialize(paramTypes[i], val);
                        if (logger.isTraceEnabled())
                            traceParameter(id.id(), val);
                    } else {
                        if (id.mimetype()) {
                            params[i] = item.getContentType();
                        } else if (id.filename()) {
                            params[i] = item.getName();
                        } else {
                            try {
                                if (InputStream.class.isAssignableFrom(paramTypes[i])) {
                                    params[i] = item.getInputStream();
                                } else {
                                    File nf = new File(item.getName());
                                    File f = File.createTempFile("___", "___" + nf.getName());
                                    item.write(f);
                                    if (File.class.isAssignableFrom(paramTypes[i])) {
                                        params[i] = f;
                                    } else {
                                        logger.warn("File upload to string field for method " + rm.getMethod());
                                        params[i] = f.getAbsolutePath();
                                    }
                                }
                            } catch (Exception e) {
                                throw new IOException(e.getMessage());
                            }
                        }
                    }
                }
            }
        }
    }

    // ===========================================================================
    @Override
    public void service(HttpServletRequest req, HttpServletResponse resp) throws IOException {
        try {
            RESTBean rb = new RESTBean();
            rb.request = req;
            rb.response = resp;
            beans.put(Thread.currentThread(), rb);

            String path = getRESTPath(req);
            if (logger.isDebugEnabled())
                logger.debug("Received REST request for : " + path);
            if (methods.containsKey(path)) {
                RESTMethod rm = methods.get(path);
                Map<String, FileItem> formFields = null;
                if (rm.isForm())
                    formFields = parseForm(req);

                String format = null;
                if (rm.isForm()) {
                    FileItem item = formFields.get("format");
                    if (item != null)
                        format = item.getString("UTF-8");
                } else
                    format = req.getParameter("format");

                if (format == null)
                    format = "json";
                RESTSerializer serializer = serializers.get(format);
                if (serializer != null) {

                    if (AnnotationUtility.getAnnotation(Unsecured.class, rm.getMethod(), rm.getObject()) == null)
                        authenticate(req, resp, rm.getObject());

                    Class<?>[] paramTypes = rm.getMethod().getParameterTypes();
                    Object[] params = new Object[paramTypes.length];

                    initParamValues(rm, req, resp, params, paramTypes);

                    if (rm.isForm())
                        fillParamValuesFromForm(rm, formFields, serializer, params, paramTypes);
                    else
                        fillParamValuesFromRequest(rm, req, serializer, params, paramTypes);

                    try {
                        if (rm.getMethod().getReturnType().equals(Void.TYPE)
                                || rm.getMethod().getReturnType().equals(InputStream.class)
                                || rm.getMethod().getReturnType().equals(OutputStream.class))
                            executeMethod(rm, params);

                        else {
                            String s = executeMethod(rm, params, serializer);
                            if (logger.isTraceEnabled())
                                logger.trace("Serialized response: " + s);
                            if (resp.getContentType() == null) {
                                resp.setContentType(serializer.getResponseMIME());
                                resp.setCharacterEncoding("utf8");
                            }
                            resp.getWriter().write(s);
                            resp.getWriter().flush();
                            resp.getWriter().close();
                        }
                    } catch (IOException e) {
                        ErrorLogger.log(getClass(), e);
                        resp.sendError(HttpServletResponse.SC_INTERNAL_SERVER_ERROR,
                                "Method execution failed: " + e.getMessage());
                    }
                } else
                    resp.sendError(HttpServletResponse.SC_INTERNAL_SERVER_ERROR, "Unsupported format");
            } else {
                logger.warn("Request for unavailable REST method " + path);
                resp.sendError(HttpServletResponse.SC_INTERNAL_SERVER_ERROR,
                        "There is no method registered with path " + path);
            }
        } catch (AuthenticationException e) {
            resp.setHeader("WWW-Authenticate", "BASIC realm=\"SecureFiles\"");
            resp.sendError(HttpServletResponse.SC_UNAUTHORIZED, "Please provide username and password");
        } finally {
            if (sessionManager != null)
                sessionManager.clearCurrentSessionId();
            beans.remove(Thread.currentThread());
        }
    }

    // ===========================================================================
    /**
     * Executes the given method using the provided parameters, and returns the result
     * serialized with the given serializer.
     *
     * @param rm The method to execute.
     * @param params The paramters to use.
     * @param serializer The serializer to use.
     * @return The serialized result.
     * @throws IOException If the method failed execution.
     */
    @ContextInitializer
    public String executeMethod(RESTMethod rm, Object[] params, RESTSerializer serializer) throws IOException {
        Object result = executeMethod(rm, params);
        return serializer.serialize(result);
    }

    // ===========================================================================
    /**
     * Traces the parameter given.
     *
     * @param name The name of the parameter.
     * @param val The value of the parameter.
     */
    private void traceParameter(String name, String[] val) {
        StringBuilder b = new StringBuilder();
        for (String v : val)
            b.append(v).append(" ");
        logger.trace("Parameter: " + name + " : " + b.toString());
    }

    // ===========================================================================
    /**
     * Executes the {@link RESTMethod} specified using the given parameters and returns the
     * result.
     *
     * @param method The method to execute.
     * @param params The parameters.
     * @return The result of the method invocation.
     * @throws IOException If an error occurs.
     */
    @ContextInitializer
    public Object executeMethod(RESTMethod method, Object[] params) throws IOException {
        try {
            if (logger.isDebugEnabled())
                logger.debug("Executing method: " + method.getMethod());
            Object result = method.getMethod().invoke(method.getObject(), params);
            if (logger.isDebugEnabled())
                logger.debug("Method returned: " + result);
            return result;
        } catch (InvocationTargetException e) {
            Throwable c = e.getCause();
            ErrorLogger.log(getClass(), c);
            throw new IOException(c.getMessage());
        } catch (Throwable e) {
            ErrorLogger.log(getClass(), e);
            if (e instanceof IOException)
                throw (IOException) e;
            else
                throw new IOException(e.getMessage());
        }
    }

    // ===========================================================================
    @Override
    public void registered(Object t) {
        for (Method m : t.getClass().getMethods()) {
            REST r = AnnotationUtility.getAnnotation(REST.class, m, t);
            if (r != null) {
                if (logger.isDebugEnabled())
                    logger.debug("Registered REST Method " + fixPath(r));
                RESTMethod rm = new RESTMethod();
                rm.setMethod(m);
                rm.setObject(t);
                rm.setForm(r.form());
                methods.put(fixPath(r), rm);
            }
        }

        if (t instanceof RESTSerializer) {
            RESTSerializer rs = (RESTSerializer) t;
            serializers.put(rs.getFormat(), rs);
        }

        if (t instanceof RESTAuthenticator)
            authenticators.add((RESTAuthenticator) t);

        if (t instanceof SessionManager)
            sessionManager = (SessionManager) t;
    }

    // ===========================================================================
    /**
     * Returns the path (fixed) from the annotation given.
     *
     * @param r The annotation to process.
     * @return The fixed path.
     */
    private String fixPath(REST r) {
        String path = r.path();
        if (path.startsWith("/"))
            path = path.substring(1);
        if (path.endsWith("/"))
            path = path.substring(0, path.length() - 1);
        return path;
    }

    // ===========================================================================
    @Override
    public void unregistered(Object t) {
        for (Method m : t.getClass().getMethods()) {
            REST r = AnnotationUtility.getAnnotation(REST.class, m, t);
            if (r != null)
                methods.remove(fixPath(r));
        }

        if (t instanceof RESTSerializer) {
            RESTSerializer rs = (RESTSerializer) t;
            serializers.remove(rs.getFormat());
        }

        if (t instanceof RESTAuthenticator)
            authenticators.remove(t);

        if (t == sessionManager)
            sessionManager = null;
    }

    // ===========================================================================
    /**
     * Returns the current request.
     *
     * @return The current request.
     */
    public static HttpServletRequest getCurrentRequest() {
        return beans.get(Thread.currentThread()).request;
    }

    // ===========================================================================
    /**
     * Returns the current response.
     *
     * @return The current response.
     */
    public static HttpServletResponse getCurrentResponse() {
        return beans.get(Thread.currentThread()).response;
    }
}