package com.github.dockerjava.netty; import io.netty.buffer.Unpooled; import io.netty.channel.Channel; import io.netty.channel.ChannelFuture; import io.netty.channel.ChannelFutureListener; import io.netty.channel.socket.DuplexChannel; import io.netty.handler.codec.http.DefaultFullHttpRequest; import io.netty.handler.codec.http.DefaultHttpRequest; import io.netty.handler.codec.http.FullHttpRequest; import io.netty.handler.codec.http.HttpClientCodec; import io.netty.handler.codec.http.HttpClientUpgradeHandler; import io.netty.handler.codec.http.HttpHeaderNames; import io.netty.handler.codec.http.HttpHeaderValues; import io.netty.handler.codec.http.HttpMethod; import io.netty.handler.codec.http.HttpRequest; import io.netty.handler.codec.http.HttpVersion; import io.netty.handler.codec.http.LastHttpContent; import io.netty.handler.codec.json.JsonObjectDecoder; import io.netty.handler.stream.ChunkedStream; import io.netty.handler.stream.ChunkedWriteHandler; import io.netty.util.concurrent.Future; import io.netty.util.concurrent.GenericFutureListener; import java.io.BufferedInputStream; import java.io.IOException; import java.io.InputStream; import java.util.HashMap; import java.util.Map; import java.util.concurrent.CountDownLatch; import com.fasterxml.jackson.core.JsonProcessingException; import com.fasterxml.jackson.core.type.TypeReference; import com.fasterxml.jackson.databind.ObjectMapper; import com.github.dockerjava.api.async.ResultCallback; import com.github.dockerjava.api.exception.DockerClientException; import com.github.dockerjava.api.model.Frame; import com.github.dockerjava.core.async.ResultCallbackTemplate; import com.github.dockerjava.netty.handler.FramedResponseStreamHandler; import com.github.dockerjava.netty.handler.HttpConnectionHijackHandler; import com.github.dockerjava.netty.handler.HttpRequestProvider; import com.github.dockerjava.netty.handler.HttpResponseHandler; import com.github.dockerjava.netty.handler.HttpResponseStreamHandler; import com.github.dockerjava.netty.handler.JsonResponseCallbackHandler; /** * This class is basically a replacement of javax.ws.rs.client.Invocation.Builder to allow simpler migration of JAX-RS code to a netty based * implementation. * * @author Marcus Linke */ public class InvocationBuilder { public class ResponseCallback<T> extends ResultCallbackTemplate<ResponseCallback<T>, T> { private T result = null; public T awaitResult() { try { awaitCompletion(); } catch (InterruptedException e) { throw new RuntimeException(e); } return result; } @Override public void onNext(T object) { result = object; } } public class SkipResultCallback extends ResultCallbackTemplate<ResponseCallback<Void>, Void> { @Override public void onNext(Void object) { } } /** * Implementation of {@link ResultCallback} with the single result event expected. */ public static class AsyncResultCallback<A_RES_T> extends ResultCallbackTemplate<AsyncResultCallback<A_RES_T>, A_RES_T> { private A_RES_T result = null; private final CountDownLatch resultReady = new CountDownLatch(1); @Override public void onNext(A_RES_T object) { onResult(object); } private void onResult(A_RES_T object) { if (resultReady.getCount() == 0) { throw new IllegalStateException("Result has already been set"); } try { result = object; } finally { resultReady.countDown(); } } @Override public void close() throws IOException { try { super.close(); } finally { resultReady.countDown(); } } /** * Blocks until {@link ResultCallback#onNext(Object)} was called for the first time */ @SuppressWarnings("unchecked") public A_RES_T awaitResult() { try { resultReady.await(); } catch (InterruptedException e) { throw new RuntimeException(e); } getFirstError(); return result; } } private ChannelProvider channelProvider; private String resource; private Map<String, String> headers = new HashMap<String, String>(); public InvocationBuilder(ChannelProvider channelProvider, String resource) { this.channelProvider = channelProvider; this.resource = resource; } public InvocationBuilder accept(MediaType mediaType) { return header(HttpHeaderNames.ACCEPT.toString(), mediaType.getMediaType()); } public InvocationBuilder header(String name, String value) { headers.put(name, value); return this; } public void delete() { HttpRequestProvider requestProvider = httpDeleteRequestProvider(); ResponseCallback<Void> callback = new ResponseCallback<Void>(); HttpResponseHandler responseHandler = new HttpResponseHandler(requestProvider, callback); Channel channel = getChannel(); channel.pipeline().addLast(responseHandler); sendRequest(requestProvider, channel); callback.awaitResult(); } public void get(ResultCallback<Frame> resultCallback) { HttpRequestProvider requestProvider = httpGetRequestProvider(); HttpResponseHandler responseHandler = new HttpResponseHandler(requestProvider, resultCallback); FramedResponseStreamHandler streamHandler = new FramedResponseStreamHandler(resultCallback); Channel channel = getChannel(); channel.pipeline().addLast(responseHandler); channel.pipeline().addLast(streamHandler); sendRequest(requestProvider, channel); } public <T> T get(TypeReference<T> typeReference) { ResponseCallback<T> callback = new ResponseCallback<T>(); get(typeReference, callback); return callback.awaitResult(); } public <T> void get(TypeReference<T> typeReference, ResultCallback<T> resultCallback) { HttpRequestProvider requestProvider = httpGetRequestProvider(); Channel channel = getChannel(); JsonResponseCallbackHandler<T> jsonResponseHandler = new JsonResponseCallbackHandler<T>(typeReference, resultCallback); HttpResponseHandler responseHandler = new HttpResponseHandler(requestProvider, resultCallback); channel.pipeline().addLast(responseHandler); channel.pipeline().addLast(new JsonObjectDecoder()); channel.pipeline().addLast(jsonResponseHandler); sendRequest(requestProvider, channel); return; } private DuplexChannel getChannel() { return channelProvider.getChannel(); } private HttpRequestProvider httpDeleteRequestProvider() { return new HttpRequestProvider() { @Override public HttpRequest getHttpRequest(String uri) { return prepareDeleteRequest(uri); } }; } private HttpRequestProvider httpGetRequestProvider() { return new HttpRequestProvider() { @Override public HttpRequest getHttpRequest(String uri) { return prepareGetRequest(uri); } }; } private HttpRequestProvider httpPostRequestProvider(final Object entity) { return new HttpRequestProvider() { @Override public HttpRequest getHttpRequest(String uri) { return preparePostRequest(uri, entity); } }; } private HttpRequestProvider httpPutRequestProvider(final Object entity) { return new HttpRequestProvider() { @Override public HttpRequest getHttpRequest(String uri) { return preparePutRequest(uri, entity); } }; } public InputStream post(final Object entity) { HttpRequestProvider requestProvider = httpPostRequestProvider(entity); Channel channel = getChannel(); AsyncResultCallback<InputStream> callback = new AsyncResultCallback<>(); HttpResponseHandler responseHandler = new HttpResponseHandler(requestProvider, callback); HttpResponseStreamHandler streamHandler = new HttpResponseStreamHandler(callback); channel.pipeline().addLast(responseHandler); channel.pipeline().addLast(streamHandler); sendRequest(requestProvider, channel); return callback.awaitResult(); } public void post(final Object entity, final InputStream stdin, final ResultCallback<Frame> resultCallback) { HttpRequestProvider requestProvider = httpPostRequestProvider(entity); FramedResponseStreamHandler streamHandler = new FramedResponseStreamHandler(resultCallback); final DuplexChannel channel = getChannel(); // result callback's close() method must be called when the servers closes the connection channel.closeFuture().addListener(new GenericFutureListener<Future<? super Void>>() { @Override public void operationComplete(Future<? super Void> future) throws Exception { resultCallback.onComplete(); } }); HttpResponseHandler responseHandler = new HttpResponseHandler(requestProvider, resultCallback); HttpConnectionHijackHandler hijackHandler = new HttpConnectionHijackHandler(responseHandler); HttpClientCodec httpClientCodec = channel.pipeline().get(HttpClientCodec.class); channel.pipeline().addLast( new HttpClientUpgradeHandler(httpClientCodec, hijackHandler, Integer.MAX_VALUE)); channel.pipeline().addLast(streamHandler); sendRequest(requestProvider, channel); // wait for successful http upgrade procedure hijackHandler.awaitUpgrade(); if (stdin != null) { // now we can start a new thread that reads from stdin and writes to the channel new Thread(new Runnable() { private int read(InputStream is, byte[] buf) { try { return is.read(buf); } catch (IOException e) { throw new RuntimeException(e); } } @Override public void run() { byte[] buffer = new byte[1024]; int read; while ((read = read(stdin, buffer)) != -1) { channel.writeAndFlush(Unpooled.copiedBuffer(buffer, 0, read)); } // we close the writing side of the socket, but keep the read side open to transfer stdout/stderr channel.shutdownOutput(); } }).start(); } } public <T> T post(final Object entity, TypeReference<T> typeReference) { ResponseCallback<T> callback = new ResponseCallback<T>(); post(entity, typeReference, callback); return callback.awaitResult(); } public <T> void post(final Object entity, TypeReference<T> typeReference, final ResultCallback<T> resultCallback) { HttpRequestProvider requestProvider = httpPostRequestProvider(entity); Channel channel = getChannel(); JsonResponseCallbackHandler<T> jsonResponseHandler = new JsonResponseCallbackHandler<T>(typeReference, resultCallback); HttpResponseHandler responseHandler = new HttpResponseHandler(requestProvider, resultCallback); channel.pipeline().addLast(responseHandler); channel.pipeline().addLast(new JsonObjectDecoder()); channel.pipeline().addLast(jsonResponseHandler); sendRequest(requestProvider, channel); return; } private HttpRequest prepareDeleteRequest(String uri) { FullHttpRequest request = new DefaultFullHttpRequest(HttpVersion.HTTP_1_1, HttpMethod.DELETE, uri); setDefaultHeaders(request); return request; } private FullHttpRequest prepareGetRequest(String uri) { FullHttpRequest request = new DefaultFullHttpRequest(HttpVersion.HTTP_1_1, HttpMethod.GET, uri); setDefaultHeaders(request); return request; } private HttpRequest preparePostRequest(String uri, Object entity) { return prepareEntityRequest(uri, entity, HttpMethod.POST); } private HttpRequest preparePutRequest(String uri, Object entity) { return prepareEntityRequest(uri, entity, HttpMethod.PUT); } private HttpRequest prepareEntityRequest(String uri, Object entity, HttpMethod httpMethod) { HttpRequest request = null; if (entity != null) { FullHttpRequest fullRequest = new DefaultFullHttpRequest(HttpVersion.HTTP_1_1, httpMethod, uri); byte[] bytes; try { bytes = new ObjectMapper().writeValueAsBytes(entity); } catch (JsonProcessingException e) { throw new RuntimeException(e); } fullRequest.headers().set(HttpHeaderNames.CONTENT_TYPE, "application/json"); fullRequest.content().clear().writeBytes(Unpooled.copiedBuffer(bytes)); fullRequest.headers().set(HttpHeaderNames.CONTENT_LENGTH, bytes.length); request = fullRequest; } else { request = new DefaultHttpRequest(HttpVersion.HTTP_1_1, httpMethod, uri); request.headers().set(HttpHeaderNames.CONTENT_LENGTH, 0); } setDefaultHeaders(request); return request; } private void sendRequest(HttpRequestProvider requestProvider, Channel channel) { ChannelFuture channelFuture = channel.writeAndFlush(requestProvider.getHttpRequest(resource)); channelFuture.addListener(new ChannelFutureListener() { @Override public void operationComplete(ChannelFuture future) throws Exception { } }); } private void setDefaultHeaders(HttpRequest request) { request.headers().set(HttpHeaderNames.HOST, ""); request.headers().set(HttpHeaderNames.CONNECTION, HttpHeaderValues.KEEP_ALIVE); request.headers().set(HttpHeaderNames.ACCEPT_ENCODING, HttpHeaderValues.GZIP); for (Map.Entry<String, String> entry : headers.entrySet()) { request.headers().set((CharSequence) entry.getKey(), entry.getValue()); } } public <T> T post(TypeReference<T> typeReference, InputStream body) { ResponseCallback<T> callback = new ResponseCallback<T>(); post(typeReference, callback, body); return callback.awaitResult(); } public <T> void post(TypeReference<T> typeReference, ResultCallback<T> resultCallback, InputStream body) { HttpRequestProvider requestProvider = httpPostRequestProvider(null); Channel channel = getChannel(); JsonResponseCallbackHandler<T> jsonResponseHandler = new JsonResponseCallbackHandler<T>(typeReference, resultCallback); HttpResponseHandler responseHandler = new HttpResponseHandler(requestProvider, resultCallback); channel.pipeline().addLast(new ChunkedWriteHandler()); channel.pipeline().addLast(responseHandler); channel.pipeline().addLast(new JsonObjectDecoder()); channel.pipeline().addLast(jsonResponseHandler); postChunkedStreamRequest(requestProvider, channel, body); } public void postStream(InputStream body) { SkipResultCallback resultCallback = new SkipResultCallback(); HttpRequestProvider requestProvider = httpPostRequestProvider(null); Channel channel = getChannel(); HttpResponseHandler responseHandler = new HttpResponseHandler(requestProvider, resultCallback); channel.pipeline().addLast(new ChunkedWriteHandler()); channel.pipeline().addLast(responseHandler); postChunkedStreamRequest(requestProvider, channel, body); try { resultCallback.awaitCompletion(); } catch (InterruptedException e) { throw new RuntimeException(e); } } private void postChunkedStreamRequest(HttpRequestProvider requestProvider, Channel channel, InputStream body) { HttpRequest request = requestProvider.getHttpRequest(resource); // don't accept FullHttpRequest here if (request instanceof FullHttpRequest) { throw new DockerClientException("fatal: request is instance of FullHttpRequest"); } request.headers().set(HttpHeaderNames.TRANSFER_ENCODING, HttpHeaderValues.CHUNKED); request.headers().remove(HttpHeaderNames.CONTENT_LENGTH); channel.write(request); channel.write(new ChunkedStream(new BufferedInputStream(body, 1024 * 1024), 1024 * 1024)); channel.write(LastHttpContent.EMPTY_LAST_CONTENT); channel.flush(); } public InputStream get() { HttpRequestProvider requestProvider = httpGetRequestProvider(); Channel channel = getChannel(); AsyncResultCallback<InputStream> resultCallback = new AsyncResultCallback<>(); HttpResponseHandler responseHandler = new HttpResponseHandler(requestProvider, resultCallback); HttpResponseStreamHandler streamHandler = new HttpResponseStreamHandler(resultCallback); channel.pipeline().addLast(responseHandler); channel.pipeline().addLast(streamHandler); sendRequest(requestProvider, channel); return resultCallback.awaitResult(); } public void put(InputStream body, MediaType mediaType) { HttpRequestProvider requestProvider = httpPutRequestProvider(null); Channel channel = getChannel(); ResponseCallback<Void> resultCallback = new ResponseCallback<Void>(); HttpResponseHandler responseHandler = new HttpResponseHandler(requestProvider, resultCallback); channel.pipeline().addLast(new ChunkedWriteHandler()); channel.pipeline().addLast(responseHandler); HttpRequest request = requestProvider.getHttpRequest(resource); // don't accept FullHttpRequest here if (request instanceof FullHttpRequest) { throw new DockerClientException("fatal: request is instance of FullHttpRequest"); } request.headers().set(HttpHeaderNames.TRANSFER_ENCODING, HttpHeaderValues.CHUNKED); request.headers().remove(HttpHeaderNames.CONTENT_LENGTH); request.headers().set(HttpHeaderNames.CONTENT_TYPE, mediaType.getMediaType()); channel.write(request); channel.write(new ChunkedStream(new BufferedInputStream(body, 1024 * 1024))); channel.writeAndFlush(LastHttpContent.EMPTY_LAST_CONTENT); resultCallback.awaitResult(); }; }