From b18ab0e76a4ae641730c273d192b44312c407d3b Mon Sep 17 00:00:00 2001 From: Simone Bordet Date: Tue, 4 Sep 2012 19:20:29 +0200 Subject: [PATCH] Jetty9 - First take at HTTP client implementation. --- .../client/BufferingResponseListener.java | 77 ++++ .../client/ByteBufferContentProvider.java | 32 ++ .../org/eclipse/jetty/client/HTTPClient.java | 294 ------------ .../org/eclipse/jetty/client/HttpClient.java | 430 ++++++++++++++++++ .../eclipse/jetty/client/HttpConnection.java | 106 +++++ .../jetty/client/HttpConversation.java | 77 ++++ .../eclipse/jetty/client/HttpDestination.java | 200 ++++++++ .../eclipse/jetty/client/HttpReceiver.java | 208 +++++++++ .../org/eclipse/jetty/client/HttpRequest.java | 259 +++++++++++ ...tandardResponse.java => HttpResponse.java} | 66 ++- .../jetty/client/HttpResponseException.java | 28 ++ .../org/eclipse/jetty/client/HttpSender.java | 319 +++++++++++++ .../jetty/client/PathContentProvider.java | 91 ++++ .../jetty/client/RedirectionListener.java | 42 ++ .../jetty/client/StandardDestination.java | 98 ---- .../eclipse/jetty/client/StandardRequest.java | 126 ----- ...er.java => StreamingResponseListener.java} | 2 +- .../eclipse/jetty/client/api/Connection.java | 4 +- .../jetty/client/api/ContentProvider.java | 5 +- .../eclipse/jetty/client/api/Destination.java | 11 +- .../org/eclipse/jetty/client/api/Headers.java | 30 -- .../org/eclipse/jetty/client/api/Request.java | 114 +++-- .../eclipse/jetty/client/api/Response.java | 78 ++-- .../jetty/client/AbstractHttpClientTest.java | 35 ++ ...TTPClientTest.java => HttpClientTest.java} | 35 +- .../jetty/client/HttpReceiverTest.java | 160 +++++++ .../eclipse/jetty/client/HttpSenderTest.java | 266 +++++++++++ .../eclipse/jetty/client/RedirectionTest.java | 58 +++ .../eclipse/jetty/client/api/ApacheUsage.java | 2 + .../eclipse/jetty/client/api/NingUsage.java | 2 + .../org/eclipse/jetty/client/api/Usage.java | 131 +++--- .../test/resources/jetty-logging.properties | 2 + .../org/eclipse/jetty/http/HttpFields.java | 91 ++-- .../org/eclipse/jetty/http/HttpParser.java | 2 +- .../eclipse/jetty/http/HttpParserTest.java | 4 +- .../eclipse/jetty/io/ByteArrayEndPoint.java | 31 +- .../org/eclipse/jetty/io/ChannelEndPoint.java | 4 +- .../jetty/io/MappedByteBufferPool.java | 3 + .../jetty/io/SelectChannelEndPoint.java | 3 +- .../org/eclipse/jetty/io/WriteFlusher.java | 4 +- .../eclipse/jetty/server/HttpConnection.java | 6 +- .../org/eclipse/jetty/util/BufferUtil.java | 103 +++-- 42 files changed, 2754 insertions(+), 885 deletions(-) create mode 100644 jetty-client-new/src/main/java/org/eclipse/jetty/client/BufferingResponseListener.java create mode 100644 jetty-client-new/src/main/java/org/eclipse/jetty/client/ByteBufferContentProvider.java delete mode 100644 jetty-client-new/src/main/java/org/eclipse/jetty/client/HTTPClient.java create mode 100644 jetty-client-new/src/main/java/org/eclipse/jetty/client/HttpClient.java create mode 100644 jetty-client-new/src/main/java/org/eclipse/jetty/client/HttpConnection.java create mode 100644 jetty-client-new/src/main/java/org/eclipse/jetty/client/HttpConversation.java create mode 100644 jetty-client-new/src/main/java/org/eclipse/jetty/client/HttpDestination.java create mode 100644 jetty-client-new/src/main/java/org/eclipse/jetty/client/HttpReceiver.java create mode 100644 jetty-client-new/src/main/java/org/eclipse/jetty/client/HttpRequest.java rename jetty-client-new/src/main/java/org/eclipse/jetty/client/{StandardResponse.java => HttpResponse.java} (54%) create mode 100644 jetty-client-new/src/main/java/org/eclipse/jetty/client/HttpResponseException.java create mode 100644 jetty-client-new/src/main/java/org/eclipse/jetty/client/HttpSender.java create mode 100644 jetty-client-new/src/main/java/org/eclipse/jetty/client/PathContentProvider.java create mode 100644 jetty-client-new/src/main/java/org/eclipse/jetty/client/RedirectionListener.java delete mode 100644 jetty-client-new/src/main/java/org/eclipse/jetty/client/StandardDestination.java delete mode 100644 jetty-client-new/src/main/java/org/eclipse/jetty/client/StandardRequest.java rename jetty-client-new/src/main/java/org/eclipse/jetty/client/{StreamResponseListener.java => StreamingResponseListener.java} (94%) delete mode 100644 jetty-client-new/src/main/java/org/eclipse/jetty/client/api/Headers.java create mode 100644 jetty-client-new/src/test/java/org/eclipse/jetty/client/AbstractHttpClientTest.java rename jetty-client-new/src/test/java/org/eclipse/jetty/client/{HTTPClientTest.java => HttpClientTest.java} (66%) create mode 100644 jetty-client-new/src/test/java/org/eclipse/jetty/client/HttpReceiverTest.java create mode 100644 jetty-client-new/src/test/java/org/eclipse/jetty/client/HttpSenderTest.java create mode 100644 jetty-client-new/src/test/java/org/eclipse/jetty/client/RedirectionTest.java create mode 100644 jetty-client-new/src/test/resources/jetty-logging.properties diff --git a/jetty-client-new/src/main/java/org/eclipse/jetty/client/BufferingResponseListener.java b/jetty-client-new/src/main/java/org/eclipse/jetty/client/BufferingResponseListener.java new file mode 100644 index 00000000000..7e5792d0679 --- /dev/null +++ b/jetty-client-new/src/main/java/org/eclipse/jetty/client/BufferingResponseListener.java @@ -0,0 +1,77 @@ +package org.eclipse.jetty.client; + +import java.nio.ByteBuffer; +import java.nio.charset.Charset; +import java.util.concurrent.CountDownLatch; +import java.util.concurrent.ExecutionException; +import java.util.concurrent.TimeUnit; +import java.util.concurrent.TimeoutException; + +import org.eclipse.jetty.client.api.Response; + +public class BufferingResponseListener extends Response.Listener.Adapter +{ + private final CountDownLatch latch = new CountDownLatch(1); + private final int maxCapacity; + private Response response; + private Throwable failure; + private byte[] buffer = new byte[0]; + + public BufferingResponseListener() + { + this(16 * 1024 * 1024); + } + + public BufferingResponseListener(int maxCapacity) + { + this.maxCapacity = maxCapacity; + } + + @Override + public void onContent(Response response, ByteBuffer content) + { + long newLength = buffer.length + content.remaining(); + if (newLength > maxCapacity) + throw new IllegalStateException("Buffering capacity exceeded"); + + byte[] newBuffer = new byte[(int)newLength]; + System.arraycopy(buffer, 0, newBuffer, 0, buffer.length); + content.get(newBuffer, buffer.length, content.remaining()); + buffer = newBuffer; + } + + @Override + public void onSuccess(Response response) + { + this.response = response; + latch.countDown(); + } + + @Override + public void onFailure(Response response, Throwable failure) + { + this.response = response; + this.failure = failure; + latch.countDown(); + } + + public Response await(long timeout, TimeUnit unit) throws InterruptedException, ExecutionException, TimeoutException + { + boolean expired = !latch.await(timeout, unit); + if (failure != null) + throw new ExecutionException(failure); + if (expired) + throw new TimeoutException(); + return response; + } + + public byte[] content() + { + return buffer; + } + + public String contentAsString(String encoding) + { + return new String(content(), Charset.forName(encoding)); + } +} diff --git a/jetty-client-new/src/main/java/org/eclipse/jetty/client/ByteBufferContentProvider.java b/jetty-client-new/src/main/java/org/eclipse/jetty/client/ByteBufferContentProvider.java new file mode 100644 index 00000000000..a2de4139e8e --- /dev/null +++ b/jetty-client-new/src/main/java/org/eclipse/jetty/client/ByteBufferContentProvider.java @@ -0,0 +1,32 @@ +package org.eclipse.jetty.client; + +import java.nio.ByteBuffer; +import java.util.Arrays; +import java.util.Iterator; + +import org.eclipse.jetty.client.api.ContentProvider; + +public class ByteBufferContentProvider implements ContentProvider +{ + private final ByteBuffer[] buffers; + + public ByteBufferContentProvider(ByteBuffer... buffers) + { + this.buffers = buffers; + } + + @Override + public long length() + { + int length = 0; + for (ByteBuffer buffer : buffers) + length += buffer.remaining(); + return length; + } + + @Override + public Iterator iterator() + { + return Arrays.asList(buffers).iterator(); + } +} diff --git a/jetty-client-new/src/main/java/org/eclipse/jetty/client/HTTPClient.java b/jetty-client-new/src/main/java/org/eclipse/jetty/client/HTTPClient.java deleted file mode 100644 index 333051feadd..00000000000 --- a/jetty-client-new/src/main/java/org/eclipse/jetty/client/HTTPClient.java +++ /dev/null @@ -1,294 +0,0 @@ -//======================================================================== -//Copyright 2012-2012 Mort Bay Consulting Pty. Ltd. -//------------------------------------------------------------------------ -//All rights reserved. This program and the accompanying materials -//are made available under the terms of the Eclipse Public License v1.0 -//and Apache License v2.0 which accompanies this distribution. -//The Eclipse Public License is available at -//http://www.eclipse.org/legal/epl-v10.html -//The Apache License v2.0 is available at -//http://www.opensource.org/licenses/apache2.0.php -//You may elect to redistribute this code under either of these licenses. -//======================================================================== - -package org.eclipse.jetty.client; - -import java.io.IOException; -import java.net.SocketAddress; -import java.net.URI; -import java.net.URISyntaxException; -import java.nio.channels.SelectionKey; -import java.nio.channels.SocketChannel; -import java.util.Arrays; -import java.util.concurrent.ConcurrentHashMap; -import java.util.concurrent.ConcurrentMap; -import java.util.concurrent.Executor; -import java.util.concurrent.Future; -import java.util.concurrent.TimeUnit; - -import com.sun.jndi.toolkit.url.Uri; -import org.eclipse.jetty.client.api.Address; -import org.eclipse.jetty.client.api.Connection; -import org.eclipse.jetty.client.api.Destination; -import org.eclipse.jetty.client.api.Request; -import org.eclipse.jetty.client.api.Response; -import org.eclipse.jetty.io.AsyncConnection; -import org.eclipse.jetty.io.AsyncEndPoint; -import org.eclipse.jetty.io.SelectChannelEndPoint; -import org.eclipse.jetty.io.SelectorManager; -import org.eclipse.jetty.util.FutureCallback; -import org.eclipse.jetty.util.Jetty; -import org.eclipse.jetty.util.component.AggregateLifeCycle; - -/** - *

{@link HTTPClient} provides an asynchronous non-blocking implementation to perform HTTP requests to a server.

- *

