com.couchbase.capi.servlet.CAPIServlet.java Source code

Java tutorial

Introduction

Here is the source code for com.couchbase.capi.servlet.CAPIServlet.java

Source

/**
 * Copyright (c) 2012 Couchbase, Inc. All rights reserved.
 *
 * 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.couchbase.capi.servlet;

import java.io.IOException;
import java.io.InputStream;
import java.io.OutputStream;
import java.io.UnsupportedEncodingException;
import java.net.URLDecoder;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.List;
import java.util.Map;

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

import org.apache.commons.io.IOUtils;
import org.codehaus.jackson.JsonGenerationException;
import org.codehaus.jackson.map.JsonMappingException;
import org.codehaus.jackson.map.ObjectMapper;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

import com.couchbase.capi.CAPIBehavior;

/**
 * This servlet implements the Couch API (CAPI)
 *
 * This is not a fully-functional implementation, rather it is a bare-minimum implementation to support
 * receiving a push replication from another CouchDB instance.
 *
 * @author mschoch
 *
 */
@SuppressWarnings("serial")
public class CAPIServlet extends HttpServlet {

    private static final Logger logger = LoggerFactory.getLogger(CAPIServlet.class);
    protected ObjectMapper mapper = new ObjectMapper();

    protected CAPIBehavior capiBehavior;

    public CAPIServlet(CAPIBehavior capiBehavior) {
        this.capiBehavior = capiBehavior;
    }

