cf.component.http.SimpleHttpServer.java Source code

Java tutorial

Introduction

Here is the source code for cf.component.http.SimpleHttpServer.java

Source

/*
 *   Copyright (c) 2012 Intellectual Reserve, 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 cf.component.http;

import io.netty.bootstrap.ServerBootstrap;
import io.netty.buffer.ByteBuf;
import io.netty.buffer.Unpooled;
import io.netty.channel.ChannelFutureListener;
import io.netty.channel.ChannelHandlerContext;
import io.netty.channel.ChannelInboundHandlerAdapter;
import io.netty.channel.ChannelInitializer;
import io.netty.channel.ChannelPipeline;
import io.netty.channel.socket.SocketChannel;
import io.netty.channel.nio.NioEventLoopGroup;
import io.netty.channel.socket.nio.NioServerSocketChannel;
import io.netty.handler.codec.http.DefaultFullHttpResponse;
import io.netty.handler.codec.http.FullHttpRequest;
import io.netty.handler.codec.http.FullHttpResponse;
import io.netty.handler.codec.http.HttpHeaders;
import io.netty.handler.codec.http.HttpObjectAggregator;
import io.netty.handler.codec.http.HttpRequestDecoder;
import io.netty.handler.codec.http.HttpResponse;
import io.netty.handler.codec.http.HttpResponseEncoder;
import io.netty.handler.codec.http.HttpResponseStatus;
import io.netty.handler.codec.http.HttpVersion;
import io.netty.handler.stream.ChunkedWriteHandler;
import io.netty.util.CharsetUtil;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

import java.io.Closeable;
import java.net.SocketAddress;
import java.util.ArrayList;
import java.util.List;
import java.util.concurrent.Executor;
import java.util.regex.Matcher;
import java.util.regex.Pattern;

/**
 * Provides a simple embeddable HTTP server for handling simple web service end-points and things like publishing
 * /varz and /healthz information and Cloud Controller REST calls to service gateways.
 *
 * @author "Mike Heath"
 */
public class SimpleHttpServer implements Closeable {

    private static final Logger LOGGER = LoggerFactory.getLogger(SimpleHttpServer.class);

    // Access must be synchronized on self
    private final List<RequestHandle> requestHandles = new ArrayList<>();

    private final NioEventLoopGroup parentGroup;
    private final NioEventLoopGroup childGroup;

    private final Executor executor;

    public SimpleHttpServer(SocketAddress localAddress) {
        parentGroup = new NioEventLoopGroup();
        childGroup = new NioEventLoopGroup();
        bootstrapServer(localAddress, parentGroup, childGroup);
        executor = createLocalThreadExecutor();
    }

    public SimpleHttpServer(SocketAddress localAddress, NioEventLoopGroup parentGroup, NioEventLoopGroup childGroup,
            Executor executor) {
        this.parentGroup = null;
        this.childGroup = null;
        bootstrapServer(localAddress, parentGroup, childGroup);
        if (executor == null) {
            this.executor = createLocalThreadExecutor();
        } else {
            this.executor = executor;
        }
    }

    private Executor createLocalThreadExecutor() {
        return new Executor() {
            @Override
            public void execute(Runnable command) {
                command.run();
            }
        };
    }

    private void bootstrapServer(SocketAddress localAddress, NioEventLoopGroup parentGroup,
            NioEventLoopGroup childGroup) {
        new ServerBootstrap().group(parentGroup, childGroup).channel(NioServerSocketChannel.class)
                .localAddress(localAddress).childHandler(new SimpleHttpServerInitializer()).bind()
                .awaitUninterruptibly(); // Make sure the server is bound before the constructor returns.

        LOGGER.info("Server listening on {}", localAddress);
    }

    @Override
    public void close() {
        if (parentGroup != null) {
            parentGroup.shutdownGracefully();
        }
        if (childGroup != null) {
            childGroup.shutdownGracefully();
        }
    }

    public void addHandler(Pattern uriPattern, RequestHandler requestHandler) {
        LOGGER.debug("Registering pattern {}", uriPattern.pattern());
        synchronized (requestHandles) {
            requestHandles.add(new RequestHandle(uriPattern, requestHandler));
        }
    }

