etcd.client.HttpClient.java Source code

Java tutorial

Introduction

Here is the source code for etcd.client.HttpClient.java

Source

/*
 *   Copyright (c) 2014 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 etcd.client;

import io.netty.bootstrap.Bootstrap;
import io.netty.channel.Channel;
import io.netty.channel.ChannelFuture;
import io.netty.channel.ChannelHandlerContext;
import io.netty.channel.ChannelInboundHandlerAdapter;
import io.netty.channel.ChannelInitializer;
import io.netty.channel.ChannelOption;
import io.netty.channel.ChannelPipeline;
import io.netty.channel.EventLoopGroup;
import io.netty.channel.socket.SocketChannel;
import io.netty.channel.socket.nio.NioSocketChannel;
import io.netty.handler.codec.http.DefaultFullHttpResponse;
import io.netty.handler.codec.http.FullHttpRequest;
import io.netty.handler.codec.http.HttpClientCodec;
import io.netty.handler.codec.http.HttpHeaders;
import io.netty.handler.codec.http.HttpObjectAggregator;
import io.netty.handler.codec.http.HttpResponseStatus;
import io.netty.util.AttributeKey;
import io.netty.util.ReferenceCounted;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

import java.net.InetSocketAddress;
import java.net.URI;
import java.util.Collections;
import java.util.Iterator;
import java.util.concurrent.Executor;
import java.util.function.Consumer;

/**
 * Light HTTP client wrapper around Netty.
 */
// TODO Remove bad hosts from selection pool after error, return after timeout
class HttpClient {

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

    private static AttributeKey<Consumer<Response>> ATTRIBUTE_KEY = AttributeKey
            .valueOf(HttpClient.class.getName() + "-attribute");
    private static AttributeKey<FullHttpRequest> REQUEST_KEY = AttributeKey
            .valueOf(HttpClient.class.getName() + "-request");

    private final EventLoopGroup eventLoopGroup;
    private final Bootstrap bootstrap;
    private final Executor executor;

    private final ServerList servers;
    private final boolean autoReconnect;
    //   private final List<Channel> channelPool = new ArrayList<>();
    //
    //   private final Object lock = new Object();

    public HttpClient(EventLoopGroup eventLoopGroup, Executor executor, ServerList servers, boolean autoReconnect) {
        this.eventLoopGroup = eventLoopGroup;
        this.executor = executor;
        this.servers = servers;
        this.autoReconnect = autoReconnect;
        bootstrap = new Bootstrap().group(eventLoopGroup).channel(NioSocketChannel.class)
                .option(ChannelOption.SO_REUSEADDR, true).handler(new ChannelInitializer<SocketChannel>() {
                    @Override
                    protected void initChannel(SocketChannel channel) throws Exception {
                        final ChannelPipeline pipeline = channel.pipeline();

                        pipeline.addLast(new HttpClientCodec(4096, 8192, 8192, true),
                                new HttpObjectAggregator(1024 * 1024), new HttpClientHandler());
                    }
                }).validate();
    }

    public void send(FullHttpRequest request, Consumer<Response> completionHandler) {
        // TODO Add support for TLS
        // TODO Add support for TLS client authentication
        send(servers.serverIterator(), request, completionHandler);
    }

    private void send(Iterator<ServerList.Server> serverIterator, FullHttpRequest request,
            Consumer<Response> completionHandler) {
        final ServerList.Server server = serverIterator.next();
        final URI address = server.getAddress();
        final ChannelFuture connectFuture = bootstrap.connect(address.getHost(), address.getPort());
        final FullHttpRequest requestCopy = request.copy();
        requestCopy.retain();
        final Channel channel = connectFuture.channel();
        channel.attr(REQUEST_KEY).set(requestCopy);
        channel.attr(ATTRIBUTE_KEY).set(completionHandler);
        connectFuture.addListener((future) -> {
            if (future.isSuccess()) {
                channel.writeAndFlush(request);
            } else {
                server.connectionFailed();
                if (autoReconnect && serverIterator.hasNext()) {
                    send(serverIterator, request, completionHandler);
                } else {
                    invokeCompletionHandler(completionHandler,
                            new Response(null, new EtcdException(future.cause())));
                }
            }
        });
    }