    /**
     * Takes a look at the structure of the URL requested and dispatch to the right handler method
     */
    @Override
    protected void service(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException {

        String uri = req.getRequestURI();
        String[] splitUri = getUriPieces(uri);

        if ((splitUri.length == 1) && splitUri[0].equals("")) {
            handleWelcome(req, resp);
        } else if ((splitUri.length == 1 && splitUri[0].startsWith("_"))) {
            handleRootSpecial(req, resp, splitUri[0]);
        } else if (splitUri.length == 1) {
            handleDatabase(req, resp, unescapeName(splitUri[0]));
        } else if (splitUri.length == 2) {
            // make sure database is valid
            String doesNotExistReason = capiBehavior.databaseExists(unescapeName(splitUri[0]));
            if (doesNotExistReason != null) {
                sendNotFoundResponse(resp, doesNotExistReason);
                return;
            }

            if (splitUri[1].equals("_bulk_docs")) {
                handleBulkDocs(req, resp, unescapeName(splitUri[0]));
            } else if (splitUri[1].equals("_revs_diff")) {
                handleRevsDiff(req, resp, unescapeName(splitUri[0]));
            } else if (splitUri[1].equals("_ensure_full_commit")) {
                handleEnsureFullCommit(req, resp, unescapeName(splitUri[0]));
            } else if (splitUri[1].startsWith("_")) {
                logger.debug("Unsupported special operation {}", splitUri[1]);
            } else {
                // this must be a document id
                handleDocument(req, resp, unescapeName(splitUri[0]), unescapeName(splitUri[1]));
            }
        } else if (splitUri.length == 3) {
            // make sure database is valid
            String doesNotExistReason = capiBehavior.databaseExists(unescapeName(splitUri[0]));
            if (doesNotExistReason != null) {
                sendNotFoundResponse(resp, doesNotExistReason);
                return;
            }

            if (splitUri[1].equals("_local")) {
                handleLocalDocument(req, resp, unescapeName(splitUri[0]), "_local/" + unescapeName(splitUri[2]));
            } else {
                // attachment request
                handleAttachment(req, resp, unescapeName(splitUri[0]), splitUri[1], splitUri[2]);
            }
        } else {
            // make sure database is valid
            String doesNotExistReason = capiBehavior.databaseExists(unescapeName(splitUri[0]));
            if (doesNotExistReason != null) {
                sendNotFoundResponse(resp, doesNotExistReason);
                return;
            }

            if (splitUri[1].equals("_local")) {
                handleLocalAttachment(req, resp, unescapeName(splitUri[0]), splitUri[2], splitUri[3]);
            } else {
                logger.debug("I don't know how to handle {}", uri);
            }
        }

    }

    /**
     * Handle special operations at the root level /_...
     * @param req
     * @param resp
     * @param special
     * @throws ServletException
     * @throws IOException
     */
    protected void handleRootSpecial(HttpServletRequest req, HttpServletResponse resp, String special)
            throws ServletException, IOException {

        if (special.equals("_pre_replicate")) {
            logger.debug("got _pre_replicate: {}", req.getRequestURI());
            handlePreReplicate(req, resp);
            return;
        } else if (special.equals("_commit_for_checkpoint")) {
            logger.debug("got _commit_for_checkpoint: {}", req.getRequestURI());
            handleCommitForCheckpoint(req, resp);
            return;
        } else {
            logger.debug("got unknown special: {}", req.getRequestURI());
        }

        InputStream is = req.getInputStream();
        int requestLength = req.getContentLength();
        byte[] buffer = new byte[requestLength];
        IOUtils.readFully(is, buffer, 0, requestLength);

        logger.trace("root special request body was: '{}'", new String(buffer));

        sendNotFoundResponse(resp, "missing");
    }

    protected void handlePreReplicate(HttpServletRequest req, HttpServletResponse resp)
            throws ServletException, IOException {

        // read the request
        InputStream is = req.getInputStream();
        int requestLength = req.getContentLength();
        byte[] buffer = new byte[requestLength];
        IOUtils.readFully(is, buffer, 0, requestLength);

        @SuppressWarnings("unchecked")
        Map<String, Object> parsedValue = (Map<String, Object>) mapper.readValue(buffer, Map.class);
        logger.trace("pre replicate parsed value is " + parsedValue);

        int vbucket = (Integer) parsedValue.get("vb");
        String bucket = (String) parsedValue.get("bucket");
        String bucketUUID = (String) parsedValue.get("bucketUUID");
        String vbopaque = (String) parsedValue.get("vbopaque");
        String commitopaque = (String) parsedValue.get("commitopaque");

        String vbucketUUID = capiBehavior.getVBucketUUID("default", bucket, vbucket);

        if ((vbopaque != null) && (!vbopaque.equals(vbucketUUID))) {
            logger.debug("returning 400");
            resp.setStatus(HttpServletResponse.SC_BAD_REQUEST);
        }
        if ((commitopaque != null) && (!commitopaque.equals(vbucketUUID))) {
            logger.debug("returning 400");
            resp.setStatus(HttpServletResponse.SC_BAD_REQUEST);
        }

        OutputStream os = resp.getOutputStream();
        resp.setContentType("application/json");
        Map<String, Object> responseMap = new HashMap<String, Object>();
        responseMap.put("vbopaque", vbucketUUID);
        mapper.writeValue(os, responseMap);
    }

    protected void handleCommitForCheckpoint(HttpServletRequest req, HttpServletResponse resp)
            throws ServletException, IOException {

        // read the request
        InputStream is = req.getInputStream();
        int requestLength = req.getContentLength();
        byte[] buffer = new byte[requestLength];
        IOUtils.readFully(is, buffer, 0, requestLength);

        @SuppressWarnings("unchecked")
        Map<String, Object> parsedValue = (Map<String, Object>) mapper.readValue(buffer, Map.class);
        logger.trace("commit for checkpoint parsed value is " + parsedValue);

        int vbucket = (Integer) parsedValue.get("vb");
        String bucket = (String) parsedValue.get("bucket");
        String bucketUUID = (String) parsedValue.get("bucketUUID");
        String vbopaque = (String) parsedValue.get("vbopaque");

        String vbucketUUID = capiBehavior.getVBucketUUID("default", bucket, vbucket);
        Map<String, Object> responseMap = new HashMap<String, Object>();
        responseMap.put("vbopaque", vbucketUUID);

        if ((vbopaque != null) && (!vbopaque.equals(vbucketUUID))) {
            logger.debug("returning 400");
            resp.setStatus(HttpServletResponse.SC_BAD_REQUEST);
        } else {
            // add the commit opaque
            responseMap.put("commitopaque", vbucketUUID);
        }

        OutputStream os = resp.getOutputStream();
        resp.setContentType("application/json");
        mapper.writeValue(os, responseMap);
    }

    /**
     * Handle GET requests to the root URL
     * @param req
     * @param resp
     * @throws ServletException
     * @throws IOException
     */
    protected void handleWelcome(HttpServletRequest req, HttpServletResponse resp)
            throws ServletException, IOException {

        if (!req.getMethod().equals("GET")) {
            throw new UnsupportedOperationException("Only GET operations on / are supported at this time");
        }

        logger.trace("Got " + req.getMethod() + " request for /");
        OutputStream os = resp.getOutputStream();
        resp.setContentType("application/json");
        Map<String, Object> responseMap = capiBehavior.welcome();
        mapper.writeValue(os, responseMap);
    }

    /**
     * Handle GET/HEAD requests to the database URL
     *
     * @param req
     * @param resp
     * @param database
     * @throws ServletException
     * @throws IOException
     */
    protected void handleDatabase(HttpServletRequest req, HttpServletResponse resp, String database)
            throws ServletException, IOException {

        if (!(req.getMethod().equals("GET") || req.getMethod().equals("HEAD"))) {
            throw new UnsupportedOperationException(
                    "Only GET/HEAD operations on database are supported at this time");
        }

        logger.trace("Got " + req.getMethod() + " request for " + database);

        OutputStream os = resp.getOutputStream();

        String doesNotExistReason = capiBehavior.databaseExists(database);
        if (doesNotExistReason == null) {
            if (req.getMethod().equals("GET")) {
                resp.setContentType("application/json");

                Map<String, Object> responseMap = capiBehavior.getDatabaseDetails(database);
                mapper.writeValue(os, responseMap);
            }
        } else {
            sendNotFoundResponse(resp, doesNotExistReason);
        }

    }

    /**
     * Handle _revs_diff by claiming we don't have any of these revisions
     *
     * @param req
     * @param resp
     * @param database
     * @throws ServletException
     * @throws IOException
     */
    protected void handleRevsDiff(HttpServletRequest req, HttpServletResponse resp, String database)
            throws ServletException, IOException {

        if (!req.getMethod().equals("POST")) {
            throw new UnsupportedOperationException("_revs_diff must be POST");
        }

        logger.trace("Got revs diff request for " + database);

        OutputStream os = resp.getOutputStream();
        InputStream is = req.getInputStream();

        int requestLength = req.getContentLength();
        byte[] buffer = new byte[requestLength];
        IOUtils.readFully(is, buffer, 0, requestLength);

        logger.trace("revs diff request body was {}", new String(buffer));

        @SuppressWarnings("unchecked")
        Map<String, Object> parsedValue = (Map<String, Object>) mapper.readValue(buffer, Map.class);

        logger.trace("revs diff parsed value is " + parsedValue);

        try {
            Map<String, Object> responseMap = capiBehavior.revsDiff(database, parsedValue);

            if (responseMap != null) {
                mapper.writeValue(os, responseMap);
            } else {
                sendNotFoundResponse(resp, "missing");
            }
        } catch (UnavailableException e) {
            sendServiceUnavailableResponse(resp, "too many concurrent requests");
        }
    }

    protected void handleEnsureFullCommit(HttpServletRequest req, HttpServletResponse resp, String database)
            throws ServletException, IOException {

        if (!req.getMethod().equals("POST")) {
            throw new UnsupportedOperationException("_ensure_full_commit must be POST");
        }

        logger.trace("Got ensure full commitf request for " + database);

        resp.setStatus(HttpServletResponse.SC_CREATED);
        resp.setContentType("application/json");

        if (capiBehavior.ensureFullCommit(database)) {

            Map<String, Object> responseMap = new HashMap<String, Object>();
            responseMap.put("ok", true);

            OutputStream os = resp.getOutputStream();
            mapper.writeValue(os, responseMap);
        } else {
            sendNotFoundResponse(resp, "missing");
        }
    }

    protected void handleAttachment(HttpServletRequest req, HttpServletResponse resp, String databaseName,
            String documentId, String attachmentName) {
        throw new UnsupportedOperationException("Document attachments are not supported at this time");
    }

    protected void handleLocalAttachment(HttpServletRequest req, HttpServletResponse resp, String databaseName,
            String documentId, String attachemntName) {
        throw new UnsupportedOperationException("Local Document attachments are not supported at this time");
    }

    protected void handleDocument(HttpServletRequest req, HttpServletResponse resp, String databaseName,
            String documentId) throws IOException, ServletException {
        handleDocumentInternal(req, resp, databaseName, documentId, "document");
    }

    protected void handleLocalDocument(HttpServletRequest req, HttpServletResponse resp, String databaseName,
            String documentId) throws IOException, ServletException {
        handleDocumentInternal(req, resp, databaseName, documentId, "_local");
    }

    protected void handleDocumentInternal(HttpServletRequest req, HttpServletResponse resp, String databaseName,
            String documentId, String documentType) throws IOException, ServletException {

        logger.trace(String.format("Got document request in database %s document %s type %s", databaseName,
                documentId, documentType));

        if (!(req.getMethod().equals("GET") || req.getMethod().equals("HEAD") || req.getMethod().equals("PUT"))) {
            throw new UnsupportedOperationException(
                    "Only GET/HEAD/PUT operations on documents are supported at this time");
        }

        if (req.getMethod().equals("GET") || req.getMethod().equals("HEAD")) {

            Map<String, Object> doc = null;
            if (documentType.equals("_local")) {
                doc = capiBehavior.getLocalDocument(databaseName, documentId);
            } else {
                doc = capiBehavior.getDocument(databaseName, documentId);
            }

            if (doc != null) {
                resp.setStatus(HttpServletResponse.SC_OK);
                resp.setContentType("application/json");
                OutputStream os = resp.getOutputStream();
                mapper.writeValue(os, doc);
            } else {
                sendNotFoundResponse(resp, "missing");
                return;
            }

        } else if (req.getMethod().equals("PUT")) {

            String rev = null;

            //read the document
            InputStream is = req.getInputStream();

            int requestLength = req.getContentLength();
            byte[] buffer = new byte[requestLength];
            IOUtils.readFully(is, buffer, 0, requestLength);

            @SuppressWarnings("unchecked")
            Map<String, Object> parsedValue = (Map<String, Object>) mapper.readValue(buffer, Map.class);

            if (documentType.equals("_local)")) {
                rev = capiBehavior.storeLocalDocument(databaseName, documentId, parsedValue);
            } else {
                rev = capiBehavior.storeDocument(databaseName, documentId, parsedValue);
            }

            if (rev == null) {
                throw new ServletException("Storing document did not result in valid revision");
            }

            resp.setStatus(HttpServletResponse.SC_CREATED);
            resp.setContentType("application/json");
            OutputStream os = resp.getOutputStream();

            Map<String, Object> responseMap = new HashMap<String, Object>();
            responseMap.put("ok", true);
            responseMap.put("id", documentId);
            responseMap.put("rev", rev);
            mapper.writeValue(os, responseMap);
        }

    }

    private void sendNotFoundResponse(HttpServletResponse resp, String doesNotExistReason)
            throws IOException, JsonGenerationException, JsonMappingException {
        resp.setStatus(HttpServletResponse.SC_NOT_FOUND);
        resp.setContentType("application/json");
        OutputStream os = resp.getOutputStream();

        Map<String, Object> responseMap = new HashMap<String, Object>();
        responseMap.put("error", "not_found");
        responseMap.put("reason", doesNotExistReason);
        mapper.writeValue(os, responseMap);
    }

    private void sendServiceUnavailableResponse(HttpServletResponse resp, String reason)
            throws IOException, JsonGenerationException, JsonMappingException {
        resp.setStatus(HttpServletResponse.SC_SERVICE_UNAVAILABLE);
        resp.setContentType("application/json");
        OutputStream os = resp.getOutputStream();

        Map<String, Object> responseMap = new HashMap<String, Object>();
        responseMap.put("error", "service_unavailable");
        responseMap.put("reason", reason);
        mapper.writeValue(os, responseMap);
    }

    protected void handleBulkDocs(HttpServletRequest req, HttpServletResponse resp, String database)
            throws ServletException, IOException {

        if (!req.getMethod().equals("POST")) {
            throw new UnsupportedOperationException("_bulk_docs must be POST");
        }

        logger.trace("Got bulk docs request for " + database);

        resp.setStatus(HttpServletResponse.SC_CREATED);
        resp.setContentType("application/json");

        OutputStream os = resp.getOutputStream();
        InputStream is = req.getInputStream();

        int requestLength = req.getContentLength();
        byte[] buffer = new byte[requestLength];
        IOUtils.readFully(is, buffer, 0, requestLength);

        @SuppressWarnings("unchecked")
        Map<String, Object> parsedValue = (Map<String, Object>) mapper.readValue(buffer, Map.class);

        logger.trace("parsed value is " + parsedValue);

        try {
            List<Object> responseList = capiBehavior.bulkDocs(database,
                    (ArrayList<Map<String, Object>>) parsedValue.get("docs"));
            if (responseList == null) {
                sendNotFoundResponse(resp, "missing");
                return;
            }
            mapper.writeValue(os, responseList);
        } catch (UnavailableException e) {
            sendServiceUnavailableResponse(resp, "too many concurrent requests");
        }
    }

    String[] getUriPieces(String uri) {
        // remove initial /
        if (uri.startsWith("/")) {
            uri = uri.substring(1);
        }
        String[] result = uri.split("/");
        return result;
    }

    String unescapeName(String name) throws UnsupportedEncodingException {
        return URLDecoder.decode(name, "UTF-8");
    }

}