{@link HTTPClient} provides easy-to-use methods such as {@link #GET(String)} that allow to perform HTTP - * requests in a one-liner, but also gives the ability to fine tune the configuration of requests via - * {@link Request.Builder}.

- *

{@link HTTPClient} acts as a central configuration point for network parameters (such as idle timeouts) and - * HTTP parameters (such as whether to follow redirects).

- *

{@link HTTPClient} transparently pools connections to servers, but allows direct control of connections for - * cases where this is needed.

- *

Typical usage:

- *
- * // One liner:
- * new HTTPClient().GET("http://localhost:8080/").get().getStatus();
- *
- * // Using the builder with a timeout
- * HTTPClient client = new HTTPClient();
- * Response response = client.builder("http://localhost:8080/").build().send().get(5, TimeUnit.SECONDS);
- * int status = response.getStatus();
- *
- * // Asynchronously
- * HTTPClient client = new HTTPClient();
- * client.builder("http://localhost:8080/").build().send(new Response.Listener.Adapter()
- * {
- *     @Override
- *     public void onComplete(Response response)
- *     {
- *         ...
- *     }
- * });
- * 
- */ -public class HTTPClient extends AggregateLifeCycle -{ - private final ConcurrentMap destinations = new ConcurrentHashMap<>(); - private volatile String agent = "Jetty/" + Jetty.VERSION; - private volatile boolean followRedirects = true; - private volatile Executor executor; - private volatile int maxConnectionsPerAddress = Integer.MAX_VALUE; - private volatile int maxQueueSizePerAddress = Integer.MAX_VALUE; - private volatile SocketAddress bindAddress; - private volatile SelectorManager selectorManager; - private volatile long idleTimeout; - - @Override - protected void doStart() throws Exception - { - selectorManager = newSelectorManager(); - addBean(selectorManager); - super.doStart(); - } - - protected SelectorManager newSelectorManager() - { - ClientSelectorManager result = new ClientSelectorManager(); - result.setMaxIdleTime(getIdleTimeout()); - return result; - } - - @Override - protected void doStop() throws Exception - { - super.doStop(); - } - - public long getIdleTimeout() - { - return idleTimeout; - } - - public void setIdleTimeout(long idleTimeout) - { - this.idleTimeout = idleTimeout; - } - - /** - * @return the address to bind socket channels to - * @see #setBindAddress(SocketAddress) - */ - public SocketAddress getBindAddress() - { - return bindAddress; - } - - /** - * @param bindAddress the address to bind socket channels to - * @see #getBindAddress() - */ - public void setBindAddress(SocketAddress bindAddress) - { - this.bindAddress = bindAddress; - } - - public Future GET(String uri) - { - return GET(URI.create(uri)); - } - - public Future GET(URI uri) - { - return builder(uri) - .method("GET") - // Add decoder, cookies, agent, default headers, etc. - .agent(getUserAgent()) - .followRedirects(isFollowRedirects()) - .build() - .send(); - } - - public Request.Builder builder(String uri) - { - return builder(URI.create(uri)); - } - - public Request.Builder builder(URI uri) - { - return new StandardRequest(this, uri); - } - - public Request.Builder builder(Request prototype) - { - return null; - } - - public Destination getDestination(String address) - { - Destination destination = destinations.get(address); - if (destination == null) - { - destination = new StandardDestination(this, address); - Destination existing = destinations.putIfAbsent(address, destination); - if (existing != null) - destination = existing; - } - return destination; - } - - public String getUserAgent() - { - return agent; - } - - public void setUserAgent(String agent) - { - this.agent = agent; - } - - public boolean isFollowRedirects() - { - return followRedirects; - } - - public void setFollowRedirects(boolean follow) - { - this.followRedirects = follow; - } - - public void join() - { - - } - - public void join(long timeout, TimeUnit unit) - { - } - - public Future send(Request request, Response.Listener listener) - { - URI uri = request.uri(); - String scheme = uri.getScheme(); - if (!Arrays.asList("http", "https").contains(scheme.toLowerCase())) - throw new IllegalArgumentException("Invalid protocol " + scheme); - - String key = scheme.toLowerCase() + "://" + uri.getHost().toLowerCase(); - int port = uri.getPort(); - if (port < 0) - key += "https".equalsIgnoreCase(scheme) ? ":443" : ":80"; - - return getDestination(key).send(request, listener); - } - - public Executor getExecutor() - { - return executor; - } - - public int getMaxConnectionsPerAddress() - { - return maxConnectionsPerAddress; - } - - public void setMaxConnectionsPerAddress(int maxConnectionsPerAddress) - { - this.maxConnectionsPerAddress = maxConnectionsPerAddress; - } - - public int getMaxQueueSizePerAddress() - { - return maxQueueSizePerAddress; - } - - public void setMaxQueueSizePerAddress(int maxQueueSizePerAddress) - { - this.maxQueueSizePerAddress = maxQueueSizePerAddress; - } - - protected Future newConnection(Destination destination) throws IOException - { - SocketChannel channel = SocketChannel.open(); - SocketAddress bindAddress = getBindAddress(); - if (bindAddress != null) - channel.bind(bindAddress); - channel.socket().setTcpNoDelay(true); - channel.connect(destination.address().toSocketAddress()); - - FutureCallback result = new FutureCallback<>(); - selectorManager.connect(channel, result); - return result; - } - - protected class ClientSelectorManager extends SelectorManager - { - public ClientSelectorManager() - { - this(1); - } - - public ClientSelectorManager(int selectors) - { - super(selectors); - } - - @Override - protected Selectable newEndPoint(SocketChannel channel, ManagedSelector selectSet, SelectionKey sKey) throws IOException - { - return new SelectChannelEndPoint(channel, selectSet, sKey, getMaxIdleTime()); - } - - @Override - public AsyncConnection newConnection(SocketChannel channel, AsyncEndPoint endpoint, Object attachment) - { - // TODO: SSL - - return new StandardConnection(channel, endpoint, ) - } - - @Override - protected void execute(Runnable task) - { - getExecutor().execute(task); - } - } -} diff --git a/jetty-client-new/src/main/java/org/eclipse/jetty/client/HttpClient.java b/jetty-client-new/src/main/java/org/eclipse/jetty/client/HttpClient.java new file mode 100644 index 00000000000..66844f360cc --- /dev/null +++ b/jetty-client-new/src/main/java/org/eclipse/jetty/client/HttpClient.java @@ -0,0 +1,430 @@ +//======================================================================== +//Copyright 2012-2012 Mort Bay Consulting Pty. Ltd. +//------------------------------------------------------------------------ +//All rights reserved. This program and the accompanying materials +//are made available under the terms of the Eclipse Public License v1.0 +//and Apache License v2.0 which accompanies this distribution. +//The Eclipse Public License is available at +//http://www.eclipse.org/legal/epl-v10.html +//The Apache License v2.0 is available at +//http://www.opensource.org/licenses/apache2.0.php +//You may elect to redistribute this code under either of these licenses. +//======================================================================== + +package org.eclipse.jetty.client; + +import java.io.IOException; +import java.net.ConnectException; +import java.net.InetSocketAddress; +import java.net.SocketAddress; +import java.net.URI; +import java.nio.channels.SelectionKey; +import java.nio.channels.SocketChannel; +import java.util.Arrays; +import java.util.concurrent.ConcurrentHashMap; +import java.util.concurrent.ConcurrentMap; +import java.util.concurrent.Executor; +import java.util.concurrent.Executors; +import java.util.concurrent.Future; +import java.util.concurrent.ScheduledExecutorService; +import javax.net.ssl.SSLEngine; + +import org.eclipse.jetty.client.api.Connection; +import org.eclipse.jetty.client.api.Destination; +import org.eclipse.jetty.client.api.Request; +import org.eclipse.jetty.client.api.Response; +import org.eclipse.jetty.http.HttpMethod; +import org.eclipse.jetty.http.HttpVersion; +import org.eclipse.jetty.io.ByteBufferPool; +import org.eclipse.jetty.io.EndPoint; +import org.eclipse.jetty.io.MappedByteBufferPool; +import org.eclipse.jetty.io.SelectChannelEndPoint; +import org.eclipse.jetty.io.SelectorManager; +import org.eclipse.jetty.io.ssl.SslConnection; +import org.eclipse.jetty.util.Callback; +import org.eclipse.jetty.util.FutureCallback; +import org.eclipse.jetty.util.Jetty; +import org.eclipse.jetty.util.component.AggregateLifeCycle; +import org.eclipse.jetty.util.log.Log; +import org.eclipse.jetty.util.log.Logger; +import org.eclipse.jetty.util.ssl.SslContextFactory; +import org.eclipse.jetty.util.thread.QueuedThreadPool; + +/** + *

{@link HttpClient} provides an efficient, asynchronous, non-blocking implementation + * to perform HTTP requests to a server through a simple API that offers also blocking semantic.

+ *

{@link HttpClient} provides easy-to-use methods such as {@link #GET(String)} that allow to perform HTTP + * requests in a one-liner, but also gives the ability to fine tune the configuration of requests via + * {@link HttpClient#newRequest(URI)}.

+ *

{@link HttpClient} acts as a central configuration point for network parameters (such as idle timeouts) + * and HTTP parameters (such as whether to follow redirects).

+ *

{@link HttpClient} transparently pools connections to servers, but allows direct control of connections + * for cases where this is needed.

+ *

Typical usage:

+ *
+ * // One liner:
+ * new HTTPClient().GET("http://localhost:8080/").get().status();
+ *
+ * // Building a request with a timeout
+ * HTTPClient client = new HTTPClient();
+ * Response response = client.newRequest("localhost:8080").send().get(5, TimeUnit.SECONDS);
+ * int status = response.status();
+ *
+ * // Asynchronously
+ * HTTPClient client = new HTTPClient();
+ * client.newRequest("localhost:8080").send(new Response.Listener.Adapter()
+ * {
+ *     @Override
+ *     public void onSuccess(Response response)
+ *     {
+ *         ...
+ *     }
+ * });
+ * 
+ */ +public class HttpClient extends AggregateLifeCycle +{ + private static final Logger LOG = Log.getLogger(HttpClient.class); + private final ConcurrentMap destinations = new ConcurrentHashMap<>(); + private final ConcurrentMap conversations = new ConcurrentHashMap<>(); + private volatile Executor executor; + private volatile ByteBufferPool byteBufferPool; + private volatile ScheduledExecutorService scheduler; + private volatile SelectorManager selectorManager; + private volatile SslContextFactory sslContextFactory; + + private volatile String agent = "Jetty/" + Jetty.VERSION; + private volatile boolean followRedirects = true; + private volatile int maxConnectionsPerAddress = 8; + private volatile int maxQueueSizePerAddress = 1024; + private volatile int requestBufferSize = 4096; + private volatile int responseBufferSize = 4096; + private volatile SocketAddress bindAddress; + private volatile long idleTimeout; + + public ByteBufferPool getByteBufferPool() + { + return byteBufferPool; + } + + public SslContextFactory getSslContextFactory() + { + return sslContextFactory; + } + + @Override + protected void doStart() throws Exception + { + if (executor == null) + executor = new QueuedThreadPool(); + addBean(executor); + + if (byteBufferPool == null) + byteBufferPool = new MappedByteBufferPool(); + addBean(byteBufferPool); + + if (scheduler == null) + scheduler = Executors.newSingleThreadScheduledExecutor(); + addBean(scheduler); + + selectorManager = newSelectorManager(); + addBean(selectorManager); + + super.doStart(); + } + + protected SelectorManager newSelectorManager() + { + return new ClientSelectorManager(); + } + + @Override + protected void doStop() throws Exception + { + super.doStop(); + } + + public long getIdleTimeout() + { + return idleTimeout; + } + + public void setIdleTimeout(long idleTimeout) + { + this.idleTimeout = idleTimeout; + } + + /** + * @return the address to bind socket channels to + * @see #setBindAddress(SocketAddress) + */ + public SocketAddress getBindAddress() + { + return bindAddress; + } + + /** + * @param bindAddress the address to bind socket channels to + * @see #getBindAddress() + */ + public void setBindAddress(SocketAddress bindAddress) + { + this.bindAddress = bindAddress; + } + + public Future GET(String uri) + { + return GET(URI.create(uri)); + } + + public Future GET(URI uri) + { + // TODO: Add decoder, cookies, agent, default headers, etc. + return newRequest(uri) + .method(HttpMethod.GET) + .version(HttpVersion.HTTP_1_1) + .agent(getUserAgent()) + .idleTimeout(getIdleTimeout()) + .followRedirects(isFollowRedirects()) + .send(); + } + + public Request newRequest(String host, int port) + { + return newRequest(URI.create(address("http", host, port))); + } + + public Request newRequest(URI uri) + { + return new HttpRequest(this, uri); + } + + protected Request newRequest(long id, URI uri) + { + return new HttpRequest(this, id, uri); + } + + private String address(String scheme, String host, int port) + { + return scheme + "://" + host + ":" + port; + } + + public Destination getDestination(String scheme, String host, int port) + { + String address = address(scheme, host, port); + Destination destination = destinations.get(address); + if (destination == null) + { + destination = new HttpDestination(this, scheme, host, port); + Destination existing = destinations.putIfAbsent(address, destination); + if (existing != null) + destination = existing; + } + return destination; + } + + public String getUserAgent() + { + return agent; + } + + public void setUserAgent(String agent) + { + this.agent = agent; + } + + public boolean isFollowRedirects() + { + return followRedirects; + } + + public void setFollowRedirects(boolean follow) + { + this.followRedirects = follow; + } + + public void send(Request request, Response.Listener listener) + { + String scheme = request.scheme().toLowerCase(); + if (!Arrays.asList("http", "https").contains(scheme)) + throw new IllegalArgumentException("Invalid protocol " + scheme); + + String host = request.host().toLowerCase(); + int port = request.port(); + if (port < 0) + port = "https".equals(scheme) ? 443 : 80; + + getDestination(scheme, host, port).send(request, listener); + } + + public Executor getExecutor() + { + return executor; + } + + public int getMaxConnectionsPerAddress() + { + return maxConnectionsPerAddress; + } + + public void setMaxConnectionsPerAddress(int maxConnectionsPerAddress) + { + this.maxConnectionsPerAddress = maxConnectionsPerAddress; + } + + public int getMaxQueueSizePerAddress() + { + return maxQueueSizePerAddress; + } + + public void setMaxQueueSizePerAddress(int maxQueueSizePerAddress) + { + this.maxQueueSizePerAddress = maxQueueSizePerAddress; + } + + public int getRequestBufferSize() + { + return requestBufferSize; + } + + public void setRequestBufferSize(int requestBufferSize) + { + this.requestBufferSize = requestBufferSize; + } + + public int getResponseBufferSize() + { + return responseBufferSize; + } + + public void setResponseBufferSize(int responseBufferSize) + { + this.responseBufferSize = responseBufferSize; + } + + protected void newConnection(Destination destination, Callback callback) + { + SocketChannel channel = null; + try + { + channel = SocketChannel.open(); + SocketAddress bindAddress = getBindAddress(); + if (bindAddress != null) + channel.bind(bindAddress); + channel.socket().setTcpNoDelay(true); + channel.configureBlocking(false); + channel.connect(new InetSocketAddress(destination.host(), destination.port())); + + Future result = new ConnectionCallback(destination, callback); + selectorManager.connect(channel, result); + } + catch (IOException x) + { + if (channel != null) + close(channel); + callback.failed(null, x); + } + } + + private void close(SocketChannel channel) + { + try + { + channel.close(); + } + catch (IOException x) + { + LOG.ignore(x); + } + } + + public HttpConversation conversationFor(Request request) + { + long id = request.id(); + HttpConversation conversation = conversations.get(id); + if (conversation == null) + { + conversation = new HttpConversation(); + HttpConversation existing = conversations.putIfAbsent(id, conversation); + if (existing != null) + conversation = existing; + } + return conversation; + } + + protected class ClientSelectorManager extends SelectorManager + { + public ClientSelectorManager() + { + this(1); + } + + public ClientSelectorManager(int selectors) + { + super(selectors); + } + + @Override + protected EndPoint newEndPoint(SocketChannel channel, ManagedSelector selector, SelectionKey key) + { + return new SelectChannelEndPoint(channel, selector, key, scheduler, getIdleTimeout()); + } + + @Override + public org.eclipse.jetty.io.Connection newConnection(SocketChannel channel, EndPoint endPoint, Object attachment) throws IOException + { + ConnectionCallback callback = (ConnectionCallback)attachment; + Destination destination = callback.destination; + + SslContextFactory sslContextFactory = getSslContextFactory(); + if ("https".equals(destination.scheme())) + { + if (sslContextFactory == null) + { + IOException failure = new ConnectException("Missing " + SslContextFactory.class.getSimpleName() + " for " + destination.scheme() + " requests"); + callback.failed(null, failure); + throw failure; + } + else + { + SSLEngine engine = sslContextFactory.newSSLEngine(endPoint.getRemoteAddress()); + engine.setUseClientMode(false); + + SslConnection sslConnection = new SslConnection(getByteBufferPool(), getExecutor(), endPoint, engine); + + EndPoint appEndPoint = sslConnection.getDecryptedEndPoint(); + HttpConnection connection = new HttpConnection(HttpClient.this, appEndPoint); + appEndPoint.setConnection(connection); + callback.callback.completed(connection); + connection.onOpen(); + + return sslConnection; + } + } + else + { + HttpConnection connection = new HttpConnection(HttpClient.this, endPoint); + callback.callback.completed(connection); + return connection; + } + } + + @Override + protected void execute(Runnable task) + { + getExecutor().execute(task); + } + } + + private class ConnectionCallback extends FutureCallback + { + private final Destination destination; + private final Callback callback; + + private ConnectionCallback(Destination destination, Callback callback) + { + this.destination = destination; + this.callback = callback; + } + } +} diff --git a/jetty-client-new/src/main/java/org/eclipse/jetty/client/HttpConnection.java b/jetty-client-new/src/main/java/org/eclipse/jetty/client/HttpConnection.java new file mode 100644 index 00000000000..42594385dc7 --- /dev/null +++ b/jetty-client-new/src/main/java/org/eclipse/jetty/client/HttpConnection.java @@ -0,0 +1,106 @@ +package org.eclipse.jetty.client; + +import org.eclipse.jetty.client.api.Connection; +import org.eclipse.jetty.client.api.ContentProvider; +import org.eclipse.jetty.client.api.Request; +import org.eclipse.jetty.client.api.Response; +import org.eclipse.jetty.http.HttpFields; +import org.eclipse.jetty.http.HttpHeader; +import org.eclipse.jetty.http.HttpVersion; +import org.eclipse.jetty.io.AbstractConnection; +import org.eclipse.jetty.io.EndPoint; +import org.eclipse.jetty.util.log.Log; +import org.eclipse.jetty.util.log.Logger; + +public class HttpConnection extends AbstractConnection implements Connection +{ + private static final Logger LOG = Log.getLogger(HttpConnection.class); + + private final HttpClient client; + private volatile HttpConversation conversation; + + public HttpConnection(HttpClient client, EndPoint endPoint) + { + super(endPoint, client.getExecutor()); + this.client = client; + } + + public HttpClient getHttpClient() + { + return client; + } + + @Override + public void onOpen() + { + super.onOpen(); + fillInterested(); + } + + @Override + protected boolean onReadTimeout() + { + HttpConversation conversation = this.conversation; + if (conversation != null) + conversation.idleTimeout(); + return true; + } + + @Override + public void send(Request request, Response.Listener listener) + { + normalizeRequest(request); + HttpConversation conversation = client.conversationFor(request); + this.conversation = conversation; + conversation.prepare(this, request, listener); + conversation.send(); + } + + private void normalizeRequest(Request request) + { + HttpVersion version = request.version(); + HttpFields headers = request.headers(); + ContentProvider content = request.content(); + + // Make sure the path is there + String path = request.path(); + if (path.matches("\\s*")) + request.path("/"); + + // Add content headers + if (content != null) + { + long contentLength = content.length(); + if (contentLength >= 0) + { + if (!headers.containsKey(HttpHeader.CONTENT_LENGTH.asString())) + headers.put(HttpHeader.CONTENT_LENGTH, String.valueOf(contentLength)); + } + else + { + if (!headers.containsKey(HttpHeader.TRANSFER_ENCODING.asString())) + headers.put(HttpHeader.TRANSFER_ENCODING, "chunked"); + } + } + + // TODO: decoder headers + + // If we are HTTP 1.1, add the Host header + if (version.getVersion() > 10) + { + if (!headers.containsKey(HttpHeader.HOST.asString())) + headers.put(HttpHeader.HOST, request.host() + ":" + request.port()); + } + } + + @Override + public void onFillable() + { + HttpConversation conversation = this.conversation; + if (conversation != null) + conversation.receive(); + else + // TODO test sending white space... we want to consume it but throw if it's not whitespace + LOG.warn("Ready to read response, but no receiver"); + } +} diff --git a/jetty-client-new/src/main/java/org/eclipse/jetty/client/HttpConversation.java b/jetty-client-new/src/main/java/org/eclipse/jetty/client/HttpConversation.java new file mode 100644 index 00000000000..33d70c980f5 --- /dev/null +++ b/jetty-client-new/src/main/java/org/eclipse/jetty/client/HttpConversation.java @@ -0,0 +1,77 @@ +package org.eclipse.jetty.client; + +import org.eclipse.jetty.client.api.Request; +import org.eclipse.jetty.client.api.Response; + +public class HttpConversation +{ + private final HttpSender sender; + private final HttpReceiver receiver; + private HttpConnection connection; + private Request request; + private Response.Listener listener; + private HttpResponse response; + + public HttpConversation() + { + sender = new HttpSender(); + receiver = new HttpReceiver(); + } + + public void prepare(HttpConnection connection, Request request, Response.Listener listener) + { + if (this.connection != null) + throw new IllegalStateException(); + this.connection = connection; + this.request = request; + this.listener = listener; + this.response = new HttpResponse(request, listener); + } + + public void done() + { + reset(); + } + + private void reset() + { + connection = null; + request = null; + listener = null; + } + + public HttpConnection connection() + { + return connection; + } + + public Request request() + { + return request; + } + + public Response.Listener listener() + { + return listener; + } + + public HttpResponse response() + { + return response; + } + + public void send() + { + sender.send(this); + } + + public void idleTimeout() + { + receiver.idleTimeout(); + } + + public void receive() + { + receiver.receive(this); + } +} diff --git a/jetty-client-new/src/main/java/org/eclipse/jetty/client/HttpDestination.java b/jetty-client-new/src/main/java/org/eclipse/jetty/client/HttpDestination.java new file mode 100644 index 00000000000..63111dfeefb --- /dev/null +++ b/jetty-client-new/src/main/java/org/eclipse/jetty/client/HttpDestination.java @@ -0,0 +1,200 @@ +//======================================================================== +//Copyright 2012-2012 Mort Bay Consulting Pty. Ltd. +//------------------------------------------------------------------------ +//All rights reserved. This program and the accompanying materials +//are made available under the terms of the Eclipse Public License v1.0 +//and Apache License v2.0 which accompanies this distribution. +//The Eclipse Public License is available at +//http://www.eclipse.org/legal/epl-v10.html +//The Apache License v2.0 is available at +//http://www.opensource.org/licenses/apache2.0.php +//You may elect to redistribute this code under either of these licenses. +//======================================================================== + +package org.eclipse.jetty.client; + +import java.util.Queue; +import java.util.concurrent.ArrayBlockingQueue; +import java.util.concurrent.Future; +import java.util.concurrent.RejectedExecutionException; +import java.util.concurrent.atomic.AtomicInteger; + +import org.eclipse.jetty.client.api.Connection; +import org.eclipse.jetty.client.api.Destination; +import org.eclipse.jetty.client.api.Request; +import org.eclipse.jetty.client.api.Response; +import org.eclipse.jetty.util.Callback; +import org.eclipse.jetty.util.FutureCallback; + +public class HttpDestination implements Destination +{ + private final AtomicInteger connectionCount = new AtomicInteger(); + private final HttpClient client; + private final String scheme; + private final String host; + private final int port; + private final Queue requests; + private final Queue idleConnections; + private final Queue activeConnections; + + public HttpDestination(HttpClient client, String scheme, String host, int port) + { + this.client = client; + this.scheme = scheme; + this.host = host; + this.port = port; + this.requests = new ArrayBlockingQueue<>(client.getMaxQueueSizePerAddress()); + this.idleConnections = new ArrayBlockingQueue<>(client.getMaxConnectionsPerAddress()); + this.activeConnections = new ArrayBlockingQueue<>(client.getMaxConnectionsPerAddress()); + } + + @Override + public String scheme() + { + return scheme; + } + + @Override + public String host() + { + return host; + } + + @Override + public int port() + { + return port; + } + + @Override + public void send(Request request, Response.Listener listener) + { + if (!scheme.equals(request.scheme())) + throw new IllegalArgumentException("Invalid request scheme " + request.scheme() + " for destination " + this); + if (!host.equals(request.host())) + throw new IllegalArgumentException("Invalid request host " + request.host() + " for destination " + this); + if (port != request.port()) + throw new IllegalArgumentException("Invalid request port " + request.port() + " for destination " + this); + + HttpResponse response = new HttpResponse(request, listener); + + if (client.isRunning()) + { + if (requests.offer(response)) + { + if (!client.isRunning() && requests.remove(response)) + { + throw new RejectedExecutionException(HttpClient.class.getSimpleName() + " is shutting down"); + } + else + { + Request.Listener requestListener = request.listener(); + notifyRequestQueued(requestListener, request); + ensureConnection(); + } + } + else + { + throw new RejectedExecutionException("Max requests per address " + client.getMaxQueueSizePerAddress() + " exceeded"); + } + } + } + + private void notifyRequestQueued(Request.Listener listener, Request request) + { + try + { + if (listener != null) + listener.onQueued(request); + } + catch (Exception x) + { + // TODO: log or abort request send ? + } + } + + private void ensureConnection() + { + int maxConnections = client.getMaxConnectionsPerAddress(); + while (true) + { + int count = connectionCount.get(); + + if (count >= maxConnections) + break; + + if (connectionCount.compareAndSet(count, count + 1)) + { + newConnection(new Callback() + { + @Override + public void completed(Connection connection) + { + dispatch(connection); + } + + @Override + public void failed(Connection connection, Throwable x) + { + // TODO: what here ? + } + }); + break; + } + } + } + + public Future newConnection() + { + FutureCallback result = new FutureCallback<>(); + newConnection(result); + return result; + } + + private void newConnection(Callback callback) + { + client.newConnection(this, callback); + } + + /** + * Responsibility of this method is to dequeue a request, associate it to the given {@code connection} + * and dispatch a thread to execute the request. + * + * This can be done in several ways: one could be to + * @param connection + */ + protected void dispatch(final Connection connection) + { + final Response response = requests.poll(); + if (response == null) + { + idleConnections.offer(connection); + } + else + { + activeConnections.offer(connection); + client.getExecutor().execute(new Runnable() + { + @Override + public void run() + { + connection.send(response.request(), response.listener()); + } + }); + } + } + + // TODO: 1. We must do queuing of requests in any case, because we cannot do blocking connect + // TODO: 2. We must be non-blocking connect, therefore we need to queue + + // Connections should compete for the queue of requests in separate threads + // that poses a problem of thread pool size: if < maxConnections we're starving + // + // conn1 is executed, takes on the queue => I need at least one thread per destination + + // we need to queue the request, pick an idle connection, then execute { conn.send(request, listener) } + + // if I create manually the connection, then I call send(request, listener) + + // Other ways ? +} diff --git a/jetty-client-new/src/main/java/org/eclipse/jetty/client/HttpReceiver.java b/jetty-client-new/src/main/java/org/eclipse/jetty/client/HttpReceiver.java new file mode 100644 index 00000000000..0da2874e265 --- /dev/null +++ b/jetty-client-new/src/main/java/org/eclipse/jetty/client/HttpReceiver.java @@ -0,0 +1,208 @@ +package org.eclipse.jetty.client; + +import java.io.EOFException; +import java.io.IOException; +import java.nio.ByteBuffer; +import java.util.concurrent.TimeoutException; + +import org.eclipse.jetty.client.api.Response; +import org.eclipse.jetty.http.HttpHeader; +import org.eclipse.jetty.http.HttpParser; +import org.eclipse.jetty.http.HttpVersion; +import org.eclipse.jetty.io.ByteBufferPool; +import org.eclipse.jetty.io.EndPoint; +import org.eclipse.jetty.util.log.Log; +import org.eclipse.jetty.util.log.Logger; + +class HttpReceiver implements HttpParser.ResponseHandler +{ + private static final Logger LOG = Log.getLogger(HttpReceiver.class); + + private final HttpParser parser = new HttpParser(this); + private HttpConversation conversation; + + public void receive(HttpConversation conversation) + { + if (this.conversation != null) + throw new IllegalStateException(); + this.conversation = conversation; + + HttpConnection connection = conversation.connection(); + HttpClient client = connection.getHttpClient(); + ByteBufferPool bufferPool = client.getByteBufferPool(); + ByteBuffer buffer = bufferPool.acquire(client.getResponseBufferSize(), true); + EndPoint endPoint = connection.getEndPoint(); + try + { + while (true) + { + int read = endPoint.fill(buffer); + if (read > 0) + { + parser.parseNext(buffer); + // TODO: response done, reset ? + } + else if (read == 0) + { + connection.fillInterested(); + break; + } + else + { + parser.shutdownInput(); + break; + } + } + } + catch (IOException x) + { + LOG.debug(x); + bufferPool.release(buffer); + fail(x); + } + } + + @Override + public boolean startResponse(HttpVersion version, int status, String reason) + { + // Probe the protocol listeners +// HttpClient client = connection.getHttpClient(); +// listener = client.find(status); // TODO +// listener = new RedirectionListener(connection); +// if (listener == null) +// listener = applicationListener; + + HttpResponse response = conversation.response(); + response.version(version).status(status).reason(reason); + notifyBegin(conversation.listener(), response); + return false; + } + + @Override + public boolean parsedHeader(HttpHeader header, String name, String value) + { + conversation.response().headers().put(name, value); + return false; + } + + @Override + public boolean headerComplete() + { + notifyHeaders(conversation.listener(), conversation.response()); + return false; + } + + @Override + public boolean content(ByteBuffer buffer) + { + notifyContent(conversation.listener(), conversation.response(), buffer); + return false; + } + + @Override + public boolean messageComplete(long contentLength) + { + success(); + return false; + } + + protected void success() + { + Response.Listener listener = conversation.listener(); + Response response = conversation.response(); + conversation.done(); + notifySuccess(listener, response); + } + + protected void fail(Throwable failure) + { + Response.Listener listener = conversation.listener(); + Response response = conversation.response(); + conversation.done(); + notifyFailure(listener, response, failure); + } + + @Override + public boolean earlyEOF() + { + fail(new EOFException()); + return false; + } + + @Override + public void badMessage(int status, String reason) + { + conversation.response().status(status).reason(reason); + fail(new HttpResponseException()); + } + + private void notifyBegin(Response.Listener listener, HttpResponse response) + { + try + { + if (listener != null) + listener.onBegin(response); + } + catch (Exception x) + { + LOG.info("Exception while notifying listener " + listener, x); + } + } + + private void notifyHeaders(Response.Listener listener, HttpResponse response) + { + try + { + if (listener != null) + listener.onHeaders(response); + } + catch (Exception x) + { + LOG.info("Exception while notifying listener " + listener, x); + } + } + + private void notifyContent(Response.Listener listener, HttpResponse response, ByteBuffer buffer) + { + try + { + if (listener != null) + listener.onContent(response, buffer); + } + catch (Exception x) + { + LOG.info("Exception while notifying listener " + listener, x); + } + } + + private void notifySuccess(Response.Listener listener, Response response) + { + try + { + if (listener != null) + listener.onSuccess(response); + } + catch (Exception x) + { + LOG.info("Exception while notifying listener " + listener, x); + } + } + + private void notifyFailure(Response.Listener listener, Response response, Throwable failure) + { + try + { + if (listener != null) + listener.onFailure(response, failure); + } + catch (Exception x) + { + LOG.info("Exception while notifying listener " + listener, x); + } + } + + public void idleTimeout() + { + fail(new TimeoutException()); + } +} diff --git a/jetty-client-new/src/main/java/org/eclipse/jetty/client/HttpRequest.java b/jetty-client-new/src/main/java/org/eclipse/jetty/client/HttpRequest.java new file mode 100644 index 00000000000..b5a2246e607 --- /dev/null +++ b/jetty-client-new/src/main/java/org/eclipse/jetty/client/HttpRequest.java @@ -0,0 +1,259 @@ +//======================================================================== +//Copyright 2012-2012 Mort Bay Consulting Pty. Ltd. +//------------------------------------------------------------------------ +//All rights reserved. This program and the accompanying materials +//are made available under the terms of the Eclipse Public License v1.0 +//and Apache License v2.0 which accompanies this distribution. +//The Eclipse Public License is available at +//http://www.eclipse.org/legal/epl-v10.html +//The Apache License v2.0 is available at +//http://www.opensource.org/licenses/apache2.0.php +//You may elect to redistribute this code under either of these licenses. +//======================================================================== + +package org.eclipse.jetty.client; + +import java.net.URI; +import java.util.Map; +import java.util.concurrent.Future; +import java.util.concurrent.atomic.AtomicLong; + +import org.eclipse.jetty.client.api.ContentDecoder; +import org.eclipse.jetty.client.api.ContentProvider; +import org.eclipse.jetty.client.api.Request; +import org.eclipse.jetty.client.api.Response; +import org.eclipse.jetty.http.HttpFields; +import org.eclipse.jetty.http.HttpMethod; +import org.eclipse.jetty.http.HttpVersion; +import org.eclipse.jetty.util.FutureCallback; + +public class HttpRequest implements Request +{ + private static final AtomicLong ids = new AtomicLong(); + + private final HttpClient client; + private final long id; + private String scheme; + private final String host; + private final int port; + private String path; + private HttpMethod method; + private HttpVersion version; + private String agent; + private long idleTimeout; + private Response response; + private Listener listener; + private ContentProvider content; + private final HttpFields headers = new HttpFields(); + + public HttpRequest(HttpClient client, URI uri) + { + this(client, ids.incrementAndGet(), uri); + } + + protected HttpRequest(HttpClient client, long id, URI uri) + { + this.client = client; + this.id = id; + scheme(uri.getScheme()); + host = uri.getHost(); + port = uri.getPort(); + path(uri.getPath()); + // TODO: query params + } + + @Override + public long id() + { + return id; + } + + @Override + public String scheme() + { + return scheme; + } + + @Override + public Request scheme(String scheme) + { + this.scheme = scheme; + return this; + } + + @Override + public String host() + { + return host; + } + + @Override + public int port() + { + return port; + } + + @Override + public HttpMethod method() + { + return method; + } + + @Override + public Request method(HttpMethod method) + { + this.method = method; + return this; + } + + @Override + public String path() + { + return path; + } + + @Override + public Request path(String path) + { + this.path = path; + return this; + } + + @Override + public HttpVersion version() + { + return version; + } + + @Override + public Request version(HttpVersion version) + { + this.version = version; + return this; + } + + @Override + public Request param(String name, String value) + { + return this; + } + + @Override + public Map params() + { + return null; + } + + @Override + public String agent() + { + return agent; + } + + @Override + public Request agent(String userAgent) + { + this.agent = userAgent; + return this; + } + + @Override + public Request header(String name, String value) + { + headers.add(name, value); + return this; + } + + @Override + public HttpFields headers() + { + return headers; + } + + @Override + public Listener listener() + { + return listener; + } + + @Override + public Request listener(Request.Listener listener) + { + this.listener = listener; + return this; + } + + @Override + public ContentProvider content() + { + return content; + } + + @Override + public Request content(ContentProvider content) + { + this.content = content; + return this; + } + + @Override + public Request decoder(ContentDecoder decoder) + { + return this; + } + + @Override + public Request cookie(String key, String value) + { + return this; + } + + @Override + public Request followRedirects(boolean follow) + { + return this; + } + + @Override + public long idleTimeout() + { + return idleTimeout; + } + + @Override + public Request idleTimeout(long timeout) + { + this.idleTimeout = timeout; + return this; + } + + @Override + public Future send() + { + final FutureCallback result = new FutureCallback<>(); + BufferingResponseListener listener = new BufferingResponseListener() + { + @Override + public void onSuccess(Response response) + { + super.onSuccess(response); + result.completed(response); + } + + @Override + public void onFailure(Response response, Throwable failure) + { + super.onFailure(response, failure); + result.failed(response, failure); + } + }; + send(listener); + return result; + } + + @Override + public void send(final Response.Listener listener) + { + client.send(this, listener); + } +} diff --git a/jetty-client-new/src/main/java/org/eclipse/jetty/client/StandardResponse.java b/jetty-client-new/src/main/java/org/eclipse/jetty/client/HttpResponse.java similarity index 54% rename from jetty-client-new/src/main/java/org/eclipse/jetty/client/StandardResponse.java rename to jetty-client-new/src/main/java/org/eclipse/jetty/client/HttpResponse.java index 73dcd206855..d20ebf0eb89 100644 --- a/jetty-client-new/src/main/java/org/eclipse/jetty/client/StandardResponse.java +++ b/jetty-client-new/src/main/java/org/eclipse/jetty/client/HttpResponse.java @@ -13,58 +13,82 @@ package org.eclipse.jetty.client; -import java.io.InputStream; - -import org.eclipse.jetty.client.api.ContentProvider; -import org.eclipse.jetty.client.api.Headers; import org.eclipse.jetty.client.api.Request; import org.eclipse.jetty.client.api.Response; +import org.eclipse.jetty.http.HttpFields; +import org.eclipse.jetty.http.HttpVersion; import org.eclipse.jetty.util.FutureCallback; -public class StandardResponse extends FutureCallback implements Response +public class HttpResponse extends FutureCallback implements Response { + private final HttpFields headers = new HttpFields(); private final Request request; private final Listener listener; + private HttpVersion version; + private int status; + private String reason; - public StandardResponse(Request request, Response.Listener listener) + public HttpResponse(Request request, Response.Listener listener) { this.request = request; this.listener = listener; } - @Override - public int getStatus() + public HttpVersion version() { - return 0; + return version; + } + + public HttpResponse version(HttpVersion version) + { + this.version = version; + return this; } @Override - public Headers getHeaders() + public int status() { - return null; + return status; + } + + public HttpResponse status(int status) + { + this.status = status; + return this; + } + + public String reason() + { + return reason; + } + + public HttpResponse reason(String reason) + { + this.reason = reason; + return this; } @Override - public Request getRequest() + public HttpFields headers() + { + return headers; + } + + @Override + public Request request() { return request; } @Override - public ContentProvider content() + public Listener listener() { - return null; - } - - @Override - public InputStream contentAsStream() - { - return null; + return listener; } @Override public void abort() { - request.abort(); +// request.abort(); } } diff --git a/jetty-client-new/src/main/java/org/eclipse/jetty/client/HttpResponseException.java b/jetty-client-new/src/main/java/org/eclipse/jetty/client/HttpResponseException.java new file mode 100644 index 00000000000..083330d19c4 --- /dev/null +++ b/jetty-client-new/src/main/java/org/eclipse/jetty/client/HttpResponseException.java @@ -0,0 +1,28 @@ +package org.eclipse.jetty.client; + +public class HttpResponseException extends RuntimeException +{ + public HttpResponseException() + { + } + + public HttpResponseException(String message) + { + super(message); + } + + public HttpResponseException(String message, Throwable cause) + { + super(message, cause); + } + + public HttpResponseException(Throwable cause) + { + super(cause); + } + + public HttpResponseException(String message, Throwable cause, boolean enableSuppression, boolean writableStackTrace) + { + super(message, cause, enableSuppression, writableStackTrace); + } +} diff --git a/jetty-client-new/src/main/java/org/eclipse/jetty/client/HttpSender.java b/jetty-client-new/src/main/java/org/eclipse/jetty/client/HttpSender.java new file mode 100644 index 00000000000..aad60f79da7 --- /dev/null +++ b/jetty-client-new/src/main/java/org/eclipse/jetty/client/HttpSender.java @@ -0,0 +1,319 @@ +package org.eclipse.jetty.client; + +import java.io.IOException; +import java.nio.ByteBuffer; +import java.util.Collections; +import java.util.Iterator; +import java.util.concurrent.Executor; +import java.util.concurrent.atomic.AtomicReference; + +import org.eclipse.jetty.client.api.ContentProvider; +import org.eclipse.jetty.client.api.Request; +import org.eclipse.jetty.client.api.Response; +import org.eclipse.jetty.http.HttpGenerator; +import org.eclipse.jetty.io.ByteBufferPool; +import org.eclipse.jetty.io.EndPoint; +import org.eclipse.jetty.util.BufferUtil; +import org.eclipse.jetty.util.Callback; +import org.eclipse.jetty.util.log.Log; +import org.eclipse.jetty.util.log.Logger; + +class HttpSender +{ + private static final Logger LOG = Log.getLogger(HttpSender.class); + + private final HttpGenerator generator = new HttpGenerator(); + private HttpConversation conversation; + private long contentLength; + private Iterator contentChunks; + private ByteBuffer header; + private ByteBuffer chunk; + private boolean requestHeadersComplete; + + public void send(HttpConversation conversation) + { + this.conversation = conversation; + ContentProvider content = conversation.request().content(); + this.contentLength = content == null ? -1 : content.length(); + this.contentChunks = content == null ? Collections.emptyIterator() : content.iterator(); + send(); + } + + private void send() + { + try + { + HttpConnection connection = conversation.connection(); + EndPoint endPoint = connection.getEndPoint(); + HttpClient client = connection.getHttpClient(); + ByteBufferPool byteBufferPool = client.getByteBufferPool(); + HttpGenerator.RequestInfo info = null; + ByteBuffer content = contentChunks.hasNext() ? contentChunks.next() : BufferUtil.EMPTY_BUFFER; + boolean lastContent = !contentChunks.hasNext(); + while (true) + { + HttpGenerator.Result result = generator.generateRequest(info, header, chunk, content, lastContent); + switch (result) + { + case NEED_INFO: + { + Request request = conversation.request(); + info = new HttpGenerator.RequestInfo(request.version(), request.headers(), contentLength, request.method().asString(), request.path()); + break; + } + case NEED_HEADER: + { + header = byteBufferPool.acquire(client.getRequestBufferSize(), false); + break; + } + case NEED_CHUNK: + { + chunk = byteBufferPool.acquire(HttpGenerator.CHUNK_SIZE, false); + break; + } + case FLUSH: + { + StatefulExecutorCallback callback = new StatefulExecutorCallback(client.getExecutor()) + { + @Override + protected void pendingCompleted() + { + notifyRequestHeadersComplete(); + send(); + } + + @Override + protected void failed(Throwable x) + { + fail(x); + } + }; + if (header == null) + header = BufferUtil.EMPTY_BUFFER; + if (chunk == null) + chunk = BufferUtil.EMPTY_BUFFER; + if (content == null) + content = BufferUtil.EMPTY_BUFFER; + endPoint.write(null, callback, header, chunk, content); + if (callback.pending()) + return; + + if (callback.completed()) + { + if (!requestHeadersComplete) + { + requestHeadersComplete = true; + notifyRequestHeadersComplete(); + } + releaseBuffers(); + content = contentChunks.hasNext() ? contentChunks.next() : BufferUtil.EMPTY_BUFFER; + lastContent = !contentChunks.hasNext(); + } + break; + } + case SHUTDOWN_OUT: + { + endPoint.shutdownOutput(); + break; + } + case CONTINUE: + { + break; + } + case DONE: + { + if (generator.isEnd()) + success(); + return; + } + default: + { + throw new IllegalStateException("Unknown result " + result); + } + } + } + } + catch (IOException x) + { + LOG.debug(x); + fail(x); + } + finally + { + releaseBuffers(); + } + } + + protected void success() + { + notifyRequestSuccess(); + } + + protected void fail(Throwable x) + { + BufferUtil.clear(header); + BufferUtil.clear(chunk); + releaseBuffers(); + notifyRequestFailure(x); + notifyResponseFailure(x); + conversation.connection().getEndPoint().shutdownOutput(); + generator.abort(); + } + + private void releaseBuffers() + { + ByteBufferPool byteBufferPool = conversation.connection().getHttpClient().getByteBufferPool(); + if (!BufferUtil.hasContent(header)) + { + byteBufferPool.release(header); + header = null; + } + if (!BufferUtil.hasContent(chunk)) + { + byteBufferPool.release(chunk); + chunk = null; + } + } + + private void notifyRequestHeadersComplete() + { + Request request = conversation.request(); + Request.Listener listener = request.listener(); + try + { + if (listener != null) + listener.onHeaders(request); + } + catch (Exception x) + { + LOG.info("Exception while notifying listener " + listener, x); + } + } + + private void notifyRequestSuccess() + { + Request request = conversation.request(); + Request.Listener listener = request.listener(); + try + { + if (listener != null) + listener.onSuccess(request); + } + catch (Exception x) + { + LOG.info("Exception while notifying listener " + listener, x); + } + } + + private void notifyRequestFailure(Throwable x) + { + Request request = conversation.request(); + Request.Listener listener = request.listener(); + try + { + if (listener != null) + listener.onFailure(request, x); + } + catch (Exception xx) + { + LOG.info("Exception while notifying listener " + listener, xx); + } + } + + private void notifyResponseFailure(Throwable x) + { + + Response.Listener listener = conversation.listener(); + try + { + if (listener != null) + listener.onFailure(null, x); + } + catch (Exception xx) + { + LOG.info("Exception while notifying listener " + listener, xx); + } + } + + private static abstract class StatefulExecutorCallback implements Callback, Runnable + { + private final AtomicReference state = new AtomicReference<>(State.INCOMPLETE); + private final Executor executor; + + private StatefulExecutorCallback(Executor executor) + { + this.executor = executor; + } + + @Override + public final void completed(final Void context) + { + State previous = state.get(); + while (true) + { + if (state.compareAndSet(previous, State.COMPLETE)) + break; + previous = state.get(); + } + if (previous == State.PENDING) + executor.execute(this); + } + + @Override + public final void run() + { + pendingCompleted(); + } + + protected abstract void pendingCompleted(); + + @Override + public final void failed(Void context, final Throwable x) + { + State previous = state.get(); + while (true) + { + if (state.compareAndSet(previous, State.FAILED)) + break; + previous = state.get(); + } + if (previous == State.PENDING) + { + executor.execute(new Runnable() + { + @Override + public void run() + { + failed(x); + } + }); + } + else + { + failed(x); + } + } + + protected abstract void failed(Throwable x); + + public boolean pending() + { + return state.compareAndSet(State.INCOMPLETE, State.PENDING); + } + + public boolean completed() + { + return state.get() == State.COMPLETE; + } + + public boolean failed() + { + return state.get() == State.FAILED; + } + + private enum State + { + INCOMPLETE, PENDING, COMPLETE, FAILED + } + } +} diff --git a/jetty-client-new/src/main/java/org/eclipse/jetty/client/PathContentProvider.java b/jetty-client-new/src/main/java/org/eclipse/jetty/client/PathContentProvider.java new file mode 100644 index 00000000000..0a5bbd9be1c --- /dev/null +++ b/jetty-client-new/src/main/java/org/eclipse/jetty/client/PathContentProvider.java @@ -0,0 +1,91 @@ +package org.eclipse.jetty.client; + +import java.io.IOException; +import java.nio.ByteBuffer; +import java.nio.channels.SeekableByteChannel; +import java.nio.file.AccessDeniedException; +import java.nio.file.Files; +import java.nio.file.NoSuchFileException; +import java.nio.file.Path; +import java.nio.file.StandardOpenOption; +import java.util.Iterator; +import java.util.NoSuchElementException; + +import org.eclipse.jetty.client.api.ContentProvider; + +public class PathContentProvider implements ContentProvider +{ + private final Path filePath; + private final long fileSize; + private final int bufferSize; + + public PathContentProvider(Path filePath) throws IOException + { + this(filePath, 4096); + } + + public PathContentProvider(Path filePath, int bufferSize) throws IOException + { + if (!Files.isRegularFile(filePath)) + throw new NoSuchFileException(filePath.toString()); + if (!Files.isReadable(filePath)) + throw new AccessDeniedException(filePath.toString()); + this.filePath = filePath; + this.fileSize = Files.size(filePath); + this.bufferSize = bufferSize; + } + + @Override + public long length() + { + return fileSize; + } + + @Override + public Iterator iterator() + { + return new LazyIterator(); + } + + private class LazyIterator implements Iterator + { + private final ByteBuffer buffer = ByteBuffer.allocateDirect(bufferSize); + private SeekableByteChannel channel; + private long position; + + @Override + public boolean hasNext() + { + return position < length(); + } + + @Override + public ByteBuffer next() + { + try + { + if (channel == null) + channel = Files.newByteChannel(filePath, StandardOpenOption.READ); + + buffer.clear(); + int read = channel.read(buffer); + if (read < 0) + throw new NoSuchElementException(); + + position += read; + buffer.flip(); + return buffer; + } + catch (IOException x) + { + throw (NoSuchElementException)new NoSuchElementException().initCause(x); + } + } + + @Override + public void remove() + { + throw new UnsupportedOperationException(); + } + } +} diff --git a/jetty-client-new/src/main/java/org/eclipse/jetty/client/RedirectionListener.java b/jetty-client-new/src/main/java/org/eclipse/jetty/client/RedirectionListener.java new file mode 100644 index 00000000000..0190c8755f4 --- /dev/null +++ b/jetty-client-new/src/main/java/org/eclipse/jetty/client/RedirectionListener.java @@ -0,0 +1,42 @@ +package org.eclipse.jetty.client; + +import java.net.URI; + +import org.eclipse.jetty.client.api.Request; +import org.eclipse.jetty.client.api.Response; + +public class RedirectionListener extends Response.Listener.Adapter +{ + private final HttpConnection connection; + + public RedirectionListener(HttpConnection connection) + { + this.connection = connection; + } + + @Override + public void onSuccess(Response response) + { + switch (response.status()) + { + case 301: // GET or HEAD only allowed, keep the method + { + break; + } + case 302: + case 303: // use GET for next request + { + String location = response.headers().get("location"); + HttpClient httpClient = connection.getHttpClient(); + Request redirect = httpClient.newRequest(response.request().id(), URI.create(location)); + redirect.send(this); + } + } + } + + @Override + public void onFailure(Response response, Throwable failure) + { + // TODO + } +} diff --git a/jetty-client-new/src/main/java/org/eclipse/jetty/client/StandardDestination.java b/jetty-client-new/src/main/java/org/eclipse/jetty/client/StandardDestination.java deleted file mode 100644 index ab94a3d2e17..00000000000 --- a/jetty-client-new/src/main/java/org/eclipse/jetty/client/StandardDestination.java +++ /dev/null @@ -1,98 +0,0 @@ -//======================================================================== -//Copyright 2012-2012 Mort Bay Consulting Pty. Ltd. -//------------------------------------------------------------------------ -//All rights reserved. This program and the accompanying materials -//are made available under the terms of the Eclipse Public License v1.0 -//and Apache License v2.0 which accompanies this distribution. -//The Eclipse Public License is available at -//http://www.eclipse.org/legal/epl-v10.html -//The Apache License v2.0 is available at -//http://www.opensource.org/licenses/apache2.0.php -//You may elect to redistribute this code under either of these licenses. -//======================================================================== - -package org.eclipse.jetty.client; - -import java.util.Queue; -import java.util.concurrent.ArrayBlockingQueue; -import java.util.concurrent.Future; -import java.util.concurrent.RejectedExecutionException; -import java.util.concurrent.TimeUnit; - -import org.eclipse.jetty.client.api.Address; -import org.eclipse.jetty.client.api.Connection; -import org.eclipse.jetty.client.api.Destination; -import org.eclipse.jetty.client.api.Request; -import org.eclipse.jetty.client.api.Response; - -public class StandardDestination implements Destination -{ - private final HTTPClient client; - private final Address address; - private final Queue requests; - private final Queue connections; - - public StandardDestination(HTTPClient client, Address address) - { - this.client = client; - this.address = address; - this.requests = new ArrayBlockingQueue<>(client.getMaxQueueSizePerAddress()); - this.connections = new ArrayBlockingQueue<>(client.getMaxConnectionsPerAddress()); - } - - @Override - public Address address() - { - return address; - } - - @Override - public Connection connect(long timeout, TimeUnit unit) - { - return null; - } - - @Override - public Future send(Request request, Response.Listener listener) - { - if (!address.equals(request.address())) - throw new IllegalArgumentException("Invalid request address " + request.address() + " for destination " + this); - - StandardResponse response = new StandardResponse(request, listener); - - Connection connection = connections.poll(); - if (connection == null) - newConnection(); - - if (!requests.offer(response)) - throw new RejectedExecutionException("Max requests per address " + client.getMaxQueueSizePerAddress() + " exceeded"); - - return response; - } - - protected Future newConnection() - { - return client.newConnection(this); - } - - protected Connection getConnection() - { - Connection connection = connections.poll(); - if (connection == null) - { - client. - } - return connection; - } - - // TODO: 1. We must do queuing of requests in any case, because we cannot do blocking connect - // TODO: 2. We must be non-blocking connect, therefore we need to queue - - // Connections should compete for the queue of requests in separate threads - // that poses a problem of thread pool size: if < maxConnections we're starving - // - - /** - * I need a Future connect(), and a void connect(Callback) - */ -} diff --git a/jetty-client-new/src/main/java/org/eclipse/jetty/client/StandardRequest.java b/jetty-client-new/src/main/java/org/eclipse/jetty/client/StandardRequest.java deleted file mode 100644 index 658f883cf68..00000000000 --- a/jetty-client-new/src/main/java/org/eclipse/jetty/client/StandardRequest.java +++ /dev/null @@ -1,126 +0,0 @@ -//======================================================================== -//Copyright 2012-2012 Mort Bay Consulting Pty. Ltd. -//------------------------------------------------------------------------ -//All rights reserved. This program and the accompanying materials -//are made available under the terms of the Eclipse Public License v1.0 -//and Apache License v2.0 which accompanies this distribution. -//The Eclipse Public License is available at -//http://www.eclipse.org/legal/epl-v10.html -//The Apache License v2.0 is available at -//http://www.opensource.org/licenses/apache2.0.php -//You may elect to redistribute this code under either of these licenses. -//======================================================================== - -package org.eclipse.jetty.client; - -import java.io.File; -import java.net.URI; -import java.util.concurrent.Future; - -import org.eclipse.jetty.client.api.Address; -import org.eclipse.jetty.client.api.Authentication; -import org.eclipse.jetty.client.api.ContentDecoder; -import org.eclipse.jetty.client.api.ContentProvider; -import org.eclipse.jetty.client.api.Request; -import org.eclipse.jetty.client.api.Response; - -public class StandardRequest implements Request, Request.Builder -{ - private final HTTPClient client; - private final URI uri; - private String method; - private String agent; - private Response response; - - public StandardRequest(HTTPClient client, URI uri) - { - this.client = client; - this.uri = uri; - } - - @Override - public Request.Builder method(String method) - { - this.method = method; - return this; - } - - @Override - public Request.Builder agent(String userAgent) - { - this.agent = userAgent; - return this; - } - - @Override - public Request.Builder header(String name, String value) - { - return this; - } - - @Override - public Request.Builder listener(Request.Listener listener) - { - return this; - } - - @Override - public Request.Builder file(File file) - { - return this; - } - - @Override - public Request.Builder content(ContentProvider buffer) - { - return this; - } - - @Override - public Request.Builder decoder(ContentDecoder decoder) - { - return this; - } - - @Override - public Request.Builder param(String name, String value) - { - return this; - } - - @Override - public Request.Builder cookie(String key, String value) - { - return this; - } - - @Override - public Request.Builder authentication(Authentication authentication) - { - return this; - } - - @Override - public Request.Builder followRedirects(boolean follow) - { - return this; - } - - @Override - public Request build() - { - return this; - } - - @Override - public Future send() - { - return send(null); - } - - @Override - public Future send(final Response.Listener listener) - { - return client.send(this, listener); - } -} diff --git a/jetty-client-new/src/main/java/org/eclipse/jetty/client/StreamResponseListener.java b/jetty-client-new/src/main/java/org/eclipse/jetty/client/StreamingResponseListener.java similarity index 94% rename from jetty-client-new/src/main/java/org/eclipse/jetty/client/StreamResponseListener.java rename to jetty-client-new/src/main/java/org/eclipse/jetty/client/StreamingResponseListener.java index 832a181c5a1..e7c43feda58 100644 --- a/jetty-client-new/src/main/java/org/eclipse/jetty/client/StreamResponseListener.java +++ b/jetty-client-new/src/main/java/org/eclipse/jetty/client/StreamingResponseListener.java @@ -19,7 +19,7 @@ import java.util.concurrent.TimeUnit; import org.eclipse.jetty.client.api.Response; -public class StreamResponseListener extends Response.Listener.Adapter +public class StreamingResponseListener extends Response.Listener.Adapter { public Response get(long timeout, TimeUnit seconds) { diff --git a/jetty-client-new/src/main/java/org/eclipse/jetty/client/api/Connection.java b/jetty-client-new/src/main/java/org/eclipse/jetty/client/api/Connection.java index b63147d6794..22966f4bf69 100644 --- a/jetty-client-new/src/main/java/org/eclipse/jetty/client/api/Connection.java +++ b/jetty-client-new/src/main/java/org/eclipse/jetty/client/api/Connection.java @@ -13,9 +13,7 @@ package org.eclipse.jetty.client.api; -import java.util.concurrent.Future; - public interface Connection extends AutoCloseable { - Future send(Request request, Response.Listener listener); + void send(Request request, Response.Listener listener); } diff --git a/jetty-client-new/src/main/java/org/eclipse/jetty/client/api/ContentProvider.java b/jetty-client-new/src/main/java/org/eclipse/jetty/client/api/ContentProvider.java index 92f1015d729..900259a6259 100644 --- a/jetty-client-new/src/main/java/org/eclipse/jetty/client/api/ContentProvider.java +++ b/jetty-client-new/src/main/java/org/eclipse/jetty/client/api/ContentProvider.java @@ -13,6 +13,9 @@ package org.eclipse.jetty.client.api; -public interface ContentProvider +import java.nio.ByteBuffer; + +public interface ContentProvider extends Iterable { + long length(); } diff --git a/jetty-client-new/src/main/java/org/eclipse/jetty/client/api/Destination.java b/jetty-client-new/src/main/java/org/eclipse/jetty/client/api/Destination.java index 6bab0dfc3a7..a8d6de019df 100644 --- a/jetty-client-new/src/main/java/org/eclipse/jetty/client/api/Destination.java +++ b/jetty-client-new/src/main/java/org/eclipse/jetty/client/api/Destination.java @@ -14,13 +14,16 @@ package org.eclipse.jetty.client.api; import java.util.concurrent.Future; -import java.util.concurrent.TimeUnit; public interface Destination { - Connection connect(long timeout, TimeUnit unit); + String scheme(); - Future send(Request request, Response.Listener listener); + String host(); - Address address(); + int port(); + + Future newConnection(); + + void send(Request request, Response.Listener listener); } diff --git a/jetty-client-new/src/main/java/org/eclipse/jetty/client/api/Headers.java b/jetty-client-new/src/main/java/org/eclipse/jetty/client/api/Headers.java deleted file mode 100644 index 2debb12a31b..00000000000 --- a/jetty-client-new/src/main/java/org/eclipse/jetty/client/api/Headers.java +++ /dev/null @@ -1,30 +0,0 @@ -//======================================================================== -//Copyright 2012-2012 Mort Bay Consulting Pty. Ltd. -//------------------------------------------------------------------------ -//All rights reserved. This program and the accompanying materials -//are made available under the terms of the Eclipse Public License v1.0 -//and Apache License v2.0 which accompanies this distribution. -//The Eclipse Public License is available at -//http://www.eclipse.org/legal/epl-v10.html -//The Apache License v2.0 is available at -//http://www.opensource.org/licenses/apache2.0.php -//You may elect to redistribute this code under either of these licenses. -//======================================================================== - -package org.eclipse.jetty.client.api; - -public class Headers -{ - public Header get(String name) - { - return null; - } - - public static class Header - { - public int valueAsInt() - { - return 0; - } - } -} diff --git a/jetty-client-new/src/main/java/org/eclipse/jetty/client/api/Request.java b/jetty-client-new/src/main/java/org/eclipse/jetty/client/api/Request.java index fe25f6e5587..84520587b64 100644 --- a/jetty-client-new/src/main/java/org/eclipse/jetty/client/api/Request.java +++ b/jetty-client-new/src/main/java/org/eclipse/jetty/client/api/Request.java @@ -13,53 +13,74 @@ package org.eclipse.jetty.client.api; -import java.io.File; -import java.net.URI; +import java.util.Map; import java.util.concurrent.Future; +import org.eclipse.jetty.http.HttpFields; +import org.eclipse.jetty.http.HttpMethod; +import org.eclipse.jetty.http.HttpVersion; + public interface Request { + long id(); + + String scheme(); + + Request scheme(String scheme); + + String host(); + + int port(); + + HttpMethod method(); + + Request method(HttpMethod method); + + String path(); + + Request path(String path); + + HttpVersion version(); + + Request version(HttpVersion version); + + Map params(); + + Request param(String name, String value); + + HttpFields headers(); + + Request header(String name, String value); + + ContentProvider content(); + + Request content(ContentProvider buffer); + + Request decoder(ContentDecoder decoder); + + Request cookie(String key, String value); + + String agent(); + + Request agent(String userAgent); + + long idleTimeout(); + + Request idleTimeout(long timeout); + + Request followRedirects(boolean follow); + + Listener listener(); + + Request listener(Listener listener); + Future send(); - Future send(Response.Listener listener); - - URI uri(); - - void abort(); - - /** - *

A builder for requests

. - */ - public interface Builder - { - Builder method(String method); - - Builder header(String name, String value); - - Builder listener(Request.Listener listener); - - Builder file(File file); - - Builder content(ContentProvider buffer); - - Builder decoder(ContentDecoder decoder); - - Builder param(String name, String value); - - Builder cookie(String key, String value); - - Builder authentication(Authentication authentication); - - Builder agent(String userAgent); - - Builder followRedirects(boolean follow); - - Request build(); - } + void send(Response.Listener listener); public interface Listener { - public void onQueue(Request request); + public void onQueued(Request request); public void onBegin(Request request); @@ -67,16 +88,14 @@ public interface Request public void onFlush(Request request, int bytes); - public void onComplete(Request request); + public void onSuccess(Request request); - public void onException(Request request, Exception exception); - - public void onEnd(Request request); + public void onFailure(Request request, Throwable failure); public static class Adapter implements Listener { @Override - public void onQueue(Request request) + public void onQueued(Request request) { } @@ -96,17 +115,12 @@ public interface Request } @Override - public void onComplete(Request request) + public void onSuccess(Request request) { } @Override - public void onException(Request request, Exception exception) - { - } - - @Override - public void onEnd(Request request) + public void onFailure(Request request, Throwable failure) { } } diff --git a/jetty-client-new/src/main/java/org/eclipse/jetty/client/api/Response.java b/jetty-client-new/src/main/java/org/eclipse/jetty/client/api/Response.java index 7255e36252f..e2e9a24ccb5 100644 --- a/jetty-client-new/src/main/java/org/eclipse/jetty/client/api/Response.java +++ b/jetty-client-new/src/main/java/org/eclipse/jetty/client/api/Response.java @@ -13,85 +13,63 @@ package org.eclipse.jetty.client.api; -import java.io.InputStream; import java.nio.ByteBuffer; +import org.eclipse.jetty.http.HttpFields; +import org.eclipse.jetty.http.HttpVersion; + public interface Response { - int getStatus(); + Request request(); - Headers getHeaders(); + Listener listener(); - Request getRequest(); + HttpVersion version(); - ContentProvider content(); + int status(); - InputStream contentAsStream(); + String reason(); + + HttpFields headers(); void abort(); public interface Listener { - public boolean onBegin(Response response, String version, int code, String message); + public void onBegin(Response response); - public boolean onHeader(Response response, String name, String value); + public void onHeaders(Response response); - public boolean onHeaders(Response response); + public void onContent(Response response, ByteBuffer content); - public boolean onContent(Response response, ByteBuffer content); + public void onSuccess(Response response); - public boolean onTrailer(Response response, String name, String value); - - public void onComplete(Response response); - - public void onException(Response response, Exception exception); - - public void onEnd(Response response); + public void onFailure(Response response, Throwable failure); public static class Adapter implements Listener { @Override - public boolean onBegin(Response response, String version, int code, String message) - { - return false; - } - - @Override - public boolean onHeader(Response response, String name, String value) - { - return false; - } - - @Override - public boolean onHeaders(Response response) - { - return false; - } - - @Override - public boolean onContent(Response response, ByteBuffer content) - { - return false; - } - - @Override - public boolean onTrailer(Response response, String name, String value) - { - return false; - } - - @Override - public void onComplete(Response response) + public void onBegin(Response response) { } @Override - public void onException(Response response, Exception exception) + public void onHeaders(Response response) { } @Override - public void onEnd(Response response) + public void onContent(Response response, ByteBuffer content) + { + } + + @Override + public void onSuccess(Response response) + { + } + + @Override + public void onFailure(Response response, Throwable failure) { } } diff --git a/jetty-client-new/src/test/java/org/eclipse/jetty/client/AbstractHttpClientTest.java b/jetty-client-new/src/test/java/org/eclipse/jetty/client/AbstractHttpClientTest.java new file mode 100644 index 00000000000..d5e2fceb72c --- /dev/null +++ b/jetty-client-new/src/test/java/org/eclipse/jetty/client/AbstractHttpClientTest.java @@ -0,0 +1,35 @@ +package org.eclipse.jetty.client; + +import org.eclipse.jetty.server.Handler; +import org.eclipse.jetty.server.NetworkConnector; +import org.eclipse.jetty.server.SelectChannelConnector; +import org.eclipse.jetty.server.Server; +import org.junit.After; + +public class AbstractHttpClientTest +{ + protected Server server; + protected HttpClient client; + protected NetworkConnector connector; + + public void start(Handler handler) throws Exception + { + server = new Server(); + connector = new SelectChannelConnector(server); + server.addConnector(connector); + server.setHandler(handler); + server.start(); + + client = new HttpClient(); + client.start(); + } + + @After + public void destroy() throws Exception + { + if (client != null) + client.stop(); + if (server != null) + server.stop(); + } +} diff --git a/jetty-client-new/src/test/java/org/eclipse/jetty/client/HTTPClientTest.java b/jetty-client-new/src/test/java/org/eclipse/jetty/client/HttpClientTest.java similarity index 66% rename from jetty-client-new/src/test/java/org/eclipse/jetty/client/HTTPClientTest.java rename to jetty-client-new/src/test/java/org/eclipse/jetty/client/HttpClientTest.java index d9b5f8f0aba..679c1bb34e0 100644 --- a/jetty-client-new/src/test/java/org/eclipse/jetty/client/HTTPClientTest.java +++ b/jetty-client-new/src/test/java/org/eclipse/jetty/client/HttpClientTest.java @@ -21,43 +21,12 @@ import javax.servlet.http.HttpServletResponse; import junit.framework.Assert; import org.eclipse.jetty.client.api.Response; -import org.eclipse.jetty.server.Connector; -import org.eclipse.jetty.server.Handler; import org.eclipse.jetty.server.Request; -import org.eclipse.jetty.server.SelectChannelConnector; -import org.eclipse.jetty.server.Server; import org.eclipse.jetty.server.handler.AbstractHandler; -import org.junit.After; import org.junit.Test; -public class HTTPClientTest +public class HttpClientTest extends AbstractHttpClientTest { - private Server server; - private HTTPClient client; - private Connector.NetConnector connector; - - public void start(Handler handler) throws Exception - { - server = new Server(); - connector = new SelectChannelConnector(); - server.addConnector(connector); - server.setHandler(handler); - server.start(); - - client = new HTTPClient(); - client.start(); - } - - @After - public void destroy() throws Exception - { - client.stop(); - client.join(5, TimeUnit.SECONDS); - - server.stop(); - server.join(); - } - @Test public void testGETNoResponseContent() throws Exception { @@ -73,6 +42,6 @@ public class HTTPClientTest Response response = client.GET("http://localhost:" + connector.getLocalPort()).get(5, TimeUnit.SECONDS); Assert.assertNotNull(response); - Assert.assertEquals(200, response.getStatus()); + Assert.assertEquals(200, response.status()); } } diff --git a/jetty-client-new/src/test/java/org/eclipse/jetty/client/HttpReceiverTest.java b/jetty-client-new/src/test/java/org/eclipse/jetty/client/HttpReceiverTest.java new file mode 100644 index 00000000000..ef81041b31f --- /dev/null +++ b/jetty-client-new/src/test/java/org/eclipse/jetty/client/HttpReceiverTest.java @@ -0,0 +1,160 @@ +package org.eclipse.jetty.client; + +public class HttpReceiverTest +{ +// private HttpClient client; +// +// @Before +// public void init() throws Exception +// { +// client = new HttpClient(); +// client.start(); +// } +// +// @After +// public void destroy() throws Exception +// { +// client.stop(); +// } +// +// @Test +// public void test_Receive_NoResponseContent() throws Exception +// { +// ByteArrayEndPoint endPoint = new ByteArrayEndPoint(); +// HttpConnection connection = new HttpConnection(client, endPoint); +// endPoint.setInput("" + +// "HTTP/1.1 200 OK\r\n" + +// "Content-length: 0\r\n" + +// "\r\n"); +// final AtomicReference responseRef = new AtomicReference<>(); +// final CountDownLatch latch = new CountDownLatch(1); +// HttpReceiver receiver = new HttpReceiver(connection, null, new Response.Listener.Adapter() +// { +// @Override +// public void onSuccess(Response response) +// { +// responseRef.set(response); +// latch.countDown(); +// } +// }); +// receiver.receive(connection); +// +// Assert.assertTrue(latch.await(5, TimeUnit.SECONDS)); +// Response response = responseRef.get(); +// Assert.assertNotNull(response); +// Assert.assertEquals(200, response.status()); +// Assert.assertEquals("OK", response.reason()); +// Assert.assertSame(HttpVersion.HTTP_1_1, response.version()); +// HttpFields headers = response.headers(); +// Assert.assertNotNull(headers); +// Assert.assertEquals(1, headers.size()); +// Assert.assertEquals("0", headers.get(HttpHeader.CONTENT_LENGTH)); +// } +// +// @Test +// public void test_Receive_ResponseContent() throws Exception +// { +// ByteArrayEndPoint endPoint = new ByteArrayEndPoint(); +// HttpConnection connection = new HttpConnection(client, endPoint); +// String content = "0123456789ABCDEF"; +// endPoint.setInput("" + +// "HTTP/1.1 200 OK\r\n" + +// "Content-length: " + content.length() + "\r\n" + +// "\r\n" + +// content); +// BufferingResponseListener listener = new BufferingResponseListener(); +// HttpReceiver receiver = new HttpReceiver(connection, null, listener); +// receiver.receive(connection); +// +// Response response = listener.await(5, TimeUnit.SECONDS); +// Assert.assertNotNull(response); +// Assert.assertEquals(200, response.status()); +// Assert.assertEquals("OK", response.reason()); +// Assert.assertSame(HttpVersion.HTTP_1_1, response.version()); +// HttpFields headers = response.headers(); +// Assert.assertNotNull(headers); +// Assert.assertEquals(1, headers.size()); +// Assert.assertEquals(String.valueOf(content.length()), headers.get(HttpHeader.CONTENT_LENGTH)); +// String received = listener.contentAsString("UTF-8"); +// Assert.assertEquals(content, received); +// } +// +// @Test +// public void test_Receive_ResponseContent_EarlyEOF() throws Exception +// { +// ByteArrayEndPoint endPoint = new ByteArrayEndPoint(); +// HttpConnection connection = new HttpConnection(client, endPoint); +// String content1 = "0123456789"; +// String content2 = "ABCDEF"; +// endPoint.setInput("" + +// "HTTP/1.1 200 OK\r\n" + +// "Content-length: " + (content1.length() + content2.length()) + "\r\n" + +// "\r\n" + +// content1); +// BufferingResponseListener listener = new BufferingResponseListener(); +// HttpReceiver receiver = new HttpReceiver(connection, null, listener); +// receiver.receive(connection); +// endPoint.setInputEOF(); +// receiver.receive(connection); +// +// try +// { +// listener.await(5, TimeUnit.SECONDS); +// Assert.fail(); +// } +// catch (ExecutionException e) +// { +// Assert.assertTrue(e.getCause() instanceof EOFException); +// } +// } +// +// @Test +// public void test_Receive_ResponseContent_IdleTimeout() throws Exception +// { +// ByteArrayEndPoint endPoint = new ByteArrayEndPoint(); +// HttpConnection connection = new HttpConnection(client, endPoint); +// endPoint.setInput("" + +// "HTTP/1.1 200 OK\r\n" + +// "Content-length: 1\r\n" + +// "\r\n"); +// BufferingResponseListener listener = new BufferingResponseListener(); +// HttpReceiver receiver = new HttpReceiver(connection, null, listener); +// receiver.receive(connection); +// // Simulate an idle timeout +// receiver.idleTimeout(); +// +// try +// { +// listener.await(5, TimeUnit.SECONDS); +// Assert.fail(); +// } +// catch (ExecutionException e) +// { +// Assert.assertTrue(e.getCause() instanceof TimeoutException); +// } +// } +// +// @Test +// public void test_Receive_BadResponse() throws Exception +// { +// ByteArrayEndPoint endPoint = new ByteArrayEndPoint(); +// HttpConnection connection = new HttpConnection(client, endPoint); +// endPoint.setInput("" + +// "HTTP/1.1 200 OK\r\n" + +// "Content-length: A\r\n" + +// "\r\n"); +// BufferingResponseListener listener = new BufferingResponseListener(); +// HttpReceiver receiver = new HttpReceiver(connection, null, listener); +// receiver.receive(connection); +// +// try +// { +// listener.await(5, TimeUnit.SECONDS); +// Assert.fail(); +// } +// catch (ExecutionException e) +// { +// Assert.assertTrue(e.getCause() instanceof HttpResponseException); +// } +// } +} diff --git a/jetty-client-new/src/test/java/org/eclipse/jetty/client/HttpSenderTest.java b/jetty-client-new/src/test/java/org/eclipse/jetty/client/HttpSenderTest.java new file mode 100644 index 00000000000..dda7353570f --- /dev/null +++ b/jetty-client-new/src/test/java/org/eclipse/jetty/client/HttpSenderTest.java @@ -0,0 +1,266 @@ +package org.eclipse.jetty.client; + +import java.net.URI; +import java.nio.ByteBuffer; +import java.util.concurrent.CountDownLatch; +import java.util.concurrent.TimeUnit; + +import org.eclipse.jetty.client.api.Request; +import org.eclipse.jetty.client.api.Response; +import org.eclipse.jetty.io.ByteArrayEndPoint; +import org.junit.After; +import org.junit.Assert; +import org.junit.Before; +import org.junit.Test; + +public class HttpSenderTest +{ + private HttpClient client; + + @Before + public void init() throws Exception + { + client = new HttpClient(); + client.start(); + } + + @After + public void destroy() throws Exception + { + client.stop(); + } + + @Test + public void test_Send_NoRequestContent() throws Exception + { + ByteArrayEndPoint endPoint = new ByteArrayEndPoint(); + HttpConnection connection = new HttpConnection(client, endPoint); + Request httpRequest = new HttpRequest(client, URI.create("http://localhost/")); + final CountDownLatch headersLatch = new CountDownLatch(1); + final CountDownLatch successLatch = new CountDownLatch(1); + httpRequest.listener(new Request.Listener.Adapter() + { + @Override + public void onHeaders(Request request) + { + headersLatch.countDown(); + } + + @Override + public void onSuccess(Request request) + { + successLatch.countDown(); + } + }); + connection.send(httpRequest, null); + + String requestString = endPoint.takeOutputString(); + Assert.assertTrue(requestString.startsWith("GET ")); + Assert.assertTrue(requestString.endsWith("\r\n\r\n")); + Assert.assertTrue(headersLatch.await(5, TimeUnit.SECONDS)); + Assert.assertTrue(successLatch.await(5, TimeUnit.SECONDS)); + } + + @Test + public void test_Send_NoRequestContent_IncompleteFlush() throws Exception + { + ByteArrayEndPoint endPoint = new ByteArrayEndPoint("", 16); + HttpConnection connection = new HttpConnection(client, endPoint); + Request httpRequest = new HttpRequest(client, URI.create("http://localhost/")); + final CountDownLatch headersLatch = new CountDownLatch(1); + httpRequest.listener(new Request.Listener.Adapter() + { + @Override + public void onHeaders(Request request) + { + headersLatch.countDown(); + } + }); + connection.send(httpRequest, null); + + // This take will free space in the buffer and allow for the write to complete + StringBuilder request = new StringBuilder(endPoint.takeOutputString()); + + // Wait for the write to complete + Assert.assertTrue(headersLatch.await(5, TimeUnit.SECONDS)); + request.append(endPoint.takeOutputString()); + + String requestString = request.toString(); + Assert.assertTrue(requestString.startsWith("GET ")); + Assert.assertTrue(requestString.endsWith("\r\n\r\n")); + } + + @Test + public void test_Send_NoRequestContent_Exception() throws Exception + { + ByteArrayEndPoint endPoint = new ByteArrayEndPoint(); + // Shutdown output to trigger the exception on write + endPoint.shutdownOutput(); + HttpConnection connection = new HttpConnection(client, endPoint); + Request httpRequest = new HttpRequest(client, URI.create("http://localhost/")); + final CountDownLatch failureLatch = new CountDownLatch(2); + httpRequest.listener(new Request.Listener.Adapter() + { + @Override + public void onFailure(Request request, Throwable x) + { + failureLatch.countDown(); + } + }); + connection.send(httpRequest, new Response.Listener.Adapter() + { + @Override + public void onFailure(Response response, Throwable failure) + { + failureLatch.countDown(); + } + }); + + Assert.assertTrue(failureLatch.await(5, TimeUnit.SECONDS)); + } + + @Test + public void test_Send_NoRequestContent_IncompleteFlush_Exception() throws Exception + { + ByteArrayEndPoint endPoint = new ByteArrayEndPoint("", 16); + HttpConnection connection = new HttpConnection(client, endPoint); + Request httpRequest = new HttpRequest(client, URI.create("http://localhost/")); + final CountDownLatch failureLatch = new CountDownLatch(2); + httpRequest.listener(new Request.Listener.Adapter() + { + @Override + public void onFailure(Request request, Throwable x) + { + failureLatch.countDown(); + } + }); + connection.send(httpRequest, new Response.Listener.Adapter() + { + @Override + public void onFailure(Response response, Throwable failure) + { + failureLatch.countDown(); + } + }); + + // Shutdown output to trigger the exception on write + endPoint.shutdownOutput(); + // This take will free space in the buffer and allow for the write to complete + // although it will fail because we shut down the output + endPoint.takeOutputString(); + + Assert.assertTrue(failureLatch.await(5, TimeUnit.SECONDS)); + } + + @Test + public void test_Send_SmallRequestContent_InOneBuffer() throws Exception + { + ByteArrayEndPoint endPoint = new ByteArrayEndPoint(); + HttpConnection connection = new HttpConnection(client, endPoint); + Request httpRequest = new HttpRequest(client, URI.create("http://localhost/")); + String content = "abcdef"; + httpRequest.content(new ByteBufferContentProvider(ByteBuffer.wrap(content.getBytes("UTF-8")))); + final CountDownLatch headersLatch = new CountDownLatch(1); + final CountDownLatch successLatch = new CountDownLatch(1); + httpRequest.listener(new Request.Listener.Adapter() + { + @Override + public void onHeaders(Request request) + { + headersLatch.countDown(); + } + + @Override + public void onSuccess(Request request) + { + successLatch.countDown(); + } + }); + connection.send(httpRequest, null); + + String requestString = endPoint.takeOutputString(); + Assert.assertTrue(requestString.startsWith("GET ")); + Assert.assertTrue(requestString.endsWith("\r\n\r\n" + content)); + Assert.assertTrue(headersLatch.await(5, TimeUnit.SECONDS)); + Assert.assertTrue(successLatch.await(5, TimeUnit.SECONDS)); + } + + @Test + public void test_Send_SmallRequestContent_InTwoBuffers() throws Exception + { + ByteArrayEndPoint endPoint = new ByteArrayEndPoint(); + HttpConnection connection = new HttpConnection(client, endPoint); + Request httpRequest = new HttpRequest(client, URI.create("http://localhost/")); + String content1 = "0123456789"; + String content2 = "abcdef"; + httpRequest.content(new ByteBufferContentProvider(ByteBuffer.wrap(content1.getBytes("UTF-8")), ByteBuffer.wrap(content2.getBytes("UTF-8")))); + final CountDownLatch headersLatch = new CountDownLatch(1); + final CountDownLatch successLatch = new CountDownLatch(1); + httpRequest.listener(new Request.Listener.Adapter() + { + @Override + public void onHeaders(Request request) + { + headersLatch.countDown(); + } + + @Override + public void onSuccess(Request request) + { + successLatch.countDown(); + } + }); + connection.send(httpRequest, null); + + String requestString = endPoint.takeOutputString(); + Assert.assertTrue(requestString.startsWith("GET ")); + Assert.assertTrue(requestString.endsWith("\r\n\r\n" + content1 + content2)); + Assert.assertTrue(headersLatch.await(5, TimeUnit.SECONDS)); + Assert.assertTrue(successLatch.await(5, TimeUnit.SECONDS)); + } + + @Test + public void test_Send_SmallRequestContent_Chunked_InTwoChunks() throws Exception + { + ByteArrayEndPoint endPoint = new ByteArrayEndPoint(); + HttpConnection connection = new HttpConnection(client, endPoint); + Request httpRequest = new HttpRequest(client, URI.create("http://localhost/")); + String content1 = "0123456789"; + String content2 = "ABCDEF"; + httpRequest.content(new ByteBufferContentProvider(ByteBuffer.wrap(content1.getBytes("UTF-8")), ByteBuffer.wrap(content2.getBytes("UTF-8"))) + { + @Override + public long length() + { + return -1; + } + }); + final CountDownLatch headersLatch = new CountDownLatch(1); + final CountDownLatch successLatch = new CountDownLatch(1); + httpRequest.listener(new Request.Listener.Adapter() + { + @Override + public void onHeaders(Request request) + { + headersLatch.countDown(); + } + + @Override + public void onSuccess(Request request) + { + successLatch.countDown(); + } + }); + connection.send(httpRequest, null); + + String requestString = endPoint.takeOutputString(); + Assert.assertTrue(requestString.startsWith("GET ")); + String content = Integer.toHexString(content1.length()).toUpperCase() + "\r\n" + content1 + "\r\n"; + content += Integer.toHexString(content2.length()).toUpperCase() + "\r\n" + content2 + "\r\n"; + content += "0\r\n\r\n"; + Assert.assertTrue(requestString.endsWith("\r\n\r\n" + content)); + Assert.assertTrue(headersLatch.await(5, TimeUnit.SECONDS)); + Assert.assertTrue(successLatch.await(5, TimeUnit.SECONDS)); + } + +} diff --git a/jetty-client-new/src/test/java/org/eclipse/jetty/client/RedirectionTest.java b/jetty-client-new/src/test/java/org/eclipse/jetty/client/RedirectionTest.java new file mode 100644 index 00000000000..fbfb76ecbf5 --- /dev/null +++ b/jetty-client-new/src/test/java/org/eclipse/jetty/client/RedirectionTest.java @@ -0,0 +1,58 @@ +package org.eclipse.jetty.client; + +import java.io.IOException; +import java.util.concurrent.TimeUnit; +import javax.servlet.ServletException; +import javax.servlet.http.HttpServletRequest; +import javax.servlet.http.HttpServletResponse; + +import org.eclipse.jetty.client.api.Response; +import org.eclipse.jetty.http.HttpHeader; +import org.eclipse.jetty.server.Request; +import org.eclipse.jetty.server.handler.AbstractHandler; +import org.junit.Assert; +import org.junit.Before; +import org.junit.Test; + +public class RedirectionTest extends AbstractHttpClientTest +{ + @Before + public void init() throws Exception + { + start(new RedirectHandler()); + } + + @Test + public void test303() throws Exception + { + Response response = client.newRequest("localhost", connector.getLocalPort()) + .path("/303/done") + .send().get(5, TimeUnit.SECONDS); + Assert.assertNotNull(response); + Assert.assertEquals(200, response.status()); + Assert.assertFalse(response.headers().containsKey(HttpHeader.LOCATION.asString())); + } + + private class RedirectHandler extends AbstractHandler + { + @Override + public void handle(String target, Request baseRequest, HttpServletRequest request, HttpServletResponse response) throws IOException, ServletException + { + try + { + String[] paths = target.split("/", 3); + int status = Integer.parseInt(paths[1]); + response.setStatus(status); + response.setHeader("Location", request.getScheme() + "://" + request.getServerName() + ":" + request.getServerPort() + "/" + paths[2]); + } + catch (NumberFormatException x) + { + response.setStatus(200); + } + finally + { + baseRequest.setHandled(true); + } + } + } +} diff --git a/jetty-client-new/src/test/java/org/eclipse/jetty/client/api/ApacheUsage.java b/jetty-client-new/src/test/java/org/eclipse/jetty/client/api/ApacheUsage.java index 028532ad5ec..9365a2b4ecc 100644 --- a/jetty-client-new/src/test/java/org/eclipse/jetty/client/api/ApacheUsage.java +++ b/jetty-client-new/src/test/java/org/eclipse/jetty/client/api/ApacheUsage.java @@ -15,8 +15,10 @@ package org.eclipse.jetty.client.api; import org.apache.http.client.HttpClient; import org.apache.http.impl.client.DefaultHttpClient; +import org.junit.Ignore; import org.junit.Test; +@Ignore public class ApacheUsage { @Test diff --git a/jetty-client-new/src/test/java/org/eclipse/jetty/client/api/NingUsage.java b/jetty-client-new/src/test/java/org/eclipse/jetty/client/api/NingUsage.java index 10eac876e28..969ba1d5ba1 100644 --- a/jetty-client-new/src/test/java/org/eclipse/jetty/client/api/NingUsage.java +++ b/jetty-client-new/src/test/java/org/eclipse/jetty/client/api/NingUsage.java @@ -22,8 +22,10 @@ import com.ning.http.client.Cookie; import com.ning.http.client.Realm; import com.ning.http.client.Request; import com.ning.http.client.Response; +import org.junit.Ignore; import org.junit.Test; +@Ignore public class NingUsage { @Test diff --git a/jetty-client-new/src/test/java/org/eclipse/jetty/client/api/Usage.java b/jetty-client-new/src/test/java/org/eclipse/jetty/client/api/Usage.java index f48a3833c2a..3452e12db1e 100644 --- a/jetty-client-new/src/test/java/org/eclipse/jetty/client/api/Usage.java +++ b/jetty-client-new/src/test/java/org/eclipse/jetty/client/api/Usage.java @@ -13,14 +13,21 @@ package org.eclipse.jetty.client.api; -import java.io.File; import java.io.FileOutputStream; import java.io.InputStream; +import java.nio.file.Paths; +import java.util.concurrent.CountDownLatch; import java.util.concurrent.Future; import java.util.concurrent.TimeUnit; +import java.util.concurrent.atomic.AtomicReference; -import org.eclipse.jetty.client.HTTPClient; -import org.eclipse.jetty.client.StreamResponseListener; +import org.eclipse.jetty.client.BufferingResponseListener; +import org.eclipse.jetty.client.HttpClient; +import org.eclipse.jetty.client.PathContentProvider; +import org.eclipse.jetty.client.StreamingResponseListener; +import org.eclipse.jetty.http.HttpMethod; +import org.eclipse.jetty.http.HttpVersion; +import org.junit.Assert; import org.junit.Ignore; import org.junit.Test; @@ -28,122 +35,134 @@ import org.junit.Test; public class Usage { @Test - public void testSimpleBlockingGET() throws Exception + public void testGETBlocking_ShortAPI() throws Exception { - HTTPClient client = new HTTPClient(); + HttpClient client = new HttpClient(); Future responseFuture = client.GET("http://localhost:8080/foo"); Response response = responseFuture.get(); - response.getStatus(); // 200 + Assert.assertEquals(200, response.status()); // Headers abstraction needed for: // 1. case insensitivity // 2. multi values // 3. value conversion // Reuse SPDY's ? - response.getHeaders().get("Content-Length").valueAsInt(); + response.headers().get("Content-Length"); } @Test - public void testBlockingGET() throws Exception + public void testGETBlocking() throws Exception { - HTTPClient client = new HTTPClient(); + HttpClient client = new HttpClient(); // Address must be provided, it's the only thing non defaultable - Request.Builder builder = client.builder("localhost:8080"); - Future responseFuture = builder.method("GET").path("/").header("Origin", "localhost").build().send(); - responseFuture.get(); + Request request = client.newRequest("localhost", 8080) + .scheme("https") + .method(HttpMethod.GET) + .path("/uri") + .version(HttpVersion.HTTP_1_1) + .param("a", "b") + .header("X-Header", "Y-value") + .agent("Jetty HTTP Client") + .cookie("cookie1", "value1") + .decoder(null) + .content(null) + .idleTimeout(5000L); + Future responseFuture = request.send(); + Response response = responseFuture.get(); + Assert.assertEquals(200, response.status()); } - @Test - public void testSimpleAsyncGET() throws Exception + public void testGETAsync() throws Exception { - HTTPClient client = new HTTPClient(); - client.builder("localhost:8080").method("GET").path("/").header("Origin", "localhost").build().send(new Response.Listener.Adapter() + HttpClient client = new HttpClient(); + final AtomicReference responseRef = new AtomicReference<>(); + final CountDownLatch latch = new CountDownLatch(1); + client.newRequest("localhost", 8080).send(new Response.Listener.Adapter() { @Override - public void onEnd(Response response) + public void onSuccess(Response response) { + responseRef.set(response); + latch.countDown(); } }); + Assert.assertTrue(latch.await(5, TimeUnit.SECONDS)); + Response response = responseRef.get(); + Assert.assertNotNull(response); + Assert.assertEquals(200, response.status()); } @Test public void testRequestListener() throws Exception { - HTTPClient client = new HTTPClient(); - Response response = client.builder("localhost:8080") - .method("GET") - .path("/") + HttpClient client = new HttpClient(); + Response response = client.newRequest("localhost", 8080) .listener(new Request.Listener.Adapter() { @Override - public void onEnd(Request request) + public void onSuccess(Request request) { } - }) - .build().send(new Response.Listener.Adapter() - { - @Override - public void onEnd(Response response) - { - } - }).get(); - response.getStatus(); + }).send().get(5, TimeUnit.SECONDS); + Assert.assertEquals(200, response.status()); } @Test public void testRequestWithExplicitConnectionControl() throws Exception { - HTTPClient client = new HTTPClient(); - try (Connection connection = client.getDestination(Address.from("localhost:8080")).connect(5, TimeUnit.SECONDS)) + HttpClient client = new HttpClient(); + try (Connection connection = client.getDestination("http", "localhost", 8080).newConnection().get(5, TimeUnit.SECONDS)) { - Request.Builder builder = client.builder("localhost:8080"); - Request request = builder.method("GET").path("/").header("Origin", "localhost").build(); - - Future response = connection.send(request, new Response.Listener.Adapter()); - response.get().getStatus(); + Request request = client.newRequest("localhost", 8080); + BufferingResponseListener listener = new BufferingResponseListener(); + connection.send(request, listener); + Response response = listener.await(5, TimeUnit.SECONDS); + Assert.assertNotNull(response); + Assert.assertEquals(200, response.status()); } } @Test public void testFileUpload() throws Exception { - HTTPClient client = new HTTPClient(); - Response response = client.builder("localhost:8080") - .method("GET").path("/").file(new File("")).build().send().get(); - response.getStatus(); + HttpClient client = new HttpClient(); + Response response = client.newRequest("localhost", 8080) + .content(new PathContentProvider(Paths.get(""))).send().get(); + Assert.assertEquals(200, response.status()); } @Test public void testCookie() throws Exception { - HTTPClient client = new HTTPClient(); - client.builder("localhost:8080").cookie("key", "value").build().send().get().getStatus(); // 200 + HttpClient client = new HttpClient(); + Response response = client.newRequest("localhost", 8080).cookie("key", "value").send().get(); + Assert.assertEquals(200, response.status()); } - @Test - public void testAuthentication() throws Exception - { - HTTPClient client = new HTTPClient(); - client.builder("localhost:8080").authentication(new Authentication.Kerberos()).build().send().get().getStatus(); // 200 - } +// @Test +// public void testAuthentication() throws Exception +// { +// HTTPClient client = new HTTPClient(); +// client.newRequest("localhost", 8080).authentication(new Authentication.Kerberos()).build().send().get().status(); // 200 +// } @Test public void testFollowRedirects() throws Exception { - HTTPClient client = new HTTPClient(); + HttpClient client = new HttpClient(); client.setFollowRedirects(false); - client.builder("localhost:8080").followRedirects(true).build().send().get().getStatus(); // 200 + client.newRequest("localhost", 8080).followRedirects(true).send().get().status(); // 200 } @Test public void testResponseStream() throws Exception { - HTTPClient client = new HTTPClient(); - StreamResponseListener listener = new StreamResponseListener(); - client.builder("localhost:8080").build().send(listener); + HttpClient client = new HttpClient(); + StreamingResponseListener listener = new StreamingResponseListener(); + client.newRequest("localhost", 8080).send(listener); // Call to get() blocks until the headers arrived Response response = listener.get(5, TimeUnit.SECONDS); - if (response.getStatus() == 200) + if (response.status() == 200) { // Solution 1: use input stream byte[] buffer = new byte[256]; diff --git a/jetty-client-new/src/test/resources/jetty-logging.properties b/jetty-client-new/src/test/resources/jetty-logging.properties new file mode 100644 index 00000000000..276e49dd14a --- /dev/null +++ b/jetty-client-new/src/test/resources/jetty-logging.properties @@ -0,0 +1,2 @@ +org.eclipse.jetty.util.log.class=org.eclipse.jetty.util.log.StdErrLog +org.eclipse.jetty.LEVEL=DEBUG diff --git a/jetty-http/src/main/java/org/eclipse/jetty/http/HttpFields.java b/jetty-http/src/main/java/org/eclipse/jetty/http/HttpFields.java index b8116c47b98..2f7377caf88 100644 --- a/jetty-http/src/main/java/org/eclipse/jetty/http/HttpFields.java +++ b/jetty-http/src/main/java/org/eclipse/jetty/http/HttpFields.java @@ -49,10 +49,10 @@ import org.eclipse.jetty.util.log.Logger; /* ------------------------------------------------------------ */ /** * HTTP Fields. A collection of HTTP header and or Trailer fields. - * + * *

This class is not synchronized as it is expected that modifications will only be performed by a * single thread. - * + * */ public class HttpFields implements Iterable { @@ -352,7 +352,7 @@ public class HttpFields implements Iterable /** * Get a Field by index. * @return A Field value or null if the Field value has not been set - * + * */ public Field getField(int i) { @@ -418,7 +418,7 @@ public class HttpFields implements Iterable /* -------------------------------------------------------------- */ /** * Get multi headers - * + * * @return Enumeration of the values, or null if no such header. * @param name the case-insensitive field name */ @@ -441,7 +441,7 @@ public class HttpFields implements Iterable /* -------------------------------------------------------------- */ /** * Get multi headers - * + * * @return Enumeration of the values * @param name the case-insensitive field name */ @@ -480,7 +480,7 @@ public class HttpFields implements Iterable * Get multi field values with separator. The multiple values can be represented as separate * headers of the same name, or by a single header using the separator(s), or a combination of * both. Separators may be quoted. - * + * * @param name the case-insensitive field name * @param separators String of separators. * @return Enumeration of the values, or null if no such header. @@ -523,7 +523,7 @@ public class HttpFields implements Iterable /* -------------------------------------------------------------- */ /** * Set a field. - * + * * @param name the name of the field * @param value the value of the field. If null the field is cleared. */ @@ -544,11 +544,11 @@ public class HttpFields implements Iterable { put(header,value.toString()); } - + /* -------------------------------------------------------------- */ /** * Set a field. - * + * * @param header the header name of the field * @param value the value of the field. If null the field is cleared. */ @@ -567,7 +567,7 @@ public class HttpFields implements Iterable /* -------------------------------------------------------------- */ /** * Set a field. - * + * * @param name the name of the field * @param list the List value of the field. If null the field is cleared. */ @@ -583,7 +583,7 @@ public class HttpFields implements Iterable /** * Add to or set a field. If the field is allowed to have multiple values, add will add multiple * headers of the same name. - * + * * @param name the name of the field * @param value the value of the field. * @exception IllegalArgumentException If the name is a single valued field and already has a @@ -591,9 +591,9 @@ public class HttpFields implements Iterable */ public void add(String name, String value) throws IllegalArgumentException { - if (value == null) + if (value == null) return; - + Field field = _names.get(name); Field last = null; while (field != null) @@ -612,18 +612,18 @@ public class HttpFields implements Iterable else _names.put(name, field); } - + /* -------------------------------------------------------------- */ public void add(HttpHeader header, HttpHeaderValue value) throws IllegalArgumentException { add(header,value.toString()); } - + /* -------------------------------------------------------------- */ /** * Add to or set a field. If the field is allowed to have multiple values, add will add multiple * headers of the same name. - * + * * @param name the name of the field * @param value the value of the field. * @exception IllegalArgumentException If the name is a single valued field and already has a @@ -655,18 +655,18 @@ public class HttpFields implements Iterable /* ------------------------------------------------------------ */ /** * Remove a field. - * + * * @param name */ public void remove(HttpHeader name) { remove(name.toString()); } - + /* ------------------------------------------------------------ */ /** * Remove a field. - * + * * @param name */ public void remove(String name) @@ -683,7 +683,7 @@ public class HttpFields implements Iterable /** * Get a header as an long value. Returns the value of an integer field or -1 if not found. The * case of the field name is ignored. - * + * * @param name the case-insensitive field name * @exception NumberFormatException If bad long found */ @@ -697,7 +697,7 @@ public class HttpFields implements Iterable /** * Get a header as a date value. Returns the value of a date field, or -1 if not found. The case * of the field name is ignored. - * + * * @param name the case-insensitive field name */ public long getDateField(String name) @@ -720,7 +720,7 @@ public class HttpFields implements Iterable /* -------------------------------------------------------------- */ /** * Sets the value of an long field. - * + * * @param name the field name * @param value the field long value */ @@ -729,11 +729,11 @@ public class HttpFields implements Iterable String v = Long.toString(value); put(name, v); } - + /* -------------------------------------------------------------- */ /** * Sets the value of an long field. - * + * * @param name the field name * @param value the field long value */ @@ -747,7 +747,7 @@ public class HttpFields implements Iterable /* -------------------------------------------------------------- */ /** * Sets the value of a date field. - * + * * @param name the field name * @param date the field date value */ @@ -760,7 +760,7 @@ public class HttpFields implements Iterable /* -------------------------------------------------------------- */ /** * Sets the value of a date field. - * + * * @param name the field name * @param date the field date value */ @@ -769,11 +769,11 @@ public class HttpFields implements Iterable String d=formatDate(date); put(name, d); } - + /* -------------------------------------------------------------- */ /** * Sets the value of a date field. - * + * * @param name the field name * @param date the field date value */ @@ -786,7 +786,7 @@ public class HttpFields implements Iterable /* ------------------------------------------------------------ */ /** * Format a set cookie value - * + * * @param cookie The cookie. */ public void addSetCookie(HttpCookie cookie) @@ -805,7 +805,7 @@ public class HttpFields implements Iterable /** * Format a set cookie value - * + * * @param name the name * @param value the value * @param domain the domain @@ -842,7 +842,7 @@ public class HttpFields implements Iterable if (value != null && value.length() > 0) quoted|=QuotedStringTokenizer.quoteIfNeeded(buf, value, delim); - + if (path != null && path.length() > 0) { buf.append(";Path="); @@ -921,7 +921,8 @@ public class HttpFields implements Iterable /* -------------------------------------------------------------- */ @Override - public String toString() + public String + toString() { try { @@ -963,7 +964,7 @@ public class HttpFields implements Iterable /** * Add fields from another HttpFields instance. Single valued fields are replaced, while all * others are added. - * + * * @param fields */ public void add(HttpFields fields) @@ -984,13 +985,13 @@ public class HttpFields implements Iterable /** * Get field value parameters. Some field values can have parameters. This method separates the * value from the parameters and optionally populates a map with the parameters. For example: - * + * *

-     * 
+     *
      * FieldName : Value ; param1=val1 ; param2=val2
-     * 
+     *
      * 
- * + * * @param value The Field value, possibly with parameteres. * @param parameters A map to populate with the parameters, or null * @return The value. @@ -1083,7 +1084,7 @@ public class HttpFields implements Iterable /* ------------------------------------------------------------ */ /** * List values in quality order. - * + * * @param e Enumeration of values with quality parameters * @return values in quality order. */ @@ -1153,7 +1154,7 @@ public class HttpFields implements Iterable _value = value; _next = null; } - + /* ------------------------------------------------------------ */ private Field(String name, String value) { @@ -1179,7 +1180,7 @@ public class HttpFields implements Iterable } return bytes; } - + /* ------------------------------------------------------------ */ private byte[] toSanitisedValue(String s) { @@ -1236,7 +1237,7 @@ public class HttpFields implements Iterable { return _header; } - + /* ------------------------------------------------------------ */ public String getName() { @@ -1273,20 +1274,20 @@ public class HttpFields implements Iterable { if (_value==null) return false; - + if (value.equalsIgnoreCase(_value)) return true; - + String[] split = _value.split("\\s*,\\s*"); for (String s : split) { if (value.equalsIgnoreCase(s)) return true; } - + if (_next!=null) return _next.contains(value); - + return false; } } diff --git a/jetty-http/src/main/java/org/eclipse/jetty/http/HttpParser.java b/jetty-http/src/main/java/org/eclipse/jetty/http/HttpParser.java index ee12cbabc9c..8f76b4a31c2 100644 --- a/jetty-http/src/main/java/org/eclipse/jetty/http/HttpParser.java +++ b/jetty-http/src/main/java/org/eclipse/jetty/http/HttpParser.java @@ -1141,7 +1141,7 @@ public class HttpParser } /* ------------------------------------------------------------------------------- */ - public void inputShutdown() + public void shutdownInput() { // was this unexpected? switch(_state) diff --git a/jetty-http/src/test/java/org/eclipse/jetty/http/HttpParserTest.java b/jetty-http/src/test/java/org/eclipse/jetty/http/HttpParserTest.java index c1ecdd11cd0..98d13e8f598 100644 --- a/jetty-http/src/test/java/org/eclipse/jetty/http/HttpParserTest.java +++ b/jetty-http/src/test/java/org/eclipse/jetty/http/HttpParserTest.java @@ -324,7 +324,7 @@ public class HttpParserTest parser.reset(); init(); parser.parseNext(buffer); - parser.inputShutdown(); + parser.shutdownInput(); assertEquals("PUT", _methodOrVersion); assertEquals("/doodle", _uriOrStatus); assertEquals("HTTP/1.0", _versionOrReason); @@ -400,7 +400,7 @@ public class HttpParserTest init(); parser.parseNext(buffer); - parser.inputShutdown(); + parser.shutdownInput(); assertEquals("HTTP/1.1", _methodOrVersion); assertEquals("200", _uriOrStatus); assertEquals("Correct", _versionOrReason); diff --git a/jetty-io/src/main/java/org/eclipse/jetty/io/ByteArrayEndPoint.java b/jetty-io/src/main/java/org/eclipse/jetty/io/ByteArrayEndPoint.java index 63d4af63db3..e6cace7793a 100644 --- a/jetty-io/src/main/java/org/eclipse/jetty/io/ByteArrayEndPoint.java +++ b/jetty-io/src/main/java/org/eclipse/jetty/io/ByteArrayEndPoint.java @@ -39,14 +39,14 @@ public class ByteArrayEndPoint extends AbstractEndPoint { static final Logger LOG = Log.getLogger(ByteArrayEndPoint.class); public final static InetSocketAddress NOIP=new InetSocketAddress(0); - + protected ByteBuffer _in; protected ByteBuffer _out; protected boolean _ishut; protected boolean _oshut; protected boolean _closed; protected boolean _growOutput; - + /* ------------------------------------------------------------ */ /** @@ -102,14 +102,14 @@ public class ByteArrayEndPoint extends AbstractEndPoint setIdleTimeout(idleTimeoutMs); } - - + + /* ------------------------------------------------------------ */ @Override protected void onIncompleteFlush() - { + { // Don't need to do anything here as takeOutput does the signalling. } @@ -177,7 +177,16 @@ public class ByteArrayEndPoint extends AbstractEndPoint */ public String getOutputString() { - return BufferUtil.toString(_out,StringUtil.__UTF8_CHARSET); + return getOutputString(StringUtil.__UTF8_CHARSET); + } + + /* ------------------------------------------------------------ */ + /** + * @return Returns the out. + */ + public String getOutputString(Charset charset) + { + return BufferUtil.toString(_out,charset); } /* ------------------------------------------------------------ */ @@ -198,17 +207,17 @@ public class ByteArrayEndPoint extends AbstractEndPoint */ public String takeOutputString() { - ByteBuffer buffer=takeOutput(); - return BufferUtil.toString(buffer,StringUtil.__UTF8_CHARSET); + return takeOutputString(StringUtil.__UTF8_CHARSET); } /* ------------------------------------------------------------ */ /** * @return Returns the out. */ - public String getOutputString(Charset charset) + public String takeOutputString(Charset charset) { - return BufferUtil.toString(_out,charset); + ByteBuffer buffer=takeOutput(); + return BufferUtil.toString(buffer,charset); } /* ------------------------------------------------------------ */ @@ -397,7 +406,7 @@ public class ByteArrayEndPoint extends AbstractEndPoint super.setIdleTimeout(idleTimeout); scheduleIdleTimeout(idleTimeout); } - + } diff --git a/jetty-io/src/main/java/org/eclipse/jetty/io/ChannelEndPoint.java b/jetty-io/src/main/java/org/eclipse/jetty/io/ChannelEndPoint.java index ce413d9d6cb..52fa2eb428d 100644 --- a/jetty-io/src/main/java/org/eclipse/jetty/io/ChannelEndPoint.java +++ b/jetty-io/src/main/java/org/eclipse/jetty/io/ChannelEndPoint.java @@ -47,7 +47,7 @@ public class ChannelEndPoint extends AbstractEndPoint private volatile boolean _ishut; private volatile boolean _oshut; - public ChannelEndPoint(ScheduledExecutorService scheduler,SocketChannel channel) throws IOException + public ChannelEndPoint(ScheduledExecutorService scheduler,SocketChannel channel) { super(scheduler, (InetSocketAddress)channel.socket().getLocalSocketAddress(), @@ -186,7 +186,7 @@ public class ChannelEndPoint extends AbstractEndPoint if (flushed>0) { notIdle(); - + // clear empty buffers to prevent position creeping up the buffer for (ByteBuffer b : buffers) if (BufferUtil.isEmpty(b)) diff --git a/jetty-io/src/main/java/org/eclipse/jetty/io/MappedByteBufferPool.java b/jetty-io/src/main/java/org/eclipse/jetty/io/MappedByteBufferPool.java index 7b7479c0b93..ece77e59465 100644 --- a/jetty-io/src/main/java/org/eclipse/jetty/io/MappedByteBufferPool.java +++ b/jetty-io/src/main/java/org/eclipse/jetty/io/MappedByteBufferPool.java @@ -66,6 +66,9 @@ public class MappedByteBufferPool implements ByteBufferPool public void release(ByteBuffer buffer) { + if (buffer == null) + return; + int bucket = bucketFor(buffer.capacity()); ConcurrentMap> buffers = buffersFor(buffer.isDirect()); diff --git a/jetty-io/src/main/java/org/eclipse/jetty/io/SelectChannelEndPoint.java b/jetty-io/src/main/java/org/eclipse/jetty/io/SelectChannelEndPoint.java index f27e011cec3..8d944207362 100644 --- a/jetty-io/src/main/java/org/eclipse/jetty/io/SelectChannelEndPoint.java +++ b/jetty-io/src/main/java/org/eclipse/jetty/io/SelectChannelEndPoint.java @@ -18,7 +18,6 @@ package org.eclipse.jetty.io; -import java.io.IOException; import java.nio.channels.CancelledKeyException; import java.nio.channels.SelectionKey; import java.nio.channels.SocketChannel; @@ -75,7 +74,7 @@ public class SelectChannelEndPoint extends ChannelEndPoint implements SelectorMa */ private volatile int _interestOps; - public SelectChannelEndPoint(SocketChannel channel, ManagedSelector selector, SelectionKey key, ScheduledExecutorService scheduler, long idleTimeout) throws IOException + public SelectChannelEndPoint(SocketChannel channel, ManagedSelector selector, SelectionKey key, ScheduledExecutorService scheduler, long idleTimeout) { super(scheduler,channel); _selector = selector; diff --git a/jetty-io/src/main/java/org/eclipse/jetty/io/WriteFlusher.java b/jetty-io/src/main/java/org/eclipse/jetty/io/WriteFlusher.java index 57dd2f50c3f..b47d217c515 100644 --- a/jetty-io/src/main/java/org/eclipse/jetty/io/WriteFlusher.java +++ b/jetty-io/src/main/java/org/eclipse/jetty/io/WriteFlusher.java @@ -302,7 +302,7 @@ abstract public class WriteFlusher // Are we complete? for (ByteBuffer b : buffers) { - if (b.hasRemaining()) + if (BufferUtil.hasContent(b)) { PendingState pending=new PendingState<>(buffers, context, callback); if (updateState(__WRITING,pending)) @@ -362,7 +362,7 @@ abstract public class WriteFlusher // Are we complete? for (ByteBuffer b : buffers) { - if (b.hasRemaining()) + if (BufferUtil.hasContent(b)) { if (updateState(__COMPLETING,pending)) onIncompleteFlushed(); diff --git a/jetty-server/src/main/java/org/eclipse/jetty/server/HttpConnection.java b/jetty-server/src/main/java/org/eclipse/jetty/server/HttpConnection.java index ac9736fa274..11a5f258228 100644 --- a/jetty-server/src/main/java/org/eclipse/jetty/server/HttpConnection.java +++ b/jetty-server/src/main/java/org/eclipse/jetty/server/HttpConnection.java @@ -190,7 +190,7 @@ public class HttpConnection extends AbstractConnection implements Runnable, Http } else if (filled < 0) { - _parser.inputShutdown(); + _parser.shutdownInput(); // We were only filling if fully consumed, so if we have // read -1 then we have nothing to parse and thus nothing that // will generate a response. If we had a suspended request pending @@ -517,7 +517,7 @@ public class HttpConnection extends AbstractConnection implements Runnable, Http // If no more input if (getEndPoint().isInputShutdown()) { - _parser.inputShutdown(); + _parser.shutdownInput(); return; } @@ -536,7 +536,7 @@ public class HttpConnection extends AbstractConnection implements Runnable, Http LOG.debug("{} block filled {}",this,filled); if (filled<0) { - _parser.inputShutdown(); + _parser.shutdownInput(); return; } } diff --git a/jetty-util/src/main/java/org/eclipse/jetty/util/BufferUtil.java b/jetty-util/src/main/java/org/eclipse/jetty/util/BufferUtil.java index 09d36d5e78d..ca4bd0a00f7 100644 --- a/jetty-util/src/main/java/org/eclipse/jetty/util/BufferUtil.java +++ b/jetty-util/src/main/java/org/eclipse/jetty/util/BufferUtil.java @@ -34,16 +34,16 @@ import java.nio.charset.Charset; /* ------------------------------------------------------------------------------- */ /** * Buffer utility methods. - * + * * These utility methods facilitate the usage of NIO {@link ByteBuffer}'s in a more flexible way. - * The standard {@link ByteBuffer#flip()} assumes that once flipped to flush a buffer, - * that it will be completely emptied before being cleared ready to be filled again. + * The standard {@link ByteBuffer#flip()} assumes that once flipped to flush a buffer, + * that it will be completely emptied before being cleared ready to be filled again. * The {@link #flipToFill(ByteBuffer)} and {@link #flipToFlush(ByteBuffer, int)} methods provided here * do not assume that the buffer is empty and will preserve content when flipped. *

- * ByteBuffers can be considered in one of two modes: Flush mode where valid content is contained between - * position and limit which is consumed by advancing the position; and Fill mode where empty space is between - * the position and limit, which is filled by advancing the position. In fill mode, there may be valid data + * ByteBuffers can be considered in one of two modes: Flush mode where valid content is contained between + * position and limit which is consumed by advancing the position; and Fill mode where empty space is between + * the position and limit, which is filled by advancing the position. In fill mode, there may be valid data * in the buffer before the position and the start of this data is given by the return value of {@link #flipToFill(ByteBuffer)} *

* A typical pattern for using the buffers in this style is: @@ -70,7 +70,7 @@ public class BufferUtil /* ------------------------------------------------------------ */ /** Allocate ByteBuffer in flush mode. - * The position and limit will both be zero, indicating that the buffer is + * The position and limit will both be zero, indicating that the buffer is * empty and must be flipped before any data is put to it. * @param capacity * @return Buffer @@ -84,7 +84,7 @@ public class BufferUtil /* ------------------------------------------------------------ */ /** Allocate ByteBuffer in flush mode. - * The position and limit will both be zero, indicating that the buffer is + * The position and limit will both be zero, indicating that the buffer is * empty and must be flipped before any data is put to it. * @param capacity * @return Buffer @@ -95,7 +95,7 @@ public class BufferUtil buf.limit(0); return buf; } - + /* ------------------------------------------------------------ */ /** Clear the buffer to be empty in flush mode. @@ -113,27 +113,30 @@ public class BufferUtil /* ------------------------------------------------------------ */ /** Clear the buffer to be empty in fill mode. - * The position is set to 0 and the limit is set to the capacity. + * The position is set to 0 and the limit is set to the capacity. * @param buffer The buffer to clear. */ public static void clearToFill(ByteBuffer buffer) { - buffer.position(0); - buffer.limit(buffer.capacity()); + if (buffer!=null) + { + buffer.position(0); + buffer.limit(buffer.capacity()); + } } - + /* ------------------------------------------------------------ */ /** Flip the buffer to fill mode. - * The position is set to the first unused position in the buffer + * The position is set to the first unused position in the buffer * (the old limit) and the limit is set to the capacity. * If the buffer is empty, then this call is effectively {@link #clearToFill(ByteBuffer)}. * If there is no unused space to fill, a {@link ByteBuffer#compact()} is done to attempt * to create space. *

* This method is used as a replacement to {@link ByteBuffer#compact()}. - * + * * @param buffer The buffer to flip - * @return The position of the valid data before the flipped position. This value should be + * @return The position of the valid data before the flipped position. This value should be * passed to a subsequent call to {@link #flipToFlush(ByteBuffer, int)} */ public static int flipToFill(ByteBuffer buffer) @@ -155,7 +158,7 @@ public class BufferUtil buffer.limit(buffer.capacity()); return 0; } - + buffer.position(limit); buffer.limit(capacity); return position; @@ -177,12 +180,12 @@ public class BufferUtil buffer.limit(buffer.position()); buffer.position(position); } - - + + /* ------------------------------------------------------------ */ /** Convert a ByteBuffer to a byte array. * @param buffer The buffer to convert in flush mode. The buffer is not altered. - * @return An array of bytes duplicated from the buffer. + * @return An array of bytes duplicated from the buffer. */ public static byte[] toArray(ByteBuffer buffer) { @@ -206,7 +209,7 @@ public class BufferUtil { return buf==null || buf.remaining()==0; } - + /* ------------------------------------------------------------ */ /** Check for a non null and non empty buffer. * @param buf the buffer to check @@ -216,7 +219,7 @@ public class BufferUtil { return buf!=null && buf.remaining()>0; } - + /* ------------------------------------------------------------ */ /** Check for a non null and full buffer. * @param buf the buffer to check @@ -226,7 +229,7 @@ public class BufferUtil { return buf!=null && buf.limit()==buf.capacity(); } - + /* ------------------------------------------------------------ */ /** Get remaining from null checked buffer * @param buffer The buffer to get the remaining from, in flush mode. @@ -248,7 +251,7 @@ public class BufferUtil return 0; return buffer.capacity()-buffer.limit(); } - + /* ------------------------------------------------------------ */ /** Compact the buffer * @param buffer @@ -260,7 +263,7 @@ public class BufferUtil buffer.compact().flip(); return full && buffer.limit()0) { - if (remaining<=to.remaining()) + if (remaining<=to.remaining()) { to.put(from); put=remaining; @@ -301,7 +304,7 @@ public class BufferUtil return put; } - + /* ------------------------------------------------------------ */ /** * Put data from one buffer into another, avoiding over/under flows @@ -337,7 +340,7 @@ public class BufferUtil flipToFlush(to,pos); } } - + /* ------------------------------------------------------------ */ /** */ @@ -347,14 +350,14 @@ public class BufferUtil to.limit(limit+1); to.put(limit,b); } - + /* ------------------------------------------------------------ */ public static void readFrom(File file, ByteBuffer buffer) throws IOException { RandomAccessFile raf = new RandomAccessFile(file,"r"); FileChannel channel = raf.getChannel(); long needed=raf.length(); - + while (needed>0 && buffer.hasRemaining()) needed=needed-channel.read(buffer); } @@ -387,7 +390,7 @@ public class BufferUtil out.write(buffer.get(i)); } } - + /* ------------------------------------------------------------ */ /** Convert the buffer to an ISO-8859-1 String * @param buffer The buffer to convert in flush mode. The buffer is unchanged @@ -411,7 +414,7 @@ public class BufferUtil /* ------------------------------------------------------------ */ /** Convert the buffer to an ISO-8859-1 String * @param buffer The buffer to convert in flush mode. The buffer is unchanged - * @param charset The {@link Charset} to use to convert the bytes + * @param charset The {@link Charset} to use to convert the bytes * @return The buffer as a string. */ public static String toString(ByteBuffer buffer, Charset charset) @@ -431,7 +434,7 @@ public class BufferUtil /* ------------------------------------------------------------ */ /** Convert a partial buffer to an ISO-8859-1 String * @param buffer The buffer to convert in flush mode. The buffer is unchanged - * @param charset The {@link Charset} to use to convert the bytes + * @param charset The {@link Charset} to use to convert the bytes * @return The buffer as a string. */ public static String toString(ByteBuffer buffer, int position, int length, Charset charset) @@ -450,11 +453,11 @@ public class BufferUtil } return new String(array,buffer.arrayOffset()+position,length,charset); } - + /* ------------------------------------------------------------ */ /** * Convert buffer to an integer. Parses up to the first non-numeric character. If no number is found an IllegalArgumentException is thrown - * + * * @param buffer * A buffer containing an integer in flush mode. The position is not changed. * @return an int @@ -495,7 +498,7 @@ public class BufferUtil /** * Convert buffer to an long. Parses up to the first non-numeric character. If no number is found an IllegalArgumentException is thrown - * + * * @param buffer * A buffer containing an integer in flush mode. The position is not changed. * @return an int @@ -579,7 +582,7 @@ public class BufferUtil } } } - + /* ------------------------------------------------------------ */ public static void putDecInt(ByteBuffer buffer, int n) { @@ -684,10 +687,10 @@ public class BufferUtil { return ByteBuffer.wrap(s.getBytes(charset)); } - + /** * Create a new ByteBuffer using provided byte array. - * + * * @param array * the byte array to back buffer with. * @return ByteBuffer with provided byte array, in flush mode @@ -696,10 +699,10 @@ public class BufferUtil { return ByteBuffer.wrap(array); } - + /** * Create a new ByteBuffer using the provided byte array. - * + * * @param array * the byte array to use. * @param offset @@ -719,7 +722,7 @@ public class BufferUtil MappedByteBuffer buffer=raf.getChannel().map(MapMode.READ_ONLY,0,raf.length()); return buffer; } - + public static String toSummaryString(ByteBuffer buffer) { if (buffer==null) @@ -749,19 +752,19 @@ public class BufferUtil builder.append(']'); return builder.toString(); } - + public static String toDetailString(ByteBuffer buffer) { if (buffer==null) return "null"; - + StringBuilder buf = new StringBuilder(); buf.append(buffer.getClass().getSimpleName()); buf.append("@"); if (buffer.hasArray()) buf.append(Integer.toHexString(((Object)buffer.array()).hashCode())); else - buf.append("?"); + buf.append("?"); buf.append("[p="); buf.append(buffer.position()); buf.append(",l="); @@ -771,7 +774,7 @@ public class BufferUtil buf.append(",r="); buf.append(buffer.remaining()); buf.append("]={"); - + for (int i=0;i