com.addthis.hydra.query.web.HttpQueryHandler.java Source code

Java tutorial

Introduction

Here is the source code for com.addthis.hydra.query.web.HttpQueryHandler.java

Source

/*
 * 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.addthis.hydra.query.web;

import java.util.Collection;
import java.util.List;
import java.util.Map;

import java.nio.CharBuffer;

import com.addthis.basis.kv.KVPairs;

import com.addthis.codec.jackson.Jackson;
import com.addthis.codec.json.CodecJSON;
import com.addthis.hydra.data.query.Query;
import com.addthis.hydra.query.MeshQueryMaster;
import com.addthis.hydra.query.loadbalance.QueryQueue;
import com.addthis.hydra.query.loadbalance.WorkerData;
import com.addthis.hydra.query.tracker.DetailedStatusHandler;
import com.addthis.hydra.query.tracker.QueryEntry;
import com.addthis.hydra.query.tracker.QueryEntryInfo;
import com.addthis.hydra.query.tracker.QueryTracker;
import com.addthis.hydra.util.MetricsServletShim;
import com.addthis.maljson.JSONArray;

import com.typesafe.config.ConfigFactory;
import com.yammer.metrics.Metrics;
import com.yammer.metrics.core.Counter;

import org.apache.commons.io.output.StringBuilderWriter;

import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

import io.netty.buffer.ByteBuf;
import io.netty.buffer.ByteBufUtil;
import io.netty.channel.ChannelFuture;
import io.netty.channel.ChannelFutureListener;
import io.netty.channel.ChannelHandler;
import io.netty.channel.ChannelHandlerContext;
import io.netty.channel.SimpleChannelInboundHandler;
import io.netty.handler.codec.http.DefaultHttpContent;
import io.netty.handler.codec.http.FullHttpRequest;
import io.netty.handler.codec.http.HttpContent;
import io.netty.handler.codec.http.HttpHeaders;
import io.netty.handler.codec.http.HttpMethod;
import io.netty.handler.codec.http.HttpResponse;
import io.netty.handler.codec.http.HttpResponseStatus;
import io.netty.handler.codec.http.LastHttpContent;
import io.netty.handler.codec.http.QueryStringDecoder;
import io.netty.util.concurrent.Future;

import static com.addthis.hydra.query.web.HttpUtils.sendError;
import static com.addthis.hydra.query.web.HttpUtils.sendRedirect;
import static io.netty.handler.codec.http.HttpResponseStatus.NOT_FOUND;
import static io.netty.util.CharsetUtil.UTF_8;
import static java.util.stream.Collectors.toMap;

@ChannelHandler.Sharable
public class HttpQueryHandler extends SimpleChannelInboundHandler<FullHttpRequest> {
    private static final Logger log = LoggerFactory.getLogger(HttpQueryHandler.class);

    /**
     * Used for tracking metrics and other interesting things about queries that we have run.  Provides insight
     * into currently running queries and provides the ability to cancel a query before it completes.
     */
    private final QueryTracker tracker;

    /** primary query source */
    private final MeshQueryMaster meshQueryMaster;

    private final QueryQueue queryQueue;
    private final HttpStaticFileHandler staticFileHandler;
    private final MetricsServletShim fakeMetricsServlet;

    // http metrics; may use other classes to derive metric paths for legacy metric namespace consistency
    private final Counter rawQueryCalls = Metrics.newCounter(MeshQueryMaster.class, "rawQueryCalls");

    public HttpQueryHandler(QueryTracker tracker, MeshQueryMaster meshQueryMaster, QueryQueue queryQueue) {
        super(true); // auto release
        this.tracker = tracker;
        this.meshQueryMaster = meshQueryMaster;
        this.queryQueue = queryQueue;
        this.fakeMetricsServlet = new MetricsServletShim();
        this.staticFileHandler = new HttpStaticFileHandler();
    }

    @Override
    public void exceptionCaught(ChannelHandlerContext ctx, Throwable cause) throws Exception {
        log.warn("Exception caught while serving http query endpoint", cause);
        if (ctx.channel().isActive()) {
            sendError(ctx, new HttpResponseStatus(500, cause.getMessage()));
        }
    }

    @Override
    public void channelRead0(ChannelHandlerContext ctx, FullHttpRequest request) throws Exception {
        messageReceived(ctx, request); // redirect to more sensible netty5 naming scheme
    }

    private static void decodeParameters(QueryStringDecoder urlDecoder, KVPairs kv) {
        for (Map.Entry<String, List<String>> entry : urlDecoder.parameters().entrySet()) {
            String k = entry.getKey();
            String v = entry.getValue().get(0); // ignore duplicates
            kv.add(k, v);
        }
    }

    protected void messageReceived(ChannelHandlerContext ctx, FullHttpRequest request) throws Exception {
        if (!request.getDecoderResult().isSuccess()) {
            sendError(ctx, HttpResponseStatus.BAD_REQUEST);
            return;
        }
        QueryStringDecoder urlDecoder = new QueryStringDecoder(request.getUri());
        String target = urlDecoder.path();
        if (request.getMethod() == HttpMethod.POST) {
            log.trace("POST Method handling triggered for {}", request);
            String postBody = request.content().toString(UTF_8);
            log.trace("POST body {}", postBody);
            urlDecoder = new QueryStringDecoder(postBody, false);
        }
        log.trace("target uri {}", target);
        KVPairs kv = new KVPairs();
        /**
         * The "/query/google/submit" endpoint needs to unpack the
         * "state" parameter into KV pairs.
         */
        if (target.equals("/query/google/submit")) {
            String state = urlDecoder.parameters().get("state").get(0);
            QueryStringDecoder newDecoder = new QueryStringDecoder(state, false);
            decodeParameters(newDecoder, kv);
            if (urlDecoder.parameters().containsKey("code")) {
                kv.add(GoogleDriveAuthentication.authtoken, urlDecoder.parameters().get("code").get(0));
            }
            if (urlDecoder.parameters().containsKey("error")) {
                kv.add(GoogleDriveAuthentication.autherror, urlDecoder.parameters().get("error").get(0));
            }
        } else {
            decodeParameters(urlDecoder, kv);
        }
        log.trace("kv pairs {}", kv);
        switch (target) {
        case "/": {
            sendRedirect(ctx, "/query/index.html");
            break;
        }
        case "/q/": {
            sendRedirect(ctx, "/query/call?" + kv);
            break;
        }
        case "/query/call":
        case "/query/call/": {
            rawQueryCalls.inc();
            queryQueue.queueQuery(meshQueryMaster, kv, request, ctx);
            break;
        }
        case "/query/google/authorization": {
            GoogleDriveAuthentication.gdriveAuthorization(kv, ctx);
            break;
        }
        case "/query/google/submit": {
            boolean success = GoogleDriveAuthentication.gdriveAccessToken(kv, ctx);
            if (success) {
                queryQueue.queueQuery(meshQueryMaster, kv, request, ctx);
            }
            break;
        }
        default:
            fastHandle(ctx, request, target, kv);
            break;
        }
    }

    private void fastHandle(ChannelHandlerContext ctx, FullHttpRequest request, String target, KVPairs kv)
            throws Exception {
        StringBuilderWriter writer = new StringBuilderWriter(50);
        HttpResponse response = HttpUtils.startResponse(writer);
        response.headers().add("Access-Control-Allow-Origin", "*");

        switch (target) {
        case "/metrics": {
            fakeMetricsServlet.writeMetrics(writer, kv);
            break;
        }
        case "/running":
        case "/query/list":
        case "/query/running":
        case "/v2/queries/running.list": {
            Jackson.defaultMapper().writerWithDefaultPrettyPrinter().writeValue(writer, tracker.getRunning());
            break;
        }
        case "/done":
        case "/complete":
        case "/query/done":
        case "/query/complete":
        case "/completed/list":
        case "/v2/queries/finished.list": {
            Jackson.defaultMapper().writerWithDefaultPrettyPrinter().writeValue(writer, tracker.getCompleted());
            break;
        }
        case "/query/all":
        case "/v2/queries/list": {
            Collection<QueryEntryInfo> aggregatingSnapshot = tracker.getRunning();
            aggregatingSnapshot.addAll(tracker.getCompleted());
            Jackson.defaultMapper().writerWithDefaultPrettyPrinter().writeValue(writer, aggregatingSnapshot);
            break;
        }
        case "/cancel":
        case "/query/cancel": {
            if (tracker.cancelRunning(kv.getValue("uuid"))) {
                writer.write("canceled " + kv.getValue("uuid"));
            } else {
                writer.write("canceled failed for " + kv.getValue("uuid"));
                response.setStatus(HttpResponseStatus.INTERNAL_SERVER_ERROR);
            }
            break;
        }
        case "/workers":
        case "/query/workers":
        case "/v2/queries/workers": {
            Map<String, Integer> workerSnapshot = meshQueryMaster.worky().values().stream()
                    .collect(toMap(WorkerData::hostName, WorkerData::queryLeases));
            Jackson.defaultMapper().writerWithDefaultPrettyPrinter().writeValue(writer, workerSnapshot);
            break;
        }
        case "/host":
        case "/host/list":
        case "/v2/host/list":
            String queryStatusUuid = kv.getValue("uuid");
            QueryEntry queryEntry = tracker.getQueryEntry(queryStatusUuid);
            if (queryEntry != null) {
                DetailedStatusHandler hostDetailsHandler = new DetailedStatusHandler(writer, response, ctx, request,
                        queryEntry);
                hostDetailsHandler.handle();
                return;
            } else {
                QueryEntryInfo queryEntryInfo = tracker.getCompletedQueryInfo(queryStatusUuid);
                if (queryEntryInfo != null) {
                    Jackson.defaultMapper().writerWithDefaultPrettyPrinter().writeValue(writer, queryEntryInfo);
                } else {
                    log.trace("could not find query for status");
                    if (ctx.channel().isActive()) {
                        sendError(ctx, new HttpResponseStatus(NOT_FOUND.code(), "could not find query"));
                    }
                    return;
                }
                break;
            }
        case "/git":
        case "/v2/settings/git.properties": {
            try {
                Jackson.defaultMapper().writeValue(writer,
                        ConfigFactory.parseResourcesAnySyntax("/hydra-git.properties").getConfig("git"));
            } catch (Exception ex) {
                String noGitWarning = "Error loading git.properties, possibly jar was not compiled with maven.";
                log.warn(noGitWarning);
                writer.write(noGitWarning);
            }
            break;
        }
        case "/query/encode": {
            Query q = new Query(null, new String[] { kv.getValue("query", kv.getValue("path", "")) }, null);
            JSONArray path = CodecJSON.encodeJSON(q).getJSONArray("path");
            writer.write(path.toString());
            break;
        }
        case "/query/decode": {
            String qo = "{path:" + kv.getValue("query", kv.getValue("path", "")) + "}";
            Query q = CodecJSON.decodeString(Query.class, qo);
            writer.write(q.getPaths()[0]);
            break;
        }
        default:
            // forward to static file server
            ctx.pipeline().addLast(staticFileHandler);
            request.retain();
            ctx.fireChannelRead(request);
            return; // don't do text response clean up
        }
        log.trace("response being sent {}", writer);
        ByteBuf textResponse = ByteBufUtil.encodeString(ctx.alloc(), CharBuffer.wrap(writer.getBuilder()), UTF_8);
        HttpContent content = new DefaultHttpContent(textResponse);
        response.headers().set(HttpHeaders.Names.CONTENT_LENGTH, textResponse.readableBytes());
        if (HttpHeaders.isKeepAlive(request)) {
            response.headers().set(HttpHeaders.Names.CONNECTION, HttpHeaders.Values.KEEP_ALIVE);
        }
        ctx.write(response);
        ctx.write(content);
        ChannelFuture lastContentFuture = ctx.writeAndFlush(LastHttpContent.EMPTY_LAST_CONTENT);
        log.trace("response pending");
        if (!HttpHeaders.isKeepAlive(request)) {
            log.trace("Setting close listener");
            ((Future<Void>) lastContentFuture).addListener(ChannelFutureListener.CLOSE);
        }
    }
}