com.google.nigori.server.NigoriServlet.java Source code

Java tutorial

Introduction

Here is the source code for com.google.nigori.server.NigoriServlet.java

Source

/*
 * Copyright (C) 2011 Alastair R. Beresford
 * 
 * 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.google.nigori.server;

import static com.google.nigori.common.MessageLibrary.toBytes;

import java.io.BufferedReader;
import java.io.BufferedWriter;
import java.io.IOException;
import java.io.InputStreamReader;
import java.io.OutputStreamWriter;
import java.util.HashMap;
import java.util.logging.Logger;

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

import org.apache.commons.codec.binary.Base64;

import com.google.nigori.common.MessageLibrary;
import com.google.nigori.common.MessageLibrary.JsonConversionException;
import com.google.nigori.common.NigoriMessages.AuthenticateRequest;
import com.google.nigori.common.NigoriMessages.DeleteRequest;
import com.google.nigori.common.NigoriMessages.GetIndicesRequest;
import com.google.nigori.common.NigoriMessages.GetRequest;
import com.google.nigori.common.NigoriMessages.GetResponse;
import com.google.nigori.common.NigoriMessages.GetRevisionsRequest;
import com.google.nigori.common.NigoriMessages.PutRequest;
import com.google.nigori.common.NigoriMessages.RegisterRequest;
import com.google.nigori.common.NigoriMessages.UnregisterRequest;
import com.google.nigori.common.NigoriProtocol;
import com.google.nigori.common.NotFoundException;
import com.google.nigori.common.UnauthorisedException;
import com.google.nigori.server.appengine.AppEngineDatabase;

public class NigoriServlet extends HttpServlet {

    private static final long serialVersionUID = 1L;
    private static final boolean DEBUG_JSON = false;
    private static final Logger log = Logger.getLogger(NigoriServlet.class.getName());
    private static final int maxJsonQueryLength = 1024 * 1024 * 1;
    private final NigoriProtocol protocol;

    public NigoriServlet() {
        this(new AppEngineDatabase());
    }

    public NigoriServlet(Database database) {
        super();
        this.protocol = new DatabaseNigoriProtocol(database);
    }

    private class ServletException extends Exception {
        private static final long serialVersionUID = 1L;
        private int statusCode;

        ServletException(int statusCode, String message) {
            super(message);
            this.statusCode = statusCode;
        }

        int getStatusCode() {
            return statusCode;
        }

        void writeHttpResponse(HttpServletResponse resp) throws IOException {
            resp.setContentType(MessageLibrary.MIMETYPE_JSON);
            resp.setCharacterEncoding(MessageLibrary.CHARSET);
            resp.setStatus(getStatusCode());
            resp.getOutputStream().write(toBytes(this.getMessage()));
        }
    }

    private String getJsonAsString(HttpServletRequest req, int maxLength) throws ServletException {

        if (maxLength != 0 && req.getContentLength() > maxLength) {
            return null;
        }

        String charsetName = req.getCharacterEncoding();
        if (charsetName == null) {
            charsetName = MessageLibrary.CHARSET;
        }

        try {
            BufferedReader in = new BufferedReader(new InputStreamReader(req.getInputStream(), charsetName));
            StringBuilder json = new StringBuilder();
            char[] buffer = new char[64 * 1024];
            int charsRemaining = maxJsonQueryLength;
            int charsRead;
            while ((charsRead = in.read(buffer)) != -1) {
                charsRemaining -= charsRead;
                if (charsRemaining < 0) {
                    throw new ServletException(HttpServletResponse.SC_REQUEST_ENTITY_TOO_LARGE,
                            "Json request exceeds server maximum length of " + maxLength);
                }
                json.append(buffer, 0, charsRead);
            }
            return json.toString();
        } catch (IOException ioe) {
            throw new ServletException(HttpServletResponse.SC_INTERNAL_SERVER_ERROR,
                    "Internal error receiving data from client.");
        }
    }

    /**
     * Class to support efficient lookup of appropriate handler method for a particular request.
     */
    private class RequestHandlerType {

        private String mimetype;
        private String requestType;

        RequestHandlerType(String mimetype, String requestType) {
            this.mimetype = mimetype;
            this.requestType = requestType;
        }

        @Override
        public int hashCode() {
            return mimetype.hashCode() + requestType.hashCode();
        }

        @Override
        public boolean equals(Object obj) {
            if (obj instanceof RequestHandlerType) {
                RequestHandlerType r = (RequestHandlerType) obj;
                return this.hashCode() == r.hashCode();
            }
            return false;
        }

        @Override
        public String toString() {
            return "<" + mimetype + "," + requestType + ">";
        }
    }

    /**
     * Send an SC_OK and and empty body
     * 
     * @param resp
     * @throws ServletException
     */
    private void emptyBody(HttpServletResponse resp) throws ServletException {
        try {
            resp.setContentType(MessageLibrary.MIMETYPE_JSON);
            resp.setStatus(HttpServletResponse.SC_OK);
            resp.flushBuffer();
        } catch (IOException ioe) {
            throw new ServletException(HttpServletResponse.SC_INTERNAL_SERVER_ERROR,
                    "Error attempting to write status OK message after successfully handling a PutRequest");
        }
    }

    private abstract interface RequestHandler {
        public void handle(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException,
                JsonConversionException, UnauthorisedException, NotFoundException;
    }

    private class JsonGetRequestHandler implements RequestHandler {

        @Override
        public void handle(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException,
                JsonConversionException, NotFoundException, UnauthorisedException {

            String json = getJsonAsString(req, maxJsonQueryLength);
            GetRequest request = MessageLibrary.getRequestFromJson(json);
            GetResponse response = protocol.get(request);

            String jsonresponse = MessageLibrary.toJson(response);
            resp.setContentType(MessageLibrary.MIMETYPE_JSON);
            resp.setCharacterEncoding(MessageLibrary.CHARSET);
            resp.setStatus(HttpServletResponse.SC_OK);
            BufferedWriter w = new BufferedWriter(new OutputStreamWriter(resp.getOutputStream()));
            w.write(jsonresponse);
            w.flush();
        }
    }

    private class JsonGetIndicesRequestHandler implements RequestHandler {

        @Override
        public void handle(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException,
                NotFoundException, UnauthorisedException, JsonConversionException {

            String json = getJsonAsString(req, maxJsonQueryLength);

            GetIndicesRequest request = MessageLibrary.getIndicesRequestFromJson(json);

            String response = MessageLibrary.toJson(protocol.getIndices(request));
            resp.setContentType(MessageLibrary.MIMETYPE_JSON);
            resp.setCharacterEncoding(MessageLibrary.CHARSET);
            resp.setStatus(HttpServletResponse.SC_OK);
            BufferedWriter w = new BufferedWriter(new OutputStreamWriter(resp.getOutputStream()));
            w.write(response);
            w.flush();
        }
    }

    private class JsonGetRevisionsRequestHandler implements RequestHandler {

        @Override
        public void handle(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException,
                NotFoundException, UnauthorisedException, JsonConversionException {

            String json = getJsonAsString(req, maxJsonQueryLength);

            GetRevisionsRequest request = MessageLibrary.getRevisionsRequestFromJson(json);

            String response = MessageLibrary.toJson(protocol.getRevisions(request));
            resp.setContentType(MessageLibrary.MIMETYPE_JSON);
            resp.setCharacterEncoding(MessageLibrary.CHARSET);
            resp.setStatus(HttpServletResponse.SC_OK);
            BufferedWriter w = new BufferedWriter(new OutputStreamWriter(resp.getOutputStream()));
            w.write(response);
            w.flush();
        }
    }

    private class JsonPutRequestHandler implements RequestHandler {

        @Override
        public void handle(HttpServletRequest req, HttpServletResponse resp)
                throws ServletException, JsonConversionException, IOException, UnauthorisedException {
            String json = getJsonAsString(req, maxJsonQueryLength);
            if (DEBUG_JSON) {
                System.out.println(json);
            }
            PutRequest request = MessageLibrary.putRequestFromJson(json);

            if (!protocol.put(request)) {
                throw new ServletException(HttpServletResponse.SC_INTERNAL_SERVER_ERROR,
                        "Internal storage error for key "
                                + Base64.encodeBase64String(request.getKey().toByteArray()));
            }

            emptyBody(resp);
        }
    }

    private class JsonDeleteRequestHandler implements RequestHandler {

        @Override
        public void handle(HttpServletRequest req, HttpServletResponse resp) throws ServletException,
                JsonConversionException, IOException, UnauthorisedException, NotFoundException {
            String json = getJsonAsString(req, maxJsonQueryLength);

            if (DEBUG_JSON) {
                System.out.println(json);
            }
            DeleteRequest request = MessageLibrary.deleteRequestFromJson(json);

            if (!protocol.delete(request)) {
                throw new ServletException(HttpServletResponse.SC_INTERNAL_SERVER_ERROR,
                        "Internal storage error for key "
                                + Base64.encodeBase64String(request.getKey().toByteArray()));
            }

            emptyBody(resp);
        }
    }

    private class JsonAuthenticateRequestHandler implements RequestHandler {

        @Override
        public void handle(HttpServletRequest req, HttpServletResponse resp)
                throws ServletException, JsonConversionException, IOException, UnauthorisedException {
            String json = getJsonAsString(req, maxJsonQueryLength);
            AuthenticateRequest auth = MessageLibrary.authenticateRequestFromJson(json);
            boolean success = protocol.authenticate(auth);
            if (!success) {
                throw new UnauthorisedException("Authorisation failed");
            }

            emptyBody(resp);
        }
    }

    private class JsonRegisterRequestHandler implements RequestHandler {

        @Override
        public void handle(HttpServletRequest req, HttpServletResponse resp)
                throws ServletException, JsonConversionException, IOException {

            String json = getJsonAsString(req, maxJsonQueryLength);
            RegisterRequest request = MessageLibrary.registerRequestFromJson(json);

            boolean success = protocol.register(request);
            if (!success) {
                throw new ServletException(HttpServletResponse.SC_CONFLICT,
                        "Adding user " + Base64.encodeBase64String(request.getPublicKey().toByteArray())
                                + " failed, may already exist");
            }
            emptyBody(resp);
        }
    }

    private class JsonUnregisterRequestHandler implements RequestHandler {

        @Override
        public void handle(HttpServletRequest req, HttpServletResponse resp)
                throws ServletException, IOException, UnauthorisedException, JsonConversionException {
            String json = getJsonAsString(req, maxJsonQueryLength);
            UnregisterRequest request = MessageLibrary.unregisterRequestFromJson(json);

            boolean success = protocol.unregister(request);
            if (!success) {
                throw new ServletException(HttpServletResponse.SC_INTERNAL_SERVER_ERROR, "Removing user "
                        + Base64.encodeBase64String(request.getAuth().getPublicKey().toByteArray()) + " failed");
            }
            emptyBody(resp);
        }
    }

    // TODO(beresford): double-check that Servlet instances are created rarely
    private String supportedTypes = null;
    private HashMap<RequestHandlerType, RequestHandler> handlers = initHandlers();

    private HashMap<RequestHandlerType, RequestHandler> initHandlers() {
        HashMap<RequestHandlerType, RequestHandler> h = new HashMap<RequestHandlerType, RequestHandler>();
        h.put(new RequestHandlerType(MessageLibrary.MIMETYPE_JSON, MessageLibrary.REQUEST_GET),
                new JsonGetRequestHandler());
        h.put(new RequestHandlerType(MessageLibrary.MIMETYPE_JSON, MessageLibrary.REQUEST_GET_INDICES),
                new JsonGetIndicesRequestHandler());
        h.put(new RequestHandlerType(MessageLibrary.MIMETYPE_JSON, MessageLibrary.REQUEST_GET_REVISIONS),
                new JsonGetRevisionsRequestHandler());
        h.put(new RequestHandlerType(MessageLibrary.MIMETYPE_JSON, MessageLibrary.REQUEST_PUT),
                new JsonPutRequestHandler());
        h.put(new RequestHandlerType(MessageLibrary.MIMETYPE_JSON, MessageLibrary.REQUEST_DELETE),
                new JsonDeleteRequestHandler());
        h.put(new RequestHandlerType(MessageLibrary.MIMETYPE_JSON, MessageLibrary.REQUEST_AUTHENTICATE),
                new JsonAuthenticateRequestHandler());
        h.put(new RequestHandlerType(MessageLibrary.MIMETYPE_JSON, MessageLibrary.REQUEST_REGISTER),
                new JsonRegisterRequestHandler());
        h.put(new RequestHandlerType(MessageLibrary.MIMETYPE_JSON, MessageLibrary.REQUEST_UNREGISTER),
                new JsonUnregisterRequestHandler());
        StringBuilder supportedPairs = new StringBuilder(
                "The following mimetypes and request pairs are supported: ");
        for (RequestHandlerType type : h.keySet()) {
            supportedPairs.append("(" + type.mimetype + " - " + type.requestType + ") ");
        }
        supportedTypes = supportedPairs.toString();
        return h;
    }

    /**
     * Enable cors: http://enable-cors.org/server.html to allow access from javascript/dart clients
     * using code from a different domain
     * 
     * @param resp the response to add the headers to
     */
    private void addCorsHeaders(HttpServletResponse resp) {
        resp.addHeader("Access-Control-Allow-Origin", "*");
        resp.addHeader("Access-Control-Allow-Methods", "POST, GET, OPTIONS");
        resp.addHeader("Access-Control-Allow-Headers", "Origin, X-Requested-With, Content-Type, Accept");
    }

    /**
     * Add CORS headers to options requests
     */
    @Override
    protected void doOptions(HttpServletRequest req, HttpServletResponse resp)
            throws IOException, javax.servlet.ServletException {
        addCorsHeaders(resp);
        super.doOptions(req, resp);
    }

    /**
     * Handle initial request from client and dispatch to appropriate handler or return error message.
     */
    @Override
    public void doPost(HttpServletRequest req, HttpServletResponse resp) throws IOException {
        try {
            addCorsHeaders(resp);
            // Subset of path managed by this servlet; e.g. if URI is "/nigori/get" and servlet path
            // is "/nigori, then we want to retrieve "get" as the request type
            int startIndex = req.getServletPath().length() + 1;
            String requestURI = req.getRequestURI();
            if (requestURI.length() <= startIndex) {
                ServletException s = new ServletException(HttpServletResponse.SC_BAD_REQUEST,
                        "No request type specified.\n" + supportedTypes + "\n");
                log.fine(s.toString());
                s.writeHttpResponse(resp);
                return;
            }
            String requestType = requestURI.substring(startIndex);
            String requestMimetype = req.getContentType();
            RequestHandlerType handlerType = new RequestHandlerType(requestMimetype, requestType);

            RequestHandler handler = handlers.get(handlerType);
            if (handler == null) {
                throw new ServletException(HttpServletResponse.SC_NOT_ACCEPTABLE,
                        "Unsupported request pair: " + handlerType + "\n" + supportedTypes + "\n");
            }
            try {
                handler.handle(req, resp);
            } catch (NotFoundException e) {
                ServletException s = new ServletException(HttpServletResponse.SC_NOT_FOUND,
                        e.getLocalizedMessage());
                log.fine(s.toString());
                s.writeHttpResponse(resp);
            } catch (UnauthorisedException e) {
                ServletException s = new ServletException(HttpServletResponse.SC_UNAUTHORIZED,
                        "Authorisation failed: " + e.getLocalizedMessage());
                log.warning(s.toString());
                s.writeHttpResponse(resp);
            } catch (IOException ioe) {
                throw new ServletException(HttpServletResponse.SC_INTERNAL_SERVER_ERROR,
                        "Internal error sending data to client");
            } catch (MessageLibrary.JsonConversionException jce) {
                throw new ServletException(HttpServletResponse.SC_BAD_REQUEST,
                        "JSON format error: " + jce.getMessage());
            } catch (RuntimeException re) {
                log.severe(re.toString());
                throw new ServletException(HttpServletResponse.SC_INTERNAL_SERVER_ERROR, re.toString());
            }

        } catch (ServletException e) {
            log.severe(e.toString());
            e.writeHttpResponse(resp);
        }
    }
}