    private void invokeCompletionHandler(Consumer<Response> completionHandler, Response response) {
        executor.execute(() -> completionHandler.accept(response));
    }

    public EventLoopGroup getEventLoopGroup() {
        return eventLoopGroup;
    }

    class HttpClientHandler extends ChannelInboundHandlerAdapter {
        @Override
        public void channelRead(ChannelHandlerContext ctx, Object msg) throws Exception {
            final FullHttpRequest request = ctx.channel().attr(REQUEST_KEY).getAndRemove();
            try {
                final Consumer<Response> completionCallbackHandler = ctx.channel().attr(ATTRIBUTE_KEY)
                        .getAndRemove();
                if (completionCallbackHandler == null) {
                    throw new IllegalStateException("Received a response with nothing to handle it.");
                }
                final DefaultFullHttpResponse response = (DefaultFullHttpResponse) msg;

                if (response.getStatus().equals(HttpResponseStatus.MOVED_PERMANENTLY)
                        || response.getStatus().equals(HttpResponseStatus.TEMPORARY_REDIRECT)) {
                    final URI locationUri = URI.create(response.headers().get(HttpHeaders.Names.LOCATION));
                    final URI serverUri;
                    if (locationUri.isAbsolute()) {
                        serverUri = locationUri;
                        request.headers().set(HttpHeaders.Names.HOST, serverUri.getHost());
                    } else {
                        final InetSocketAddress address = (InetSocketAddress) ctx.channel().remoteAddress();
                        serverUri = URI.create("http://" + address.getHostString() + ":" + address.getPort());
                    }
                    request.setUri(
                            locationUri.getPath() + (locationUri.getQuery() == null ? "" : locationUri.getQuery()));
                    final Iterator<ServerList.Server> serverIterator = Collections
                            .singleton(new ServerList.Server(serverUri)).iterator();
                    request.retain();
                    send(serverIterator, request, completionCallbackHandler);
                } else {
                    response.retain();
                    invokeCompletionHandler(completionCallbackHandler, new Response(response, null));
                }
            } finally {
                request.release();
                if (msg instanceof ReferenceCounted) {
                    ((ReferenceCounted) msg).release();
                }
            }
            ctx.close();
        }

        @Override
        public void channelInactive(ChannelHandlerContext ctx) throws Exception {
            //         synchronized (lock) {
            //            channelPool.remove(ctx.channel());
            //         }
            final Consumer<Response> completionCallbackHandler = ctx.channel().attr(ATTRIBUTE_KEY).getAndRemove();
            if (completionCallbackHandler != null) {
                invokeCompletionHandler(completionCallbackHandler,
                        new Response(null, new EtcdException("Connection closed unexpectedly")));
            }
        }

        @Override
        public void exceptionCaught(ChannelHandlerContext ctx, Throwable cause) throws Exception {
            final Consumer<Response> completionCallbackHandler = ctx.channel().attr(ATTRIBUTE_KEY).getAndRemove();
            if (completionCallbackHandler != null) {
                final Response response;
                if (cause instanceof EtcdException) {
                    response = new Response(null, (EtcdException) cause);
                } else {
                    response = new Response(null, new EtcdException(cause));
                }
                invokeCompletionHandler(completionCallbackHandler, response);
            } else {
                LOGGER.error("Error processing server request", cause);
            }
            ctx.channel().close();
        }
    }

    class Response {
        private final DefaultFullHttpResponse response;
        private final EtcdException exception;

        Response(DefaultFullHttpResponse response, EtcdException exception) {
            this.response = response;
            this.exception = exception;
        }

        public DefaultFullHttpResponse getHttpResponse() {
            if (exception != null) {
                throw exception;
            }
            return response;
        }
    }

}