org.thingsplode.synapse.endpoint.handlers.HttpRequestHandler.java Source code

Java tutorial

Introduction

Here is the source code for org.thingsplode.synapse.endpoint.handlers.HttpRequestHandler.java

Source

/*
 * Copyright 2016 tamas.csaba@gmail.com.
 *
 * 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 org.thingsplode.synapse.endpoint.handlers;

import io.netty.channel.ChannelFuture;
import io.netty.channel.ChannelFutureListener;
import io.netty.channel.ChannelHandlerContext;
import io.netty.channel.SimpleChannelInboundHandler;
import io.netty.handler.codec.http.FullHttpRequest;
import io.netty.handler.codec.http.HttpHeaderNames;
import io.netty.handler.codec.http.HttpHeaders;
import io.netty.handler.codec.http.HttpMethod;
import io.netty.handler.codec.http.HttpResponseStatus;
import io.netty.handler.codec.http.websocketx.WebSocketServerHandshaker;
import io.netty.handler.codec.http.websocketx.WebSocketServerHandshakerFactory;
import java.io.UnsupportedEncodingException;
import java.util.Map.Entry;
import java.util.Optional;
import java.util.concurrent.atomic.AtomicLong;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.thingsplode.synapse.core.AbstractMessage;
import org.thingsplode.synapse.core.Request;
import org.thingsplode.synapse.core.RequestMethod;
import org.thingsplode.synapse.core.Uri;
import org.thingsplode.synapse.endpoint.Endpoint;
import org.thingsplode.synapse.util.Util;
import static org.thingsplode.synapse.endpoint.handlers.HttpResponseHandler.sendError;

/**
 *
 * @author Csaba Tamas
 */
//todo: cleanup the content from below
//https://github.com/RestExpress/RestExpress/blob/master/core/src/test/java/org/restexpress/contenttype/MediaTypeParserTest.java
//https://github.com/jwboardman/khs-stockticker/blob/master/src/main/java/com/khs/stockticker/StockTickerServerHandler.java
//https://keyholesoftware.com/2015/03/16/netty-a-different-kind-of-websocket-server/
//http://microservices.io/patterns/microservices.html
//http://netty.io/4.0/api/io/netty/channel/ChannelPipeline.html
public class HttpRequestHandler extends SimpleChannelInboundHandler<FullHttpRequest> {

    private static final Logger logger = LoggerFactory.getLogger(HttpRequestHandler.class);
    public static final String UPGRADE_TO_WEBSOCKET = "websocket";
    private final String endpointId;
    private boolean pipelining = false;
    private final AtomicLong sequence = new AtomicLong(0);

    public HttpRequestHandler(String endpointId, boolean pipelining) {
        this.endpointId = endpointId;
        this.pipelining = pipelining;
    }

    @Override
    protected void channelRead0(ChannelHandlerContext ctx, FullHttpRequest httpRequest) throws Exception {
        //todo: support for API keys
        ///endpoints/json?api_key=565656
        try {
            // Handle a bad request.
            if (!httpRequest.decoderResult().isSuccess()) {
                HttpResponseHandler.sendError(ctx, HttpResponseStatus.BAD_REQUEST, "Could not decode request.",
                        httpRequest);
                return;
            }

            if (httpRequest.method().equals(HttpMethod.HEAD) || httpRequest.method().equals(HttpMethod.PATCH)
                    || httpRequest.method().equals(HttpMethod.TRACE)
                    || httpRequest.method().equals(HttpMethod.CONNECT)
                    || httpRequest.method().equals(HttpMethod.OPTIONS)) {
                HttpResponseHandler.sendError(ctx, HttpResponseStatus.FORBIDDEN,
                        "Method forbidden (The following are not supported: HEAD, PATCH, TRACE, CONNECT, OPTIONS).",
                        httpRequest);
                return;
            }

            //check websocket upgrade request
            String upgradeHeader = httpRequest.headers().get(HttpHeaderNames.UPGRADE);
            if (!Util.isEmpty(upgradeHeader) && UPGRADE_TO_WEBSOCKET.equalsIgnoreCase(upgradeHeader)) {
                //case websocket upgrade request is detected -> Prepare websocket handshake
                upgradeToWebsocket(ctx, httpRequest);
                return;
            } else {
                //case simple http request
                Request request = new Request(prepareHeader(ctx, httpRequest));

                //no information about the object type / it will be processed in a later stage
                //todo: with requestbodytype header value early deserialization would be possible, however not beneficial in routing cases
                request.setBody(httpRequest.content());
                ctx.fireChannelRead(request);
            }
        } catch (Exception ex) {
            logger.error("Channel read error: " + ex.getMessage(), ex);
            HttpResponseHandler.sendError(ctx, HttpResponseStatus.INTERNAL_SERVER_ERROR,
                    ex.getClass().getSimpleName() + ": " + ex.getMessage(), httpRequest);
        }
    }

