co.cask.cdap.internal.app.runtime.procedure.ProcedureDispatcher.java Source code

Java tutorial

Introduction

Here is the source code for co.cask.cdap.internal.app.runtime.procedure.ProcedureDispatcher.java

Source

/*
 * Copyright  2014 Cask Data, 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 co.cask.cdap.internal.app.runtime.procedure;

import co.cask.cdap.api.procedure.ProcedureRequest;
import co.cask.cdap.common.metrics.MetricsCollector;
import com.google.common.base.Charsets;
import com.google.common.collect.ImmutableMap;
import com.google.common.collect.Maps;
import com.google.common.reflect.TypeToken;
import com.google.gson.Gson;
import org.jboss.netty.buffer.ChannelBuffer;
import org.jboss.netty.buffer.ChannelBufferInputStream;
import org.jboss.netty.buffer.ChannelBufferOutputStream;
import org.jboss.netty.buffer.ChannelBuffers;
import org.jboss.netty.channel.Channel;
import org.jboss.netty.channel.ChannelFutureListener;
import org.jboss.netty.channel.ChannelHandlerContext;
import org.jboss.netty.channel.Channels;
import org.jboss.netty.channel.ExceptionEvent;
import org.jboss.netty.channel.MessageEvent;
import org.jboss.netty.channel.SimpleChannelHandler;
import org.jboss.netty.handler.codec.http.DefaultHttpResponse;
import org.jboss.netty.handler.codec.http.HttpHeaders;
import org.jboss.netty.handler.codec.http.HttpMethod;
import org.jboss.netty.handler.codec.http.HttpRequest;
import org.jboss.netty.handler.codec.http.HttpResponse;
import org.jboss.netty.handler.codec.http.HttpResponseStatus;
import org.jboss.netty.handler.codec.http.HttpVersion;
import org.jboss.netty.handler.codec.http.QueryStringDecoder;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

import java.io.IOException;
import java.io.InputStreamReader;
import java.io.OutputStreamWriter;
import java.io.Writer;
import java.lang.reflect.Type;
import java.util.List;
import java.util.Map;
import java.util.regex.Matcher;
import java.util.regex.Pattern;
import javax.annotation.Nullable;

/**
 * This class dispatch HTTP requests to HandlerMethod. It uses thread local to control
 * how many instances of HandlerMethod created, hence it is supposed to be used shared
 * around all ChannelPipeline.
 */
final class ProcedureDispatcher extends SimpleChannelHandler {

    private static final Logger LOG = LoggerFactory.getLogger(ProcedureDispatcher.class);
    private static final Type REQUEST_TYPE = new TypeToken<Map<String, String>>() {
    }.getType();
    private static final Pattern REQUEST_URI_PATTERN = Pattern.compile("apps/(.+)/procedures/(.+)/methods/(.+)$");
    private static final Pattern METHOD_GET_PATTERN = Pattern.compile("^(.*?)[?]");
    private static final Gson GSON = new Gson();
    private static final Type QUERY_PARAMS_TYPE = new TypeToken<Map<String, String>>() {
    }.getType();
    private static final Maps.EntryTransformer<String, List<String>, String> MULTIMAP_TO_MAP_FUNCTION = new Maps.EntryTransformer<String, List<String>, String>() {
        @Override
        public String transformEntry(@Nullable String key, @Nullable List<String> value) {
            if (value == null || value.isEmpty()) {
                return null;
            }
            return value.get(0);
        }
    };

    private final MetricsCollector metrics;
    private final ThreadLocal<HandlerMethod> handlerMethod;

    ProcedureDispatcher(final HandlerMethodFactory handlerMethodFactory, MetricsCollector metrics) {
        this.metrics = metrics;
        handlerMethod = new ThreadLocal<HandlerMethod>() {
            @Override
            protected HandlerMethod initialValue() {
                return handlerMethodFactory.create();
            }
        };
    }

    @Override
    public void messageReceived(ChannelHandlerContext ctx, MessageEvent e) throws Exception {
        Object message = e.getMessage();
        if (!(message instanceof HttpRequest)) {
            super.messageReceived(ctx, e);
            return;
        }

        handleRequest((HttpRequest) message, ctx.getChannel());
    }