    private class SimpleHttpServerInitializer extends ChannelInitializer<SocketChannel> {
        @Override
        public void initChannel(SocketChannel ch) throws Exception {
            final ChannelPipeline pipeline = ch.pipeline();

            pipeline.addLast("decoder", new HttpRequestDecoder());
            pipeline.addLast("aggregator", new HttpObjectAggregator(65536));
            pipeline.addLast("encoder", new HttpResponseEncoder());
            pipeline.addLast("chunkedWriter", new ChunkedWriteHandler());

            pipeline.addLast("handler", new SimpleHttpServerHandler());
        }
    }

    private class SimpleHttpServerHandler extends ChannelInboundHandlerAdapter {
        @Override
        public void channelRead(final ChannelHandlerContext context, final Object message) throws Exception {
            final FullHttpRequest request = (FullHttpRequest) message;
            if (LOGGER.isDebugEnabled()) {
                LOGGER.debug("Method: {}", request.getMethod());
                LOGGER.debug("URI: " + request.getUri());
                for (String name : request.headers().names()) {
                    for (String value : request.headers().getAll(name)) {
                        LOGGER.debug("{}: {}", name, value);
                    }
                }
                LOGGER.debug("Request body: {}", request.content().toString(CharsetUtil.UTF_8));
                LOGGER.debug("=== End of Request ===============");
            }

            if (!request.getDecoderResult().isSuccess()) {
                sendError(context, HttpResponseStatus.BAD_REQUEST, "Bad request");
                return;
            }

            final String uri = request.getUri();
            final RequestHandler requestHandler;
            final Matcher uriMatcher;
            lock: synchronized (requestHandles) {
                for (RequestHandle handle : requestHandles) {
                    final Matcher matcher = handle.uriPattern.matcher(uri);
                    if (matcher.matches()) {
                        requestHandler = handle.handler;
                        uriMatcher = matcher;
                        break lock;
                    }
                }
                requestHandler = null;
                uriMatcher = null;
            }

            if (requestHandler != null) {
                // Copy buffer to make sure it's accessible if request is handled by another thread.
                final ByteBuf content = Unpooled.copiedBuffer(request.content());
                executor.execute(new Runnable() {
                    @Override
                    public void run() {
                        try {
                            final HttpResponse httpResponse = requestHandler.handleRequest(request, uriMatcher,
                                    content);
                            // Close the connection as soon as the message is sent.
                            context.write(httpResponse).addListener(ChannelFutureListener.CLOSE);
                            context.flush();
                        } catch (Exception e) {
                            exceptionCaught(context, e);
                        }
                    }
                });
            } else {
                LOGGER.debug("Returning 404");
                sendError(context, HttpResponseStatus.NOT_FOUND, "Not found");
            }
        }

        @Override
        public void channelActive(ChannelHandlerContext ctx) throws Exception {
            LOGGER.debug("Received connection from {}", ctx.channel().remoteAddress());
        }

        @Override
        public void channelInactive(ChannelHandlerContext ctx) throws Exception {
            LOGGER.debug("Connection to {} closed", ctx.channel().remoteAddress());
        }

        @Override
        public void exceptionCaught(ChannelHandlerContext ctx, Throwable cause) {
            LOGGER.error(cause.getMessage(), cause);
            if (ctx.channel().isOpen()) {
                if (cause instanceof RequestException) {
                    sendError(ctx, ((RequestException) cause).getStatus(), cause.getMessage());
                } else {
                    sendError(ctx, HttpResponseStatus.INTERNAL_SERVER_ERROR, cause.getMessage());
                }
            }
        }

        private void sendError(ChannelHandlerContext context, HttpResponseStatus status, String message) {
            final ByteBuf buf = Unpooled.copiedBuffer("Failure: " + message + "\r\n", CharsetUtil.UTF_8);
            FullHttpResponse response = new DefaultFullHttpResponse(HttpVersion.HTTP_1_1, status, buf);
            response.headers().add(HttpHeaders.Names.CONTENT_TYPE, "text/plain; charset=UTF-8");

            // Close the connection as soon as the error message is sent.
            context.write(response).addListener(ChannelFutureListener.CLOSE);
            context.flush();
        }
    }

    private class RequestHandle {
        private final Pattern uriPattern;
        private final RequestHandler handler;

        private RequestHandle(Pattern uriPattern, RequestHandler handler) {
            this.uriPattern = uriPattern;
            this.handler = handler;
        }
    }
}