com.btoddb.chronicle.catchers.RestCatcherImpl.java Source code

Java tutorial

Introduction

Here is the source code for com.btoddb.chronicle.catchers.RestCatcherImpl.java

Source

package com.btoddb.chronicle.catchers;

/*
 * #%L
 * fast-persistent-queue
 * %%
 * Copyright (C) 2014 btoddb.com
 * %%
 * Permission is hereby granted, free of charge, to any person obtaining a copy
 * of this software and associated documentation files (the "Software"), to deal
 * in the Software without restriction, including without limitation the rights
 * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
 * copies of the Software, and to permit persons to whom the Software is
 * furnished to do so, subject to the following conditions:
 * 
 * The above copyright notice and this permission notice shall be included in
 * all copies or substantial portions of the Software.
 * 
 * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
 * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
 * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
 * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
 * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
 * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
 * THE SOFTWARE.
 * #L%
 */

import com.btoddb.chronicle.Config;
import com.btoddb.chronicle.Event;
import com.btoddb.chronicle.Utils;
import com.fasterxml.jackson.core.type.TypeReference;
import org.eclipse.jetty.jmx.MBeanContainer;
import org.eclipse.jetty.server.Connector;
import org.eclipse.jetty.server.Request;
import org.eclipse.jetty.server.Server;
import org.eclipse.jetty.server.ServerConnector;
import org.eclipse.jetty.server.handler.AbstractHandler;
import org.eclipse.jetty.server.handler.ContextHandler;
import org.eclipse.jetty.server.handler.DefaultHandler;
import org.eclipse.jetty.server.handler.ErrorHandler;
import org.eclipse.jetty.server.handler.HandlerCollection;
import org.eclipse.jetty.server.handler.StatisticsHandler;
import org.eclipse.jetty.util.log.Log;
import org.eclipse.jetty.util.thread.QueuedThreadPool;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

import javax.servlet.ServletException;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import java.io.BufferedInputStream;
import java.io.IOException;
import java.io.InputStream;
import java.lang.management.ManagementFactory;
import java.util.Collections;
import java.util.List;
import java.util.concurrent.ArrayBlockingQueue;

import static com.codahale.metrics.MetricRegistry.name;

public class RestCatcherImpl extends CatcherBaseImpl {
    private static final Logger logger = LoggerFactory.getLogger(RestCatcherImpl.class);

    private Server server;

    private int port = 8083;
    private String bind = "0.0.0.0";
    private int maxBatchSize = 100;

    @Override
    public void init(Config config) throws Exception {
        super.init(config);

        updateMetrics(null);

        startJettyServer();
    }

    private void startJettyServer() {
        QueuedThreadPool tp = new QueuedThreadPool(200, 8, 30000, new ArrayBlockingQueue<Runnable>(1000));
        tp.setName("Chronicle-RestV1-ThreadPool");

        server = new Server(tp);

        // Setup JMX - do this before setting *anything* else on the server object
        MBeanContainer mbContainer = new MBeanContainer(ManagementFactory.getPlatformMBeanServer());
        server.addEventListener(mbContainer);
        server.addBean(mbContainer);
        server.addBean(Log.getLogger(RestCatcherImpl.class));

        // bind connector to IP and port
        ServerConnector connector = new ServerConnector(server);
        connector.setHost(bind);
        connector.setPort(port);
        server.setConnectors(new Connector[] { connector });

        // setup handlers/stats for the context "/v1/events"
        HandlerCollection v1EventHandlers = new HandlerCollection();
        v1EventHandlers.addHandler(new RequestHandler());
        v1EventHandlers.addHandler(new DefaultHandler());
        StatisticsHandler statsHandler = new StatisticsHandler();
        statsHandler.setHandler(v1EventHandlers);

        ContextHandler context = new ContextHandler();
        context.setDisplayName("Chronicle-RestV1");
        context.setContextPath("/v1/events");
        context.setAllowNullPathInfo(true); // to avoid redirect on POST
        context.setHandler(statsHandler);

        // setup handlers/contexts for the overall server
        HandlerCollection serverHandlers = new HandlerCollection();
        serverHandlers.addHandler(context);
        server.setHandler(serverHandlers);
        server.addBean(new ErrorHandler() {
            @Override
            public void handle(String target, Request baseRequest, HttpServletRequest request,
                    HttpServletResponse response) throws IOException {
                logger.warn(String.format("RESTv1 response code = %d : %s : forward-for=%s : %s",
                        baseRequest.getResponse().getStatus(), baseRequest.getRemoteAddr(),
                        baseRequest.getHeader("X-Forwarded-For"), baseRequest.getRequestURL()));
                super.handle(target, baseRequest, request, response);
            }
        });

        try {
            server.start();
            logger.info("jetty started and listening on port " + port);
        } catch (Exception e) {
            logger.error("exception while starting jetty server", e);
        }
    }