    @Override
    public void exceptionCaught(ChannelHandlerContext ctx, ExceptionEvent e) throws Exception {
        LOG.error("Exception caught in channel processing.", e.getCause());
        ctx.getChannel().close();
    }

    /**
     * Sends a error response and close the channel.
     * @param status Status of the response.
     * @param channel Netty channel for output.
     */
    private void errorResponse(HttpResponseStatus status, Channel channel, String content) {
        metrics.increment("query.failed", 1);
        HttpResponse response = new DefaultHttpResponse(HttpVersion.HTTP_1_1, status);
        response.setHeader(HttpHeaders.Names.CONTENT_TYPE, "text/plain; charset=utf-8");
        response.setContent(ChannelBuffers.wrappedBuffer(Charsets.UTF_8.encode(content)));
        Channels.write(channel, response).addListener(ChannelFutureListener.CLOSE);
    }

    private void handleRequest(HttpRequest httpRequest, Channel channel) {
        if (!(HttpMethod.POST.equals(httpRequest.getMethod())
                || (HttpMethod.GET.equals(httpRequest.getMethod())))) {
            errorResponse(HttpResponseStatus.METHOD_NOT_ALLOWED, channel,
                    "Only GET and POST methods are supported.");
            return;
        }

        Matcher uriMatcher = REQUEST_URI_PATTERN.matcher(httpRequest.getUri());
        if (!uriMatcher.find()) {
            errorResponse(HttpResponseStatus.BAD_REQUEST, channel, "Invalid request uri.");
            return;
        }

        String requestMethod = uriMatcher.group(3);

        ProcedureRequest request = createProcedureRequest(httpRequest, channel, requestMethod);
        if (request == null) {
            return;
        }

        // Lookup the request handler and handle
        HandlerMethod handler;
        try {
            handler = handlerMethod.get();
        } catch (Throwable t) {
            LOG.error("Fail to get procedure.", t);
            errorResponse(HttpResponseStatus.INTERNAL_SERVER_ERROR, channel, "Fail to get procedure.");
            return;
        }
        handler.handle(request, new HttpProcedureResponder(channel));
    }

    private ProcedureRequest createProcedureRequest(HttpRequest request, Channel channel, String requestMethod) {
        try {
            Map<String, String> args;
            ChannelBuffer content;
            if (HttpMethod.POST.equals(request.getMethod())) {
                content = request.getContent();
            } else {
                //GET method - Get key/value pairs from the URI
                Map<String, List<String>> queryParams = new QueryStringDecoder(request.getUri()).getParameters();
                content = ChannelBuffers.EMPTY_BUFFER;

                if (!queryParams.isEmpty()) {
                    content = jsonEncode(Maps.transformEntries(queryParams, MULTIMAP_TO_MAP_FUNCTION),
                            QUERY_PARAMS_TYPE, ChannelBuffers.dynamicBuffer(request.getUri().length()));
                }
            }

            if (content == null || !content.readable()) {
                args = ImmutableMap.of();
            } else {
                args = GSON.fromJson(new InputStreamReader(new ChannelBufferInputStream(content), Charsets.UTF_8),
                        REQUEST_TYPE);
            }

            //Extract the GET method name
            Matcher methodMatcher = METHOD_GET_PATTERN.matcher(requestMethod);
            if (methodMatcher.find()) {
                requestMethod = methodMatcher.group(1);
            }

            return new DefaultProcedureRequest(requestMethod, args);

        } catch (Exception ex) {
            errorResponse(HttpResponseStatus.BAD_REQUEST, channel, "Only json map<string,string> is supported.");
        }
        return null;
    }

    private <T> ChannelBuffer jsonEncode(T obj, Type type, ChannelBuffer buffer) throws IOException {
        Writer writer = new OutputStreamWriter(new ChannelBufferOutputStream(buffer), Charsets.UTF_8);
        try {
            GSON.toJson(obj, type, writer);
        } finally {
            writer.close();
        }
        return buffer;
    }

}