    private void upgradeToWebsocket(ChannelHandlerContext ctx, FullHttpRequest httpRequest) {

        String wsUrl = "ws://" + httpRequest.headers().get(HttpHeaderNames.HOST) + "/" + endpointId;
        //todo: configure frame size
        WebSocketServerHandshakerFactory wsHandshakeFactory = new WebSocketServerHandshakerFactory(wsUrl, null,
                false);
        WebSocketServerHandshaker handshaker = wsHandshakeFactory.newHandshaker(httpRequest);
        if (handshaker == null) {
            WebSocketServerHandshakerFactory.sendUnsupportedVersionResponse(ctx.channel());
        } else {
            handshaker.handshake(ctx.channel(), httpRequest)
                    .addListener((ChannelFutureListener) new ChannelFutureListener() {
                        @Override
                        public void operationComplete(ChannelFuture future) throws Exception {
                            if (future.isSuccess()) {
                                logger.debug("Switching to websocket. Replacing the "
                                        + Endpoint.HTTP_RESPONSE_HANDLER + " with " + Endpoint.WS_RESPONSE_HANDLER);
                                ctx.pipeline().replace(Endpoint.HTTP_REQUEST_HANDLER, Endpoint.WS_REQUEST_HANDLER,
                                        new WebsocketRequestHandler(handshaker));
                                ctx.pipeline().replace(Endpoint.HTTP_RESPONSE_HANDLER, Endpoint.WS_RESPONSE_HANDLER,
                                        new WebsocketResponseHandler());
                                ctx.pipeline().addAfter(Endpoint.WS_RESPONSE_HANDLER, Endpoint.WS_COMMAND_HANDLER,
                                        new Command2WsEncoder());
                                if (ctx.pipeline().get(Endpoint.RESPONSE_INTROSPECTOR) != null) {
                                    ctx.pipeline().addAfter(Endpoint.RESPONSE_INTROSPECTOR,
                                            Endpoint.WS_REQUEST_INTROSPECTOR, new WebsocketIntrospector());
                                }
                            } else {
                                String msg = "Dispatching upgrade acknowledgement was not successfull due to ";
                                if (future.cause() != null) {
                                    logger.error(msg + future.cause().getClass().getSimpleName() + " with message: "
                                            + future.cause().getMessage(), future.cause());
                                } else {
                                    logger.error(msg + " unknown reasons.");
                                }
                            }
                        }
                    });
        }
    }

    private Request.RequestHeader prepareHeader(ChannelHandlerContext ctx, FullHttpRequest httpRequest) {
        Request.RequestHeader header = null;
        Optional<Entry<String, String>> msgIdOpt = httpRequest.headers().entries().stream()
                .filter(e -> e.getKey().equalsIgnoreCase(AbstractMessage.PROP_MESSAGE_ID)).findFirst();
        String msgId = msgIdOpt.isPresent() ? msgIdOpt.get().getValue() : null;
        try {
            header = new Request.RequestHeader(msgId, new Uri(httpRequest.uri()),
                    RequestMethod.fromHttpMethod(httpRequest.method()));
            header.addAllProperties(httpRequest.headers());
            if (HttpHeaders.isKeepAlive(httpRequest)) {
                header.setKeepalive(true);
            } else {
                header.setKeepalive(false);
            }
            if (pipelining) {
                header.addProperty(Request.RequestHeader.MSG_SEQ, String.valueOf(sequence.getAndIncrement()));
            }
            header.addProperty(AbstractMessage.PROP_RCV_TRANSPORT, AbstractMessage.PROP_HTTP_TRANSPORT);
        } catch (UnsupportedEncodingException ex) {
            logger.error(ex.getMessage(), ex);
            HttpResponseHandler.sendError(ctx, HttpResponseStatus.BAD_REQUEST,
                    ex.getClass().getSimpleName() + ": " + ex.getMessage(), httpRequest);
        }
        return header;
    }

    @Override
    public void exceptionCaught(ChannelHandlerContext ctx, Throwable cause) {
        logger.error("Error while processing HTTP request: " + cause.getMessage(), cause);
        sendError(ctx, HttpResponseStatus.INTERNAL_SERVER_ERROR,
                cause.getClass().getSimpleName() + ": " + cause.getMessage());
    }

}