    public boolean isJsonArray(InputStream inStream) {
        if (null == inStream) {
            return false;
        }

        int count = 100;
        inStream.mark(count);
        int ch;
        try {
            do {
                ch = inStream.read();
            } while ('[' != ch && '{' != ch && Character.isWhitespace((char) ch) && -1 != ch && --count > 0);

            inStream.reset();
            if (0 == count) {
                Utils.logAndThrow(logger, "unrecognizable JSON, or too much whitespace before JSON doc starts");
            }
            return '[' == ch;
        } catch (IOException e) {
            Utils.logAndThrow(logger, "exception while looking for JSON in InputStream", e);
        }

        return false;
    }

    @Override
    public void shutdown() {
        if (null != server) {
            try {
                server.stop();
            } catch (Exception e) {
                logger.error("exception while shutting down jetty server", e);
            }
        }
    }

    private void updateMetrics(Integer contentLength) {
        if (null != contentLength) {
            config.getCatcherMetrics().getRegistry().histogram(name(getId(), "content-length"))
                    .update(contentLength);
        } else {
            config.getCatcherMetrics().getRegistry().histogram(name(getId(), "content-length"));
        }
    }

    public class RequestHandler extends AbstractHandler {
        public void handle(String target, Request baseRequest, HttpServletRequest request,
                HttpServletResponse response) throws IOException, ServletException {
            // make sure we have a POST (PUT has the assumption to be idempotent - FPQ is not)
            if (!"POST".equals(request.getMethod())) {
                response.setStatus(HttpServletResponse.SC_METHOD_NOT_ALLOWED);
                response.getWriter().print(
                        "For this request the server only supports POST - in particular PUT is not supported because FPQ is not idempotent");
                return;
            }

            //
            // TODO:BTB - check that request isn't "too large"
            //

            config.getCatcherMetrics().markBatchStart(getId());
            updateMetrics(request.getContentLength());
            try {
                doIt(baseRequest, request, response);
            } finally {
                config.getCatcherMetrics().markBatchEnd(getId());
            }
        }

        private void doIt(Request baseRequest, HttpServletRequest request, HttpServletResponse response)
                throws IOException {
            List<Event> eventList;

            baseRequest.setHandled(true);
            BufferedInputStream reqInStream = new BufferedInputStream(request.getInputStream());

            // check for list of events, or single event
            try {
                if (!isJsonArray(reqInStream)) {
                    Event event = config.getEventSerializer().deserialize(reqInStream);
                    eventList = Collections.singletonList(event);
                } else {
                    eventList = config.getEventSerializer().deserializeList(reqInStream);
                }
            } catch (Exception e) {
                response.setStatus(HttpServletResponse.SC_BAD_REQUEST);
                response.getWriter().print(e.getMessage());
                return;
            }

            // parse list of events

            if (eventList.size() > maxBatchSize) {
                response.setStatus(HttpServletResponse.SC_REQUEST_ENTITY_TOO_LARGE);
                response.getWriter().print(
                        String.format("Too many events received. The maximum batch size is %d", maxBatchSize));
                return;
            }

            try {
                catchEvents(eventList);
            } catch (Exception e) {
                logger.error("exception while processing events", e);
                response.setStatus(HttpServletResponse.SC_INTERNAL_SERVER_ERROR);
                response.getWriter().print(e.getMessage());
                return;
            }

            response.setStatus(HttpServletResponse.SC_OK);
            response.setContentType("application/json");
            response.getWriter().print(String.format("{ \"received\": %d}", eventList.size()));
        }
    }

    @SuppressWarnings("unused")
    public void setBind(String bind) {
        this.bind = bind;
    }

    @SuppressWarnings("unused")
    public void setPort(int port) {
        this.port = port;
    }

    public int getPort() {
        return port;
    }

    public String getBind() {
        return bind;
    }

    @SuppressWarnings("unused")
    public int getMaxBatchSize() {
        return maxBatchSize;
    }

    @SuppressWarnings("unused")
    public void setMaxBatchSize(int maxBatchSize) {
        this.maxBatchSize = maxBatchSize;
    }

}