--- a/src/java.base/share/classes/jdk/internal/misc/InnocuousThread.java Sun Nov 05 17:05:57 2017 +0000
+++ b/src/java.base/share/classes/jdk/internal/misc/InnocuousThread.java Sun Nov 05 17:32:13 2017 +0000
@@ -62,10 +62,16 @@
* set to the system class loader.
*/
public static Thread newThread(String name, Runnable target) {
- return new InnocuousThread(INNOCUOUSTHREADGROUP,
- target,
- name,
- ClassLoader.getSystemClassLoader());
+ return AccessController.doPrivileged(
+ new PrivilegedAction<Thread>() {
+ @Override
+ public Thread run() {
+ return new InnocuousThread(INNOCUOUSTHREADGROUP,
+ target,
+ name,
+ ClassLoader.getSystemClassLoader());
+ }
+ });
}
/**
@@ -80,8 +86,14 @@
* Returns a new InnocuousThread with null context class loader.
*/
public static Thread newSystemThread(String name, Runnable target) {
- return new InnocuousThread(INNOCUOUSTHREADGROUP,
- target, name, null);
+ return AccessController.doPrivileged(
+ new PrivilegedAction<Thread>() {
+ @Override
+ public Thread run() {
+ return new InnocuousThread(INNOCUOUSTHREADGROUP,
+ target, name, null);
+ }
+ });
}
private InnocuousThread(ThreadGroup group, Runnable target, String name, ClassLoader tccl) {
--- a/src/jdk.httpserver/share/classes/sun/net/httpserver/ChunkedInputStream.java Sun Nov 05 17:05:57 2017 +0000
+++ b/src/jdk.httpserver/share/classes/sun/net/httpserver/ChunkedInputStream.java Sun Nov 05 17:32:13 2017 +0000
@@ -135,6 +135,8 @@
needToReadHeader = true;
consumeCRLF();
}
+ if (n < 0 && !eof)
+ throw new IOException("connection closed before all data received");
return n;
}
--- a/src/jdk.httpserver/share/classes/sun/net/httpserver/FixedLengthInputStream.java Sun Nov 05 17:05:57 2017 +0000
+++ b/src/jdk.httpserver/share/classes/sun/net/httpserver/FixedLengthInputStream.java Sun Nov 05 17:32:13 2017 +0000
@@ -60,6 +60,8 @@
t.getServerImpl().requestCompleted (t.getConnection());
}
}
+ if (n < 0 && !eof)
+ throw new IOException("Connection closed before all bytes received");
return n;
}
--- a/src/jdk.httpserver/share/classes/sun/net/httpserver/ServerImpl.java Sun Nov 05 17:05:57 2017 +0000
+++ b/src/jdk.httpserver/share/classes/sun/net/httpserver/ServerImpl.java Sun Nov 05 17:32:13 2017 +0000
@@ -797,7 +797,8 @@
// fashion.
void requestCompleted (HttpConnection c) {
- assert c.getState() == State.REQUEST;
+ State s = c.getState();
+ assert s == State.REQUEST : "State is not REQUEST ("+s+")";
reqConnections.remove (c);
c.rspStartedTime = getTime();
rspConnections.add (c);
@@ -806,7 +807,8 @@
// called after response has been sent
void responseCompleted (HttpConnection c) {
- assert c.getState() == State.RESPONSE;
+ State s = c.getState();
+ assert s == State.RESPONSE : "State is not RESPONSE ("+s+")";
rspConnections.remove (c);
c.setState (State.IDLE);
}
--- a/src/jdk.incubator.httpclient/share/classes/jdk/incubator/http/AbstractAsyncSSLConnection.java Sun Nov 05 17:05:57 2017 +0000
+++ b/src/jdk.incubator.httpclient/share/classes/jdk/incubator/http/AbstractAsyncSSLConnection.java Sun Nov 05 17:32:13 2017 +0000
@@ -28,9 +28,20 @@
import java.io.IOException;
import java.net.InetSocketAddress;
import java.nio.ByteBuffer;
+import java.nio.channels.SocketChannel;
+import java.util.Arrays;
+import java.util.List;
import java.util.concurrent.CompletableFuture;
+import javax.net.ssl.SNIHostName;
+import javax.net.ssl.SSLContext;
import javax.net.ssl.SSLEngine;
+import javax.net.ssl.SSLEngineResult;
+import javax.net.ssl.SSLParameters;
+
+import jdk.incubator.http.internal.common.SSLTube;
+import jdk.incubator.http.internal.common.Log;
import jdk.incubator.http.internal.common.ExceptionallyCloseable;
+import jdk.incubator.http.internal.common.Utils;
/**
@@ -52,34 +63,135 @@
*
*/
abstract class AbstractAsyncSSLConnection extends HttpConnection
- implements AsyncConnection, ExceptionallyCloseable {
-
+ implements ExceptionallyCloseable
+{
+ protected final SSLEngine engine;
+ protected final String serverName;
+ protected final SSLParameters sslParameters;
- AbstractAsyncSSLConnection(InetSocketAddress addr, HttpClientImpl client) {
+ AbstractAsyncSSLConnection(InetSocketAddress addr,
+ HttpClientImpl client,
+ String serverName,
+ String[] alpn) {
super(addr, client);
+ this.serverName = serverName;
+ SSLContext context = client.theSSLContext();
+ sslParameters = createSSLParameters(client, context, serverName, alpn);
+ Log.logParams(sslParameters);
+ engine = createEngine(context, sslParameters);
+ }
+
+ abstract HttpConnection plainConnection();
+ abstract SSLTube getConnectionFlow();
+
+ final CompletableFuture<String> getALPN() {
+ assert connected();
+ return getConnectionFlow().getALPN();
}
- abstract SSLEngine getEngine();
- abstract AsyncSSLDelegate sslDelegate();
- abstract HttpConnection plainConnection();
- abstract HttpConnection downgrade();
+ final SSLEngine getEngine() { return engine; }
+
+ @Override
+ SSLParameters sslParameters() { return sslParameters; }
+
+ private static SSLParameters createSSLParameters(HttpClientImpl client,
+ SSLContext context,
+ String serverName,
+ String[] alpn) {
+ SSLParameters sslp = client.sslParameters();
+ SSLParameters sslParameters = Utils.copySSLParameters(sslp);
+ if (alpn != null) {
+ Log.logSSL("AbstractAsyncSSLConnection: Setting application protocols: {0}",
+ Arrays.toString(alpn));
+ sslParameters.setApplicationProtocols(alpn);
+ } else {
+ Log.logSSL("AbstractAsyncSSLConnection: no applications set!");
+ }
+ if (serverName != null) {
+ sslParameters.setServerNames(List.of(new SNIHostName(serverName)));
+ }
+ return sslParameters;
+ }
+
+ private static SSLEngine createEngine(SSLContext context,
+ SSLParameters sslParameters) {
+ SSLEngine engine = context.createSSLEngine();
+ engine.setUseClientMode(true);
+ engine.setSSLParameters(sslParameters);
+ return engine;
+ }
@Override
final boolean isSecure() {
return true;
}
- // Blocking read functions not used here
- @Override
- protected final ByteBuffer readImpl() throws IOException {
- throw new UnsupportedOperationException("Not supported.");
+ // Support for WebSocket/RawChannelImpl which unfortunately
+ // still depends on synchronous read/writes.
+ // It should be removed when RawChannelImpl moves to using asynchronous APIs.
+ static final class SSLConnectionChannel extends DetachedConnectionChannel {
+ final DetachedConnectionChannel delegate;
+ final SSLDelegate sslDelegate;
+ SSLConnectionChannel(DetachedConnectionChannel delegate, SSLDelegate sslDelegate) {
+ this.delegate = delegate;
+ this.sslDelegate = sslDelegate;
+ }
+
+ SocketChannel channel() {
+ return delegate.channel();
+ }
+
+ @Override
+ ByteBuffer read() throws IOException {
+ SSLDelegate.WrapperResult r = sslDelegate.recvData(ByteBuffer.allocate(8192));
+ // TODO: check for closure
+ int n = r.result.bytesProduced();
+ if (n > 0) {
+ return r.buf;
+ } else if (n == 0) {
+ return Utils.EMPTY_BYTEBUFFER;
+ } else {
+ return null;
+ }
+ }
+ @Override
+ long write(ByteBuffer[] buffers, int start, int number) throws IOException {
+ long l = SSLDelegate.countBytes(buffers, start, number);
+ SSLDelegate.WrapperResult r = sslDelegate.sendData(buffers, start, number);
+ if (r.result.getStatus() == SSLEngineResult.Status.CLOSED) {
+ if (l > 0) {
+ throw new IOException("SSLHttpConnection closed");
+ }
+ }
+ return l;
+ }
+ @Override
+ public void shutdownInput() throws IOException {
+ delegate.shutdownInput();
+ }
+ @Override
+ public void shutdownOutput() throws IOException {
+ delegate.shutdownOutput();
+ }
+ @Override
+ public void close() {
+ delegate.close();
+ }
}
- // whenReceivedResponse only used in HTTP/1.1 (Http1Exchange)
- // AbstractAsyncSSLConnection is only used with HTTP/2
+ // Support for WebSocket/RawChannelImpl which unfortunately
+ // still depends on synchronous read/writes.
+ // It should be removed when RawChannelImpl moves to using asynchronous APIs.
@Override
- final CompletableFuture<Void> whenReceivingResponse() {
- throw new UnsupportedOperationException("Not supported.");
+ DetachedConnectionChannel detachChannel() {
+ HttpClientImpl client = client();
+ assert client != null;
+ DetachedConnectionChannel detachedChannel = plainConnection().detachChannel();
+ SSLDelegate sslDelegate = new SSLDelegate(engine,
+ detachedChannel.channel(),
+ client,
+ serverName);
+ return new SSLConnectionChannel(detachedChannel, sslDelegate);
}
}
--- /dev/null Thu Jan 01 00:00:00 1970 +0000
+++ b/src/jdk.incubator.httpclient/share/classes/jdk/incubator/http/AbstractSubscription.java Sun Nov 05 17:32:13 2017 +0000
@@ -0,0 +1,45 @@
+/*
+ * Copyright (c) 2017, Oracle and/or its affiliates. All rights reserved.
+ * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER.
+ *
+ * This code is free software; you can redistribute it and/or modify it
+ * under the terms of the GNU General Public License version 2 only, as
+ * published by the Free Software Foundation. Oracle designates this
+ * particular file as subject to the "Classpath" exception as provided
+ * by Oracle in the LICENSE file that accompanied this code.
+ *
+ * This code is distributed in the hope that it will be useful, but WITHOUT
+ * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or
+ * FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License
+ * version 2 for more details (a copy is included in the LICENSE file that
+ * accompanied this code).
+ *
+ * You should have received a copy of the GNU General Public License version
+ * 2 along with this work; if not, write to the Free Software Foundation,
+ * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA.
+ *
+ * Please contact Oracle, 500 Oracle Parkway, Redwood Shores, CA 94065 USA
+ * or visit www.oracle.com if you need additional information or have any
+ * questions.
+ */
+
+package jdk.incubator.http;
+
+import java.util.concurrent.Flow;
+import jdk.incubator.http.internal.common.Demand;
+
+/**
+ * A {@link Flow.Subscription} wrapping a {@link Demand} instance.
+ *
+ */
+abstract class AbstractSubscription implements Flow.Subscription {
+
+ private final Demand demand = new Demand();
+
+ /**
+ * Returns the subscription's demand.
+ * @return the subscription's demand.
+ */
+ protected Demand demand() { return demand; }
+
+}
--- a/src/jdk.incubator.httpclient/share/classes/jdk/incubator/http/AsyncConnection.java Sun Nov 05 17:05:57 2017 +0000
+++ b/src/jdk.incubator.httpclient/share/classes/jdk/incubator/http/AsyncConnection.java Sun Nov 05 17:32:13 2017 +0000
@@ -28,14 +28,11 @@
import jdk.incubator.http.internal.common.ByteBufferReference;
import java.io.IOException;
-import java.nio.ByteBuffer;
-import java.util.function.Consumer;
-import java.util.function.Supplier;
/**
* Implemented by classes that offer an asynchronous interface.
*
- * PlainHttpConnection, AsyncSSLConnection AsyncSSLDelegate.
+ * PlainHttpConnection, AsyncSSLConnection.
*
* setAsyncCallbacks() is called to set the callback for reading
* and error notification. Reads all happen on the selector thread, which
@@ -51,31 +48,6 @@
interface AsyncConnection {
/**
- * Enables asynchronous sending and receiving mode. The given async
- * receiver will receive all incoming data. asyncInput() will be called
- * to trigger reads. asyncOutput() will be called to drive writes.
- *
- * The errorReceiver callback must be called when any fatal exception
- * occurs. Connection is assumed to be closed afterwards.
- */
- void setAsyncCallbacks(Consumer<ByteBufferReference> asyncReceiver,
- Consumer<Throwable> errorReceiver,
- Supplier<ByteBufferReference> readBufferSupplier);
-
-
-
- /**
- * Does whatever is required to start reading. Usually registers
- * an event with the selector thread.
- */
- void startReading();
-
- /**
- * Cancel asynchronous reading. Used to downgrade a HTTP/2 connection to HTTP/1
- */
- void stopAsyncReading();
-
- /**
* In async mode, this method puts buffers at the end of the send queue.
* When in async mode, calling this method should later be followed by
* subsequent flushAsync invocation.
@@ -85,11 +57,6 @@
void writeAsync(ByteBufferReference[] buffers) throws IOException;
/**
- * Re-enable asynchronous reads through the callback
- */
- void enableCallback();
-
- /**
* In async mode, this method may put buffers at the beginning of send queue,
* breaking frames sequence and allowing to write these buffers before other
* buffers in the queue.
--- a/src/jdk.incubator.httpclient/share/classes/jdk/incubator/http/AsyncEvent.java Sun Nov 05 17:05:57 2017 +0000
+++ b/src/jdk.incubator.httpclient/share/classes/jdk/incubator/http/AsyncEvent.java Sun Nov 05 17:32:13 2017 +0000
@@ -25,23 +25,24 @@
package jdk.incubator.http;
+import java.io.IOException;
import java.nio.channels.SelectableChannel;
/**
* Event handling interface from HttpClientImpl's selector.
*
- * If BLOCKING is set, then the channel will be put in blocking
- * mode prior to handle() being called. If false, then it remains non-blocking.
- *
* If REPEATING is set then the event is not cancelled after being posted.
*/
abstract class AsyncEvent {
- public static final int BLOCKING = 0x1; // non blocking if not set
public static final int REPEATING = 0x2; // one off event if not set
protected final int flags;
+ AsyncEvent() {
+ this(0);
+ }
+
AsyncEvent(int flags) {
this.flags = flags;
}
@@ -55,12 +56,13 @@
/** Called when event occurs */
public abstract void handle();
- /** Called when selector is shutting down. Abort all exchanges. */
- public abstract void abort();
-
- public boolean blocking() {
- return (flags & BLOCKING) != 0;
- }
+ /**
+ * Called when an error occurs during registration, or when the selector has
+ * been shut down. Aborts all exchanges.
+ *
+ * @param ioe the IOException, or null
+ */
+ public abstract void abort(IOException ioe);
public boolean repeating() {
return (flags & REPEATING) != 0;
--- a/src/jdk.incubator.httpclient/share/classes/jdk/incubator/http/AsyncSSLConnection.java Sun Nov 05 17:05:57 2017 +0000
+++ b/src/jdk.incubator.httpclient/share/classes/jdk/incubator/http/AsyncSSLConnection.java Sun Nov 05 17:32:13 2017 +0000
@@ -26,37 +26,30 @@
package jdk.incubator.http;
import java.io.IOException;
+import java.lang.System.Logger.Level;
import java.net.InetSocketAddress;
-import java.nio.ByteBuffer;
import java.nio.channels.SocketChannel;
import java.util.concurrent.CompletableFuture;
-import java.util.function.Consumer;
-import java.util.function.Supplier;
-import javax.net.ssl.SSLEngine;
+import jdk.incubator.http.internal.common.ByteBufferReference;
+import jdk.incubator.http.internal.common.SSLTube;
+import jdk.incubator.http.internal.common.Utils;
-import jdk.incubator.http.internal.common.ByteBufferReference;
-import jdk.incubator.http.internal.common.Utils;
/**
* Asynchronous version of SSLConnection.
*/
class AsyncSSLConnection extends AbstractAsyncSSLConnection {
- final AsyncSSLDelegate sslDelegate;
final PlainHttpConnection plainConnection;
- final String serverName;
+ final PlainHttpPublisher writePublisher;
+ private volatile SSLTube flow;
- AsyncSSLConnection(InetSocketAddress addr, HttpClientImpl client, String[] ap) {
- super(addr, client);
+ AsyncSSLConnection(InetSocketAddress addr,
+ HttpClientImpl client,
+ String[] alpn) {
+ super(addr, client, Utils.getServerName(addr), alpn);
plainConnection = new PlainHttpConnection(addr, client);
- serverName = Utils.getServerName(addr);
- sslDelegate = new AsyncSSLDelegate(plainConnection, client, ap, serverName);
- }
-
- @Override
- synchronized void configureMode(Mode mode) throws IOException {
- super.configureMode(mode);
- plainConnection.configureMode(mode);
+ writePublisher = new PlainHttpPublisher();
}
@Override
@@ -65,30 +58,26 @@
}
@Override
- AsyncSSLDelegate sslDelegate() {
- return sslDelegate;
- }
-
- @Override
- public void connect() throws IOException, InterruptedException {
- plainConnection.connect();
- configureMode(Mode.ASYNC);
- startReading();
- sslDelegate.connect();
- }
-
- @Override
public CompletableFuture<Void> connectAsync() {
- // not used currently
- throw new InternalError();
+ return plainConnection
+ .connectAsync()
+ .thenApply( unused -> {
+ // create the SSLTube wrapping the SocketTube, with the given engine
+ flow = new SSLTube(engine,
+ client().theExecutor(),
+ plainConnection.getConnectionFlow());
+ return null; } );
}
@Override
boolean connected() {
- return plainConnection.connected() && sslDelegate.connected();
+ return plainConnection.connected();
}
@Override
+ HttpPublisher publisher() { return writePublisher; }
+
+ @Override
boolean isProxied() {
return false;
}
@@ -99,97 +88,50 @@
}
@Override
- public void enableCallback() {
- sslDelegate.enableCallback();
- }
-
- @Override
ConnectionPool.CacheKey cacheKey() {
return ConnectionPool.cacheKey(address, null);
}
@Override
- long write(ByteBuffer[] buffers, int start, int number)
- throws IOException
- {
- ByteBuffer[] bufs = Utils.reduce(buffers, start, number);
- long n = Utils.remaining(bufs);
- sslDelegate.writeAsync(ByteBufferReference.toReferences(bufs));
- sslDelegate.flushAsync();
- return n;
- }
-
- @Override
- long write(ByteBuffer buffer) throws IOException {
- long n = buffer.remaining();
- sslDelegate.writeAsync(ByteBufferReference.toReferences(buffer));
- sslDelegate.flushAsync();
- return n;
+ public void writeAsync(ByteBufferReference[] buffers) throws IOException {
+ writePublisher.writeAsync(buffers);
}
@Override
public void writeAsyncUnordered(ByteBufferReference[] buffers) throws IOException {
- assert getMode() == Mode.ASYNC;
- sslDelegate.writeAsyncUnordered(buffers);
- }
-
- @Override
- public void writeAsync(ByteBufferReference[] buffers) throws IOException {
- assert getMode() == Mode.ASYNC;
- sslDelegate.writeAsync(buffers);
+ writePublisher.writeAsyncUnordered(buffers);
}
@Override
public void flushAsync() throws IOException {
- sslDelegate.flushAsync();
+ writePublisher.flushAsync();
}
@Override
public void closeExceptionally(Throwable cause) {
- Utils.close(cause, sslDelegate, plainConnection.channel());
+ debug.log(Level.DEBUG, () -> "closing: " + cause);
+ plainConnection.close();
}
@Override
public void close() {
- Utils.close(sslDelegate, plainConnection.channel());
+ plainConnection.close();
}
@Override
void shutdownInput() throws IOException {
+ debug.log(Level.DEBUG, "plainConnection.channel().shutdownInput()");
plainConnection.channel().shutdownInput();
}
@Override
void shutdownOutput() throws IOException {
+ debug.log(Level.DEBUG, "plainConnection.channel().shutdownOutput()");
plainConnection.channel().shutdownOutput();
}
- @Override
- SSLEngine getEngine() {
- return sslDelegate.getEngine();
- }
-
- @Override
- public void setAsyncCallbacks(Consumer<ByteBufferReference> asyncReceiver,
- Consumer<Throwable> errorReceiver,
- Supplier<ByteBufferReference> readBufferSupplier) {
- sslDelegate.setAsyncCallbacks(asyncReceiver, errorReceiver, readBufferSupplier);
- plainConnection.setAsyncCallbacks(sslDelegate::asyncReceive, errorReceiver, sslDelegate::getNetBuffer);
- }
-
- @Override
- public void startReading() {
- plainConnection.startReading();
- sslDelegate.startReading();
- }
-
- @Override
- public void stopAsyncReading() {
- plainConnection.stopAsyncReading();
- }
-
- @Override
- SSLConnection downgrade() {
- return new SSLConnection(this);
- }
+ @Override
+ SSLTube getConnectionFlow() {
+ return flow;
+ }
}
--- a/src/jdk.incubator.httpclient/share/classes/jdk/incubator/http/AsyncSSLDelegate.java Sun Nov 05 17:05:57 2017 +0000
+++ /dev/null Thu Jan 01 00:00:00 1970 +0000
@@ -1,692 +0,0 @@
-/*
- * Copyright (c) 2015, 2016, Oracle and/or its affiliates. All rights reserved.
- * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER.
- *
- * This code is free software; you can redistribute it and/or modify it
- * under the terms of the GNU General Public License version 2 only, as
- * published by the Free Software Foundation. Oracle designates this
- * particular file as subject to the "Classpath" exception as provided
- * by Oracle in the LICENSE file that accompanied this code.
- *
- * This code is distributed in the hope that it will be useful, but WITHOUT
- * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or
- * FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License
- * version 2 for more details (a copy is included in the LICENSE file that
- * accompanied this code).
- *
- * You should have received a copy of the GNU General Public License version
- * 2 along with this work; if not, write to the Free Software Foundation,
- * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA.
- *
- * Please contact Oracle, 500 Oracle Parkway, Redwood Shores, CA 94065 USA
- * or visit www.oracle.com if you need additional information or have any
- * questions.
- */
-
-package jdk.incubator.http;
-
-import java.io.IOException;
-import java.nio.ByteBuffer;
-import java.util.ArrayList;
-import java.util.Arrays;
-import java.util.List;
-import java.util.concurrent.Semaphore;
-import java.util.concurrent.CompletableFuture;
-import java.util.function.Consumer;
-import java.util.function.Supplier;
-
-import static javax.net.ssl.SSLEngineResult.Status.*;
-import javax.net.ssl.*;
-
-import jdk.incubator.http.internal.common.AsyncWriteQueue;
-import jdk.incubator.http.internal.common.ByteBufferPool;
-import jdk.incubator.http.internal.common.ByteBufferReference;
-import jdk.incubator.http.internal.common.Log;
-import jdk.incubator.http.internal.common.Queue;
-import jdk.incubator.http.internal.common.Utils;
-import static javax.net.ssl.SSLEngineResult.HandshakeStatus.*;
-import jdk.incubator.http.internal.common.ExceptionallyCloseable;
-
-/**
- * Asynchronous wrapper around SSLEngine. send and receive is fully non
- * blocking. When handshaking is required, a thread is created to perform
- * the handshake and application level sends do not take place during this time.
- *
- * Is implemented using queues and functions operating on the receiving end
- * of each queue.
- *
- * Application writes to:
- * ||
- * \/
- * appOutputQ
- * ||
- * \/
- * appOutputQ read by "upperWrite" method which does SSLEngine.wrap
- * and does async write to PlainHttpConnection
- *
- * Reading side is as follows
- * --------------------------
- *
- * "upperRead" method reads off channelInputQ and calls SSLEngine.unwrap and
- * when decrypted data is returned, it is passed to the user's Consumer<ByteBuffer>
- * /\
- * ||
- * channelInputQ
- * /\
- * ||
- * "asyncReceive" method puts buffers into channelInputQ. It is invoked from
- * OP_READ events from the selector.
- *
- * Whenever handshaking is required, the doHandshaking() method is called
- * which creates a thread to complete the handshake. It takes over the
- * channelInputQ from upperRead, and puts outgoing packets on channelOutputQ.
- * Selector events are delivered to asyncReceive and lowerWrite as normal.
- *
- * Errors
- *
- * Any exception thrown by the engine or channel, causes all Queues to be closed
- * the channel to be closed, and the error is reported to the user's
- * Consumer<Throwable>
- */
-class AsyncSSLDelegate implements ExceptionallyCloseable, AsyncConnection {
-
- // outgoing buffers put in this queue first and may remain here
- // while SSL handshaking happening.
- final AsyncWriteQueue appOutputQ = new AsyncWriteQueue(this::upperWrite);
-
- // Bytes read into this queue before being unwrapped. Backup on this
- // Q should only happen when the engine is stalled due to delegated tasks
- final Queue<ByteBufferReference> channelInputQ;
-
- // input occurs through the read() method which is expected to be called
- // when the selector signals some data is waiting to be read. All incoming
- // handshake data is handled in this method, which means some calls to
- // read() may return zero bytes of user data. This is not a sign of spinning,
- // just that the handshake mechanics are being executed.
-
- final SSLEngine engine;
- final SSLParameters sslParameters;
- final HttpConnection lowerOutput;
- final HttpClientImpl client;
- final String serverName;
- // should be volatile to provide proper synchronization(visibility) action
- volatile Consumer<ByteBufferReference> asyncReceiver;
- volatile Consumer<Throwable> errorHandler;
- volatile boolean connected = false;
-
- // Locks.
- final Object reader = new Object();
- // synchronizing handshake state
- final Semaphore handshaker = new Semaphore(1);
- final String[] alpn;
-
- // alpn[] may be null. upcall is callback which receives incoming decoded bytes off socket
-
- AsyncSSLDelegate(HttpConnection lowerOutput, HttpClientImpl client, String[] alpn, String sname)
- {
- SSLContext context = client.sslContext();
- this.serverName = sname;
- engine = context.createSSLEngine();
- engine.setUseClientMode(true);
- SSLParameters sslp = client.sslParameters()
- .orElseGet(context::getSupportedSSLParameters);
- sslParameters = Utils.copySSLParameters(sslp);
- if (alpn != null) {
- Log.logSSL("AsyncSSLDelegate: Setting application protocols: " + Arrays.toString(alpn));
- sslParameters.setApplicationProtocols(alpn);
- } else {
- Log.logSSL("AsyncSSLDelegate: no applications set!");
- }
- if (serverName != null) {
- SNIHostName sn = new SNIHostName(serverName);
- sslParameters.setServerNames(List.of(sn));
- }
- logParams(sslParameters);
- engine.setSSLParameters(sslParameters);
- this.lowerOutput = lowerOutput;
- this.client = client;
- this.channelInputQ = new Queue<>();
- this.channelInputQ.registerPutCallback(this::upperRead);
- this.alpn = alpn;
- }
-
- @Override
- public void writeAsync(ByteBufferReference[] src) throws IOException {
- appOutputQ.put(src);
- }
-
- @Override
- public void writeAsyncUnordered(ByteBufferReference[] buffers) throws IOException {
- appOutputQ.putFirst(buffers);
- }
-
- @Override
- public void flushAsync() throws IOException {
- if (appOutputQ.flush()) {
- lowerOutput.flushAsync();
- }
- }
-
- SSLEngine getEngine() {
- return engine;
- }
-
- @Override
- public void closeExceptionally(Throwable t) {
- Utils.close(t, appOutputQ, channelInputQ, lowerOutput);
- }
-
- @Override
- public void close() {
- Utils.close(appOutputQ, channelInputQ, lowerOutput);
- }
-
- // The code below can be uncommented to shake out
- // the implementation by inserting random delays and trigger
- // handshake in the SelectorManager thread (upperRead)
- // static final java.util.Random random =
- // new java.util.Random(System.currentTimeMillis());
-
- /**
- * Attempts to wrap buffers from appOutputQ and place them on the
- * channelOutputQ for writing. If handshaking is happening, then the
- * process stalls and last buffers taken off the appOutputQ are put back
- * into it until handshaking completes.
- *
- * This same method is called to try and resume output after a blocking
- * handshaking operation has completed.
- */
- private boolean upperWrite(ByteBufferReference[] refs, AsyncWriteQueue delayCallback) {
- // currently delayCallback is not used. Use it when it's needed to execute handshake in another thread.
- try {
- ByteBuffer[] buffers = ByteBufferReference.toBuffers(refs);
- int bytes = Utils.remaining(buffers);
- while (bytes > 0) {
- EngineResult r = wrapBuffers(buffers);
- int bytesProduced = r.bytesProduced();
- int bytesConsumed = r.bytesConsumed();
- bytes -= bytesConsumed;
- if (bytesProduced > 0) {
- lowerOutput.writeAsync(new ByteBufferReference[]{r.destBuffer});
- }
-
- // The code below can be uncommented to shake out
- // the implementation by inserting random delays and trigger
- // handshake in the SelectorManager thread (upperRead)
-
- // int sleep = random.nextInt(100);
- // if (sleep > 20) {
- // Thread.sleep(sleep);
- // }
-
- // handshaking is happening or is needed
- if (r.handshaking()) {
- Log.logTrace("Write: needs handshake");
- doHandshakeNow("Write");
- }
- }
- ByteBufferReference.clear(refs);
- } catch (Throwable t) {
- closeExceptionally(t);
- errorHandler.accept(t);
- }
- // We always return true: either all the data was sent, or
- // an exception happened and we have closed the queue.
- return true;
- }
-
- // Connecting at this level means the initial handshake has completed.
- // This means that the initial SSL parameters are available including
- // ALPN result.
- void connect() throws IOException, InterruptedException {
- doHandshakeNow("Init");
- connected = true;
- }
-
- boolean connected() {
- return connected;
- }
-
- private void startHandshake(String tag) {
- Runnable run = () -> {
- try {
- doHandshakeNow(tag);
- } catch (Throwable t) {
- Log.logTrace("{0}: handshake failed: {1}", tag, t);
- closeExceptionally(t);
- errorHandler.accept(t);
- }
- };
- client.executor().execute(run);
- }
-
- private void doHandshakeNow(String tag)
- throws IOException, InterruptedException
- {
- handshaker.acquire();
- try {
- channelInputQ.disableCallback();
- lowerOutput.flushAsync();
- Log.logTrace("{0}: Starting handshake...", tag);
- doHandshakeImpl();
- Log.logTrace("{0}: Handshake completed", tag);
- // don't unblock the channel here, as we aren't sure yet, whether ALPN
- // negotiation succeeded. Caller will call enableCallback() externally
- } finally {
- handshaker.release();
- }
- }
-
- public void enableCallback() {
- channelInputQ.enableCallback();
- }
-
- /**
- * Executes entire handshake in calling thread.
- * Returns after handshake is completed or error occurs
- */
- private void doHandshakeImpl() throws IOException {
- engine.beginHandshake();
- while (true) {
- SSLEngineResult.HandshakeStatus status = engine.getHandshakeStatus();
- switch(status) {
- case NEED_TASK: {
- List<Runnable> tasks = obtainTasks();
- for (Runnable task : tasks) {
- task.run();
- }
- } break;
- case NEED_WRAP:
- handshakeWrapAndSend();
- break;
- case NEED_UNWRAP: case NEED_UNWRAP_AGAIN:
- handshakeReceiveAndUnWrap();
- break;
- case FINISHED:
- return;
- case NOT_HANDSHAKING:
- return;
- default:
- throw new InternalError("Unexpected Handshake Status: "
- + status);
- }
- }
- }
-
- // acknowledge a received CLOSE request from peer
- void doClosure() throws IOException {
- //while (!wrapAndSend(emptyArray))
- //;
- }
-
- List<Runnable> obtainTasks() {
- List<Runnable> l = new ArrayList<>();
- Runnable r;
- while ((r = engine.getDelegatedTask()) != null) {
- l.add(r);
- }
- return l;
- }
-
- @Override
- public void setAsyncCallbacks(Consumer<ByteBufferReference> asyncReceiver,
- Consumer<Throwable> errorReceiver,
- Supplier<ByteBufferReference> readBufferSupplier) {
- this.asyncReceiver = asyncReceiver;
- this.errorHandler = errorReceiver;
- // readBufferSupplier is not used,
- // because of AsyncSSLDelegate has its own appBufferPool
- }
-
- @Override
- public void startReading() {
- // maybe this class does not need to implement AsyncConnection
- }
-
- @Override
- public void stopAsyncReading() {
- // maybe this class does not need to implement AsyncConnection
- }
-
-
- static class EngineResult {
- final SSLEngineResult result;
- final ByteBufferReference destBuffer;
-
-
- // normal result
- EngineResult(SSLEngineResult result) {
- this(result, null);
- }
-
- EngineResult(SSLEngineResult result, ByteBufferReference destBuffer) {
- this.result = result;
- this.destBuffer = destBuffer;
- }
-
- boolean handshaking() {
- SSLEngineResult.HandshakeStatus s = result.getHandshakeStatus();
- return s != FINISHED && s != NOT_HANDSHAKING;
- }
-
- int bytesConsumed() {
- return result.bytesConsumed();
- }
-
- int bytesProduced() {
- return result.bytesProduced();
- }
-
- SSLEngineResult.HandshakeStatus handshakeStatus() {
- return result.getHandshakeStatus();
- }
-
- SSLEngineResult.Status status() {
- return result.getStatus();
- }
- }
-
- EngineResult handshakeWrapAndSend() throws IOException {
- EngineResult r = wrapBuffer(Utils.EMPTY_BYTEBUFFER);
- if (r.bytesProduced() > 0) {
- lowerOutput.writeAsync(new ByteBufferReference[]{r.destBuffer});
- lowerOutput.flushAsync();
- }
- return r;
- }
-
- // called during handshaking. It blocks until a complete packet
- // is available, unwraps it and returns.
- void handshakeReceiveAndUnWrap() throws IOException {
- ByteBufferReference ref = channelInputQ.take();
- while (true) {
- // block waiting for input
- EngineResult r = unwrapBuffer(ref.get());
- SSLEngineResult.Status status = r.status();
- if (status == BUFFER_UNDERFLOW) {
- // wait for another buffer to arrive
- ByteBufferReference ref1 = channelInputQ.take();
- ref = combine (ref, ref1);
- continue;
- }
- // OK
- // theoretically possible we could receive some user data
- if (r.bytesProduced() > 0) {
- asyncReceiver.accept(r.destBuffer);
- } else {
- r.destBuffer.clear();
- }
- // it is also possible that a delegated task could be needed
- // even though they are handled in the calling function
- if (r.handshakeStatus() == NEED_TASK) {
- obtainTasks().stream().forEach((task) -> task.run());
- }
-
- if (!ref.get().hasRemaining()) {
- ref.clear();
- return;
- }
- }
- }
-
- EngineResult wrapBuffer(ByteBuffer src) throws SSLException {
- ByteBuffer[] bufs = new ByteBuffer[1];
- bufs[0] = src;
- return wrapBuffers(bufs);
- }
-
- private final ByteBufferPool netBufferPool = new ByteBufferPool();
- private final ByteBufferPool appBufferPool = new ByteBufferPool();
-
- /**
- * provides buffer of sslEngine@getPacketBufferSize().
- * used for encrypted buffers after wrap or before unwrap.
- * @return ByteBufferReference
- */
- public ByteBufferReference getNetBuffer() {
- return netBufferPool.get(engine.getSession().getPacketBufferSize());
- }
-
- /**
- * provides buffer of sslEngine@getApplicationBufferSize().
- * @return ByteBufferReference
- */
- private ByteBufferReference getAppBuffer() {
- return appBufferPool.get(engine.getSession().getApplicationBufferSize());
- }
-
- EngineResult wrapBuffers(ByteBuffer[] src) throws SSLException {
- ByteBufferReference dst = getNetBuffer();
- while (true) {
- SSLEngineResult sslResult = engine.wrap(src, dst.get());
- switch (sslResult.getStatus()) {
- case BUFFER_OVERFLOW:
- // Shouldn't happen. We allocated buffer with packet size
- // get it again if net buffer size was changed
- dst = getNetBuffer();
- break;
- case CLOSED:
- case OK:
- dst.get().flip();
- return new EngineResult(sslResult, dst);
- case BUFFER_UNDERFLOW:
- // Shouldn't happen. Doesn't returns when wrap()
- // underflow handled externally
- return new EngineResult(sslResult);
- default:
- assert false;
- }
- }
- }
-
- EngineResult unwrapBuffer(ByteBuffer srcbuf) throws IOException {
- ByteBufferReference dst = getAppBuffer();
- while (true) {
- SSLEngineResult sslResult = engine.unwrap(srcbuf, dst.get());
- switch (sslResult.getStatus()) {
- case BUFFER_OVERFLOW:
- // may happen only if app size buffer was changed.
- // get it again if app buffer size changed
- dst = getAppBuffer();
- break;
- case CLOSED:
- doClosure();
- throw new IOException("Engine closed");
- case BUFFER_UNDERFLOW:
- dst.clear();
- return new EngineResult(sslResult);
- case OK:
- dst.get().flip();
- return new EngineResult(sslResult, dst);
- }
- }
- }
-
- /**
- * Asynchronous read input. Call this when selector fires.
- * Unwrap done in upperRead because it also happens in
- * doHandshake() when handshake taking place
- */
- public void asyncReceive(ByteBufferReference buffer) {
- try {
- channelInputQ.put(buffer);
- } catch (Throwable t) {
- closeExceptionally(t);
- errorHandler.accept(t);
- }
- }
-
- private ByteBufferReference pollInput() throws IOException {
- return channelInputQ.poll();
- }
-
- private ByteBufferReference pollInput(ByteBufferReference next) throws IOException {
- return next == null ? channelInputQ.poll() : next;
- }
-
- public void upperRead() {
- ByteBufferReference src;
- ByteBufferReference next = null;
- synchronized (reader) {
- try {
- src = pollInput();
- if (src == null) {
- return;
- }
- while (true) {
- EngineResult r = unwrapBuffer(src.get());
- switch (r.result.getStatus()) {
- case BUFFER_UNDERFLOW:
- // Buffer too small. Need to combine with next buf
- next = pollInput(next);
- if (next == null) {
- // no data available.
- // push buffer back until more data available
- channelInputQ.pushback(src);
- return;
- } else {
- src = shift(src, next);
- if (!next.get().hasRemaining()) {
- next.clear();
- next = null;
- }
- }
- break;
- case OK:
- // check for any handshaking work
- if (r.handshaking()) {
- // handshaking is happening or is needed
- // so we put the buffer back on Q to process again
- // later.
- Log.logTrace("Read: needs handshake");
- channelInputQ.pushback(src);
- startHandshake("Read");
- return;
- }
- asyncReceiver.accept(r.destBuffer);
- }
- if (src.get().hasRemaining()) {
- continue;
- }
- src.clear();
- src = pollInput(next);
- next = null;
- if (src == null) {
- return;
- }
- }
- } catch (Throwable t) {
- closeExceptionally(t);
- errorHandler.accept(t);
- }
- }
- }
-
- ByteBufferReference shift(ByteBufferReference ref1, ByteBufferReference ref2) {
- ByteBuffer buf1 = ref1.get();
- if (buf1.capacity() < engine.getSession().getPacketBufferSize()) {
- ByteBufferReference newRef = getNetBuffer();
- ByteBuffer newBuf = newRef.get();
- newBuf.put(buf1);
- buf1 = newBuf;
- ref1.clear();
- ref1 = newRef;
- } else {
- buf1.compact();
- }
- ByteBuffer buf2 = ref2.get();
- Utils.copy(buf2, buf1, Math.min(buf1.remaining(), buf2.remaining()));
- buf1.flip();
- return ref1;
- }
-
-
- ByteBufferReference combine(ByteBufferReference ref1, ByteBufferReference ref2) {
- ByteBuffer buf1 = ref1.get();
- ByteBuffer buf2 = ref2.get();
- int avail1 = buf1.capacity() - buf1.remaining();
- if (buf2.remaining() < avail1) {
- buf1.compact();
- buf1.put(buf2);
- buf1.flip();
- ref2.clear();
- return ref1;
- }
- int newsize = buf1.remaining() + buf2.remaining();
- ByteBuffer newbuf = ByteBuffer.allocate(newsize); // getting rid of buffer pools
- newbuf.put(buf1);
- newbuf.put(buf2);
- newbuf.flip();
- ref1.clear();
- ref2.clear();
- return ByteBufferReference.of(newbuf);
- }
-
- SSLParameters getSSLParameters() {
- return sslParameters;
- }
-
- static void logParams(SSLParameters p) {
- if (!Log.ssl()) {
- return;
- }
-
- if (p == null) {
- Log.logSSL("SSLParameters: Null params");
- return;
- }
-
- final StringBuilder sb = new StringBuilder("SSLParameters:");
- final List<Object> params = new ArrayList<>();
- if (p.getCipherSuites() != null) {
- for (String cipher : p.getCipherSuites()) {
- sb.append("\n cipher: {")
- .append(params.size()).append("}");
- params.add(cipher);
- }
- }
-
- // SSLParameters.getApplicationProtocols() can't return null
- // JDK 8 EXCL START
- for (String approto : p.getApplicationProtocols()) {
- sb.append("\n application protocol: {")
- .append(params.size()).append("}");
- params.add(approto);
- }
- // JDK 8 EXCL END
-
- if (p.getProtocols() != null) {
- for (String protocol : p.getProtocols()) {
- sb.append("\n protocol: {")
- .append(params.size()).append("}");
- params.add(protocol);
- }
- }
-
- if (p.getServerNames() != null) {
- for (SNIServerName sname : p.getServerNames()) {
- sb.append("\n server name: {")
- .append(params.size()).append("}");
- params.add(sname.toString());
- }
- }
- sb.append('\n');
-
- Log.logSSL(sb.toString(), params.toArray());
- }
-
- String getSessionInfo() {
- StringBuilder sb = new StringBuilder();
- String application = engine.getApplicationProtocol();
- SSLSession sess = engine.getSession();
- String cipher = sess.getCipherSuite();
- String protocol = sess.getProtocol();
- sb.append("Handshake complete alpn: ")
- .append(application)
- .append(", Cipher: ")
- .append(cipher)
- .append(", Protocol: ")
- .append(protocol);
- return sb.toString();
- }
-}
--- a/src/jdk.incubator.httpclient/share/classes/jdk/incubator/http/AsyncSSLTunnelConnection.java Sun Nov 05 17:05:57 2017 +0000
+++ b/src/jdk.incubator.httpclient/share/classes/jdk/incubator/http/AsyncSSLTunnelConnection.java Sun Nov 05 17:32:13 2017 +0000
@@ -26,15 +26,12 @@
package jdk.incubator.http;
import java.io.IOException;
+import java.lang.System.Logger.Level;
import java.net.InetSocketAddress;
-import java.nio.ByteBuffer;
import java.nio.channels.SocketChannel;
import java.util.concurrent.CompletableFuture;
-import java.util.function.Consumer;
-import java.util.function.Supplier;
-import javax.net.ssl.SSLEngine;
-import javax.net.ssl.SSLParameters;
import jdk.incubator.http.internal.common.ByteBufferReference;
+import jdk.incubator.http.internal.common.SSLTube;
import jdk.incubator.http.internal.common.Utils;
/**
@@ -43,48 +40,43 @@
class AsyncSSLTunnelConnection extends AbstractAsyncSSLConnection {
final PlainTunnelingConnection plainConnection;
- final AsyncSSLDelegate sslDelegate;
- final String serverName;
+ final PlainHttpPublisher writePublisher;
+ volatile SSLTube flow;
- @Override
- public void connect() throws IOException, InterruptedException {
- plainConnection.connect();
- configureMode(Mode.ASYNC);
- startReading();
- sslDelegate.connect();
- }
-
- @Override
- boolean connected() {
- return plainConnection.connected() && sslDelegate.connected();
+ AsyncSSLTunnelConnection(InetSocketAddress addr,
+ HttpClientImpl client,
+ String[] alpn,
+ InetSocketAddress proxy)
+ {
+ super(addr, client, Utils.getServerName(addr), alpn);
+ this.plainConnection = new PlainTunnelingConnection(addr, proxy, client);
+ this.writePublisher = new PlainHttpPublisher();
}
@Override
public CompletableFuture<Void> connectAsync() {
- throw new InternalError();
- }
-
- AsyncSSLTunnelConnection(InetSocketAddress addr,
- HttpClientImpl client,
- String[] alpn,
- InetSocketAddress proxy)
- {
- super(addr, client);
- this.serverName = Utils.getServerName(addr);
- this.plainConnection = new PlainTunnelingConnection(addr, proxy, client);
- this.sslDelegate = new AsyncSSLDelegate(plainConnection, client, alpn, serverName);
+ debug.log(Level.DEBUG, "Connecting plain tunnel connection");
+ // This will connect the PlainHttpConnection flow, so that
+ // its HttpSubscriber and HttpPublisher are subscribed to the
+ // SocketTube
+ return plainConnection
+ .connectAsync()
+ .thenApply( unused -> {
+ debug.log(Level.DEBUG, "creating SSLTube");
+ // create the SSLTube wrapping the SocketTube, with the given engine
+ flow = new SSLTube(engine,
+ client().theExecutor(),
+ plainConnection.getConnectionFlow());
+ return null;} );
}
@Override
- synchronized void configureMode(Mode mode) throws IOException {
- super.configureMode(mode);
- plainConnection.configureMode(mode);
+ boolean connected() {
+ return plainConnection.connected(); // && sslDelegate.connected();
}
@Override
- SSLParameters sslParameters() {
- return sslDelegate.getSSLParameters();
- }
+ HttpPublisher publisher() { return writePublisher; }
@Override
public String toString() {
@@ -97,52 +89,28 @@
}
@Override
- AsyncSSLDelegate sslDelegate() {
- return sslDelegate;
- }
-
- @Override
ConnectionPool.CacheKey cacheKey() {
return ConnectionPool.cacheKey(address, plainConnection.proxyAddr);
}
@Override
- long write(ByteBuffer[] buffers, int start, int number) throws IOException {
- //debugPrint("Send", buffers, start, number);
- ByteBuffer[] bufs = Utils.reduce(buffers, start, number);
- long n = Utils.remaining(bufs);
- sslDelegate.writeAsync(ByteBufferReference.toReferences(bufs));
- sslDelegate.flushAsync();
- return n;
- }
-
- @Override
- long write(ByteBuffer buffer) throws IOException {
- //debugPrint("Send", buffer);
- long n = buffer.remaining();
- sslDelegate.writeAsync(ByteBufferReference.toReferences(buffer));
- sslDelegate.flushAsync();
- return n;
- }
-
- @Override
public void writeAsync(ByteBufferReference[] buffers) throws IOException {
- sslDelegate.writeAsync(buffers);
+ writePublisher.writeAsync(buffers);
}
@Override
public void writeAsyncUnordered(ByteBufferReference[] buffers) throws IOException {
- sslDelegate.writeAsyncUnordered(buffers);
+ writePublisher.writeAsyncUnordered(buffers);
}
@Override
public void flushAsync() throws IOException {
- sslDelegate.flushAsync();
+ writePublisher.flushAsync();
}
@Override
public void close() {
- Utils.close(sslDelegate, plainConnection.channel());
+ plainConnection.close();
}
@Override
@@ -166,41 +134,13 @@
}
@Override
- public void setAsyncCallbacks(Consumer<ByteBufferReference> asyncReceiver,
- Consumer<Throwable> errorReceiver,
- Supplier<ByteBufferReference> readBufferSupplier) {
- sslDelegate.setAsyncCallbacks(asyncReceiver, errorReceiver, readBufferSupplier);
- plainConnection.setAsyncCallbacks(sslDelegate::asyncReceive, errorReceiver, sslDelegate::getNetBuffer);
- }
-
- @Override
- public void startReading() {
- plainConnection.startReading();
- sslDelegate.startReading();
- }
-
- @Override
- public void stopAsyncReading() {
- plainConnection.stopAsyncReading();
+ public void closeExceptionally(Throwable cause) {
+ debug.log(Level.DEBUG, "Closing connection: ", cause);
+ plainConnection.close();
}
@Override
- public void enableCallback() {
- sslDelegate.enableCallback();
- }
-
- @Override
- public void closeExceptionally(Throwable cause) throws IOException {
- Utils.close(cause, sslDelegate, plainConnection.channel());
- }
-
- @Override
- SSLEngine getEngine() {
- return sslDelegate.getEngine();
- }
-
- @Override
- SSLTunnelConnection downgrade() {
- return new SSLTunnelConnection(this);
- }
+ SSLTube getConnectionFlow() {
+ return flow;
+ }
}
--- /dev/null Thu Jan 01 00:00:00 1970 +0000
+++ b/src/jdk.incubator.httpclient/share/classes/jdk/incubator/http/AsyncTriggerEvent.java Sun Nov 05 17:32:13 2017 +0000
@@ -0,0 +1,59 @@
+/*
+ * Copyright (c) 2017, Oracle and/or its affiliates. All rights reserved.
+ * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER.
+ *
+ * This code is free software; you can redistribute it and/or modify it
+ * under the terms of the GNU General Public License version 2 only, as
+ * published by the Free Software Foundation. Oracle designates this
+ * particular file as subject to the "Classpath" exception as provided
+ * by Oracle in the LICENSE file that accompanied this code.
+ *
+ * This code is distributed in the hope that it will be useful, but WITHOUT
+ * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or
+ * FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License
+ * version 2 for more details (a copy is included in the LICENSE file that
+ * accompanied this code).
+ *
+ * You should have received a copy of the GNU General Public License version
+ * 2 along with this work; if not, write to the Free Software Foundation,
+ * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA.
+ *
+ * Please contact Oracle, 500 Oracle Parkway, Redwood Shores, CA 94065 USA
+ * or visit www.oracle.com if you need additional information or have any
+ * questions.
+ */
+
+package jdk.incubator.http;
+
+import java.io.IOException;
+import java.nio.channels.SelectableChannel;
+import java.util.Objects;
+import java.util.function.Consumer;
+
+/**
+ * An asynchronous event which is triggered only once from the selector manager
+ * thread as soon as event registration are handled.
+ */
+final class AsyncTriggerEvent extends AsyncEvent{
+
+ private final Runnable trigger;
+ private final Consumer<? super IOException> errorHandler;
+ AsyncTriggerEvent(Consumer<? super IOException> errorHandler,
+ Runnable trigger) {
+ super(0);
+ this.trigger = Objects.requireNonNull(trigger);
+ this.errorHandler = Objects.requireNonNull(errorHandler);
+ }
+ /** Returns null */
+ @Override
+ public SelectableChannel channel() { return null; }
+ /** Returns 0 */
+ @Override
+ public int interestOps() { return 0; }
+ @Override
+ public void handle() { trigger.run(); }
+ @Override
+ public void abort(IOException ioe) { errorHandler.accept(ioe); }
+ @Override
+ public boolean repeating() { return false; }
+}
--- /dev/null Thu Jan 01 00:00:00 1970 +0000
+++ b/src/jdk.incubator.httpclient/share/classes/jdk/incubator/http/BufferingSubscriber.java Sun Nov 05 17:32:13 2017 +0000
@@ -0,0 +1,294 @@
+/*
+ * Copyright (c) 2017, Oracle and/or its affiliates. All rights reserved.
+ * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER.
+ *
+ * This code is free software; you can redistribute it and/or modify it
+ * under the terms of the GNU General Public License version 2 only, as
+ * published by the Free Software Foundation. Oracle designates this
+ * particular file as subject to the "Classpath" exception as provided
+ * by Oracle in the LICENSE file that accompanied this code.
+ *
+ * This code is distributed in the hope that it will be useful, but WITHOUT
+ * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or
+ * FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License
+ * version 2 for more details (a copy is included in the LICENSE file that
+ * accompanied this code).
+ *
+ * You should have received a copy of the GNU General Public License version
+ * 2 along with this work; if not, write to the Free Software Foundation,
+ * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA.
+ *
+ * Please contact Oracle, 500 Oracle Parkway, Redwood Shores, CA 94065 USA
+ * or visit www.oracle.com if you need additional information or have any
+ * questions.
+ */
+
+package jdk.incubator.http;
+
+import java.nio.ByteBuffer;
+import java.util.ArrayList;
+import java.util.Collections;
+import java.util.List;
+import java.util.ListIterator;
+import java.util.Objects;
+import java.util.concurrent.CompletionStage;
+import java.util.concurrent.Flow;
+import java.util.concurrent.atomic.AtomicBoolean;
+import jdk.incubator.http.internal.common.Demand;
+import jdk.incubator.http.internal.common.SequentialScheduler;
+import jdk.incubator.http.internal.common.Utils;
+
+/**
+ * A buffering BodySubscriber. When subscribed, accumulates ( buffers ) a given
+ * amount ( in bytes ) of a publisher's data before pushing it to a downstream
+ * subscriber.
+ */
+class BufferingSubscriber<T> implements HttpResponse.BodySubscriber<T>
+{
+ /** The downstream consumer of the data. */
+ private final HttpResponse.BodySubscriber<T> downstreamSubscriber;
+ /** The amount of data to be accumulate before pushing downstream. */
+ private final int bufferSize;
+
+ /** The subscription, created lazily. */
+ private volatile Flow.Subscription subscription;
+ /** The downstream subscription, created lazily. */
+ private volatile DownstreamSubscription downstreamSubscription;
+
+ /** Must be held when accessing the internal buffers. */
+ private final Object buffersLock = new Object();
+ /** The internal buffers holding the buffered data. */
+ private ArrayList<ByteBuffer> internalBuffers;
+ /** The actual accumulated remaining bytes in internalBuffers. */
+ private int accumulatedBytes;
+
+ /** State of the buffering subscriber:
+ * 1) [UNSUBSCRIBED] when initially created
+ * 2) [ACTIVE] when subscribed and can receive data
+ * 3) [ERROR | CANCELLED | COMPLETE] (terminal state)
+ */
+ static final int UNSUBSCRIBED = 0x01;
+ static final int ACTIVE = 0x02;
+ static final int ERROR = 0x04;
+ static final int CANCELLED = 0x08;
+ static final int COMPLETE = 0x10;
+
+ private volatile int state;
+
+ BufferingSubscriber(HttpResponse.BodySubscriber<T> downstreamSubscriber,
+ int bufferSize) {
+ this.downstreamSubscriber = downstreamSubscriber;
+ this.bufferSize = bufferSize;
+ synchronized (buffersLock) {
+ internalBuffers = new ArrayList<>();
+ }
+ state = UNSUBSCRIBED;
+ }
+
+ /** Returns the number of bytes remaining in the given buffers. */
+ private static final int remaining(List<ByteBuffer> buffers) {
+ return buffers.stream().mapToInt(ByteBuffer::remaining).sum();
+ }
+
+ /**
+ * Tells whether, or not, there is at least a sufficient number of bytes
+ * accumulated in the internal buffers. If the subscriber is COMPLETE, and
+ * has some buffered data, then there is always enough ( to pass downstream ).
+ */
+ private final boolean hasEnoughAccumulatedBytes() {
+ assert Thread.holdsLock(buffersLock);
+ return accumulatedBytes >= bufferSize
+ || (state == COMPLETE && accumulatedBytes > 0);
+ }
+
+ /**
+ * Returns a new, unmodifiable, List<ByteBuffer> containing exactly the
+ * amount of data as required before pushing downstream. The amount of data
+ * may be less than required ( bufferSize ), in the case where the subscriber
+ * is COMPLETE.
+ */
+ private List<ByteBuffer> fromInternalBuffers() {
+ assert Thread.holdsLock(buffersLock);
+ int leftToFill = bufferSize;
+ int state = this.state;
+ assert (state == ACTIVE || state == CANCELLED)
+ ? accumulatedBytes >= leftToFill : true;
+ List<ByteBuffer> dsts = new ArrayList<>();
+
+ ListIterator<ByteBuffer> itr = internalBuffers.listIterator();
+ while (itr.hasNext()) {
+ ByteBuffer b = itr.next();
+ if (b.remaining() <= leftToFill) {
+ itr.remove();
+ if (b.position() != 0)
+ b = b.slice(); // ensure position = 0 when propagated
+ dsts.add(b);
+ leftToFill -= b.remaining();
+ accumulatedBytes -= b.remaining();
+ if (leftToFill == 0)
+ break;
+ } else {
+ int prevLimit = b.limit();
+ b.limit(b.position() + leftToFill);
+ ByteBuffer slice = b.slice();
+ dsts.add(slice);
+ b.limit(prevLimit);
+ b.position(b.position() + leftToFill);
+ accumulatedBytes -= leftToFill;
+ leftToFill = 0;
+ break;
+ }
+ }
+ assert (state == ACTIVE || state == CANCELLED)
+ ? leftToFill == 0 : state == COMPLETE;
+ assert (state == ACTIVE || state == CANCELLED)
+ ? remaining(dsts) == bufferSize : state == COMPLETE;
+ assert accumulatedBytes >= 0;
+ assert dsts.stream().noneMatch(b -> b.position() != 0);
+ return Collections.unmodifiableList(dsts);
+ }
+
+ /** Subscription that is passed to the downstream subscriber. */
+ private class DownstreamSubscription implements Flow.Subscription {
+ private final AtomicBoolean cancelled = new AtomicBoolean(); // false
+ private final Demand demand = new Demand();
+
+ @Override
+ public void request(long n) {
+ if (n <= 0L) {
+ onError(new IllegalArgumentException(
+ "non-positive subscription request"));
+ return;
+ }
+ if (cancelled.get())
+ return;
+
+ demand.increase(n);
+
+ pushDemanded();
+ }
+
+ private final SequentialScheduler pushDemandedScheduler =
+ new SequentialScheduler(new PushDemandedTask());
+
+ void pushDemanded() {
+ if (cancelled.get())
+ return;
+ pushDemandedScheduler.runOrSchedule();
+ }
+
+ class PushDemandedTask extends SequentialScheduler.CompleteRestartableTask {
+ @Override
+ public void run() {
+ try {
+ while (true) {
+ List<ByteBuffer> item;
+ synchronized (buffersLock) {
+ if (cancelled.get())
+ return;
+ if (!hasEnoughAccumulatedBytes())
+ break;
+ if (!demand.tryDecrement())
+ break;
+ item = fromInternalBuffers();
+ }
+ assert item != null;
+
+ downstreamSubscriber.onNext(item);
+ }
+ if (cancelled.get())
+ return;
+
+ // complete only if all data consumed
+ boolean complete;
+ synchronized (buffersLock) {
+ complete = state == COMPLETE && internalBuffers.isEmpty();
+ }
+ if (complete) {
+ downstreamSubscriber.onComplete();
+ return;
+ }
+ } catch (Throwable t) {
+ cancel(); // cancel if there is any find of error
+ throw t;
+ }
+
+ boolean requestMore = false;
+ synchronized (buffersLock) {
+ if (!hasEnoughAccumulatedBytes() && !demand.isFulfilled()) {
+ // request more upstream data
+ requestMore = true;
+ }
+ }
+ if (requestMore)
+ subscription.request(1);
+ }
+ }
+
+ @Override
+ public void cancel() {
+ if (cancelled.compareAndExchange(false, true))
+ return; // already cancelled
+
+ state = CANCELLED; // set CANCELLED state of upstream subscriber
+ subscription.cancel(); // cancel upstream subscription
+ pushDemandedScheduler.stop(); // stop the demand scheduler
+ }
+ }
+
+ @Override
+ public void onSubscribe(Flow.Subscription subscription) {
+ Objects.requireNonNull(subscription);
+ if (this.subscription != null) {
+ subscription.cancel();
+ return;
+ }
+
+ int s = this.state;
+ assert s == UNSUBSCRIBED;
+ state = ACTIVE;
+ this.subscription = subscription;
+ downstreamSubscription = new DownstreamSubscription();
+ downstreamSubscriber.onSubscribe(downstreamSubscription);
+ }
+
+ @Override
+ public void onNext(List<ByteBuffer> item) {
+ Objects.requireNonNull(item);
+
+ int s = state;
+ if (s == CANCELLED)
+ return;
+
+ if (s != ACTIVE)
+ throw new InternalError("onNext on inactive subscriber");
+
+ synchronized (buffersLock) {
+ accumulatedBytes += Utils.accumulateBuffers(internalBuffers, item);
+ }
+
+ downstreamSubscription.pushDemanded();
+ }
+
+ @Override
+ public void onError(Throwable throwable) {
+ Objects.requireNonNull(throwable);
+ int s = state;
+ assert s == ACTIVE : "Expected ACTIVE, got:" + s;
+ state = ERROR;
+ downstreamSubscriber.onError(throwable);
+ }
+
+ @Override
+ public void onComplete() {
+ int s = state;
+ assert s == ACTIVE : "Expected ACTIVE, got:" + s;
+ state = COMPLETE;
+ downstreamSubscription.pushDemanded();
+ }
+
+ @Override
+ public CompletionStage<T> getBody() {
+ return downstreamSubscriber.getBody();
+ }
+}
--- a/src/jdk.incubator.httpclient/share/classes/jdk/incubator/http/ConnectionPool.java Sun Nov 05 17:05:57 2017 +0000
+++ b/src/jdk.incubator.httpclient/share/classes/jdk/incubator/http/ConnectionPool.java Sun Nov 05 17:32:13 2017 +0000
@@ -25,14 +25,24 @@
package jdk.incubator.http;
-import java.lang.ref.WeakReference;
+import java.io.IOException;
+import java.lang.System.Logger.Level;
import java.net.InetSocketAddress;
+import java.nio.ByteBuffer;
+import java.time.Instant;
+import java.time.temporal.ChronoUnit;
+import java.util.ArrayList;
+import java.util.Collections;
import java.util.HashMap;
+import java.util.Iterator;
import java.util.LinkedList;
+import java.util.List;
import java.util.ListIterator;
import java.util.Objects;
-import java.util.concurrent.atomic.AtomicLong;
-import java.util.concurrent.atomic.AtomicReference;
+import java.util.Optional;
+import java.util.concurrent.Flow;
+import java.util.stream.Collectors;
+import jdk.incubator.http.internal.common.FlowTube;
import jdk.incubator.http.internal.common.Utils;
/**
@@ -40,34 +50,18 @@
*/
final class ConnectionPool {
- // These counters are used to distribute ids for debugging
- // The ACTIVE_CLEANER_COUNTER will tell how many CacheCleaner
- // are active at a given time. It will increase when a new
- // CacheCleaner is started and decrease when it exits.
- static final AtomicLong ACTIVE_CLEANER_COUNTER = new AtomicLong();
- // The POOL_IDS_COUNTER increases each time a new ConnectionPool
- // is created. It may wrap and become negative but will never be
- // decremented.
- static final AtomicLong POOL_IDS_COUNTER = new AtomicLong();
- // The cleanerCounter is used to name cleaner threads within a
- // a connection pool, and increments monotically.
- // It may wrap and become negative but will never be
- // decremented.
- final AtomicLong cleanerCounter = new AtomicLong();
-
static final long KEEP_ALIVE = Utils.getIntegerNetProperty(
"jdk.httpclient.keepalive.timeout", 1200); // seconds
+ static final boolean DEBUG = Utils.DEBUG; // Revisit: temporary dev flag.
+ final System.Logger debug = Utils.getDebugLogger(this::dbgString, DEBUG);
// Pools of idle connections
- final HashMap<CacheKey,LinkedList<HttpConnection>> plainPool;
- final HashMap<CacheKey,LinkedList<HttpConnection>> sslPool;
- // A monotically increasing id for this connection pool.
- // It may be negative (that's OK)
- // Mostly used for debugging purposes when looking at thread dumps.
- // Global scope.
- final long poolID = POOL_IDS_COUNTER.incrementAndGet();
- final AtomicReference<CacheCleaner> cleanerRef;
+ private final HashMap<CacheKey,LinkedList<HttpConnection>> plainPool;
+ private final HashMap<CacheKey,LinkedList<HttpConnection>> sslPool;
+ private final ExpiryList expiryList;
+ private final String dbgTag; // used for debug
+ boolean stopped;
/**
* Entries in connection pool are keyed by destination address and/or
@@ -110,28 +104,30 @@
}
}
- static class ExpiryEntry {
- final HttpConnection connection;
- final long expiry; // absolute time in seconds of expiry time
- ExpiryEntry(HttpConnection connection, long expiry) {
- this.connection = connection;
- this.expiry = expiry;
- }
+ ConnectionPool() {
+ this("ConnectionPool(?)");
}
- final LinkedList<ExpiryEntry> expiryList;
+ ConnectionPool(long clientId) {
+ this("ConnectionPool("+clientId+")");
+ }
/**
* There should be one of these per HttpClient.
*/
- ConnectionPool() {
+ private ConnectionPool(String tag) {
+ dbgTag = tag;
plainPool = new HashMap<>();
sslPool = new HashMap<>();
- expiryList = new LinkedList<>();
- cleanerRef = new AtomicReference<>();
+ expiryList = new ExpiryList();
+ }
+
+ final String dbgString() {
+ return dbgTag;
}
void start() {
+ assert !stopped : "Already stopped";
}
static CacheKey cacheKey(InetSocketAddress destination,
@@ -143,6 +139,7 @@
synchronized HttpConnection getConnection(boolean secure,
InetSocketAddress addr,
InetSocketAddress proxy) {
+ if (stopped) return null;
CacheKey key = new CacheKey(addr, proxy);
HttpConnection c = secure ? findConnection(key, sslPool)
: findConnection(key, plainPool);
@@ -153,16 +150,49 @@
/**
* Returns the connection to the pool.
*/
- synchronized void returnToPool(HttpConnection conn) {
- if (conn instanceof PlainHttpConnection) {
- putConnection(conn, plainPool);
- } else {
- putConnection(conn, sslPool);
+ void returnToPool(HttpConnection conn) {
+ returnToPool(conn, Instant.now(), KEEP_ALIVE);
+ }
+
+ // Called also by whitebox tests
+ void returnToPool(HttpConnection conn, Instant now, long keepAlive) {
+
+ // Don't call registerCleanupTrigger while holding a lock,
+ // but register it before the connection is added to the pool,
+ // since we don't want to trigger the cleanup if the connection
+ // is not in the pool.
+ CleanupTrigger cleanup = registerCleanupTrigger(conn);
+
+ // it's possible that cleanup may have been called.
+ synchronized(this) {
+ if (cleanup.isDone()) {
+ return;
+ } else if (stopped) {
+ conn.close();
+ return;
+ }
+ if (conn instanceof PlainHttpConnection) {
+ putConnection(conn, plainPool);
+ } else {
+ assert conn.isSecure();
+ putConnection(conn, sslPool);
+ }
+ expiryList.add(conn, now, keepAlive);
}
- addToExpiryList(conn);
//System.out.println("Return to pool: " + conn);
}
+ private CleanupTrigger registerCleanupTrigger(HttpConnection conn) {
+ // Connect the connection flow to a pub/sub pair that will take the
+ // connection out of the pool and close it if anything happens
+ // while the connection is sitting in the pool.
+ CleanupTrigger cleanup = new CleanupTrigger(conn);
+ FlowTube flow = conn.getConnectionFlow();
+ debug.log(Level.DEBUG, "registering %s", cleanup);
+ flow.connectFlows(cleanup, cleanup);
+ return cleanup;
+ }
+
private HttpConnection
findConnection(CacheKey key,
HashMap<CacheKey,LinkedList<HttpConnection>> pool) {
@@ -171,20 +201,24 @@
return null;
} else {
HttpConnection c = l.removeFirst();
- removeFromExpiryList(c);
+ expiryList.remove(c);
return c;
}
}
/* called from cache cleaner only */
- private void
+ private boolean
removeFromPool(HttpConnection c,
HashMap<CacheKey,LinkedList<HttpConnection>> pool) {
//System.out.println("cacheCleaner removing: " + c);
- LinkedList<HttpConnection> l = pool.get(c.cacheKey());
- assert l != null;
- boolean wasPresent = l.remove(c);
- assert wasPresent;
+ assert Thread.holdsLock(this);
+ CacheKey k = c.cacheKey();
+ List<HttpConnection> l = pool.get(k);
+ if (l == null || l.isEmpty()) {
+ pool.remove(k);
+ return false;
+ }
+ return l.remove(c);
}
private void
@@ -199,135 +233,262 @@
l.add(c);
}
- static String makeCleanerName(long poolId, long cleanerId) {
- return "HTTP-Cache-cleaner-" + poolId + "-" + cleanerId;
+ /**
+ * Purge expired connection and return the number of milliseconds
+ * in which the next connection is scheduled to expire.
+ * If no connections are scheduled to be purged return 0.
+ * @return the delay in milliseconds in which the next connection will
+ * expire.
+ */
+ long purgeExpiredConnectionsAndReturnNextDeadline() {
+ if (!expiryList.purgeMaybeRequired()) return 0;
+ return purgeExpiredConnectionsAndReturnNextDeadline(Instant.now());
}
- // only runs while entries exist in cache
- final static class CacheCleaner extends Thread {
+ // Used for whitebox testing
+ long purgeExpiredConnectionsAndReturnNextDeadline(Instant now) {
+ long nextPurge = 0;
+
+ // We may be in the process of adding new elements
+ // to the expiry list - but those elements will not
+ // have outlast their keep alive timer yet since we're
+ // just adding them.
+ if (!expiryList.purgeMaybeRequired()) return nextPurge;
- volatile boolean stopping;
- // A monotically increasing id. May wrap and become negative (that's OK)
- // Mostly used for debugging purposes when looking at thread dumps.
- // Scoped per connection pool.
- final long cleanerID;
- // A reference to the owning ConnectionPool.
- // This reference's referent may become null if the HttpClientImpl
- // that owns this pool is GC'ed.
- final WeakReference<ConnectionPool> ownerRef;
-
- CacheCleaner(ConnectionPool owner) {
- this(owner, owner.cleanerCounter.incrementAndGet());
+ List<HttpConnection> closelist;
+ synchronized (this) {
+ closelist = expiryList.purgeUntil(now);
+ for (HttpConnection c : closelist) {
+ if (c instanceof PlainHttpConnection) {
+ boolean wasPresent = removeFromPool(c, plainPool);
+ assert wasPresent;
+ } else {
+ boolean wasPresent = removeFromPool(c, sslPool);
+ assert wasPresent;
+ }
+ }
+ nextPurge = now.until(
+ expiryList.nextExpiryDeadline().orElse(now),
+ ChronoUnit.MILLIS);
}
-
- CacheCleaner(ConnectionPool owner, long cleanerID) {
- super(null, null, makeCleanerName(owner.poolID, cleanerID), 0, false);
- this.cleanerID = cleanerID;
- this.ownerRef = new WeakReference<>(owner);
- setDaemon(true);
- }
+ closelist.forEach(this::close);
+ return nextPurge;
+ }
- synchronized boolean stopping() {
- return stopping || ownerRef.get() == null;
- }
-
- synchronized void stopCleaner() {
- stopping = true;
- }
+ private void close(HttpConnection c) {
+ try {
+ c.close();
+ } catch (Throwable e) {} // ignore
+ }
- @Override
- public void run() {
- ACTIVE_CLEANER_COUNTER.incrementAndGet();
- try {
- while (!stopping()) {
- try {
- Thread.sleep(3000);
- } catch (InterruptedException e) {}
- ConnectionPool owner = ownerRef.get();
- if (owner == null) return;
- owner.cleanCache(this);
- owner = null;
- }
- } finally {
- ACTIVE_CLEANER_COUNTER.decrementAndGet();
+ void stop() {
+ List<HttpConnection> closelist = Collections.emptyList();
+ try {
+ synchronized (this) {
+ stopped = true;
+ closelist = expiryList.stream()
+ .map(e -> e.connection)
+ .collect(Collectors.toList());
+ expiryList.clear();
+ plainPool.clear();
+ sslPool.clear();
}
+ } finally {
+ closelist.forEach(this::close);
+ }
+ }
+
+ static final class ExpiryEntry {
+ final HttpConnection connection;
+ final Instant expiry; // absolute time in seconds of expiry time
+ ExpiryEntry(HttpConnection connection, Instant expiry) {
+ this.connection = connection;
+ this.expiry = expiry;
}
}
- synchronized void removeFromExpiryList(HttpConnection c) {
- if (c == null) {
- return;
+ /**
+ * Manages a LinkedList of sorted ExpiryEntry. The entry with the closer
+ * deadline is at the tail of the list, and the entry with the farther
+ * deadline is at the head. In the most common situation, new elements
+ * will need to be added at the head (or close to it), and expired elements
+ * will need to be purged from the tail.
+ */
+ private static final class ExpiryList {
+ private final LinkedList<ExpiryEntry> list = new LinkedList<>();
+ private volatile boolean mayContainEntries;
+
+ // A loosely accurate boolean whose value is computed
+ // at the end of each operation performed on ExpiryList;
+ // Does not require synchronizing on the ConnectionPool.
+ boolean purgeMaybeRequired() {
+ return mayContainEntries;
+ }
+
+ // Returns the next expiry deadline
+ // should only be called while holding a synchronization
+ // lock on the ConnectionPool
+ Optional<Instant> nextExpiryDeadline() {
+ if (list.isEmpty()) return Optional.empty();
+ else return Optional.of(list.getLast().expiry);
+ }
+
+ // should only be called while holding a synchronization
+ // lock on the ConnectionPool
+ void add(HttpConnection conn) {
+ add(conn, Instant.now(), KEEP_ALIVE);
}
- ListIterator<ExpiryEntry> li = expiryList.listIterator();
- while (li.hasNext()) {
- ExpiryEntry e = li.next();
- if (e.connection.equals(c)) {
- li.remove();
- return;
+
+ // Used by whitebox test.
+ void add(HttpConnection conn, Instant now, long keepAlive) {
+ Instant then = now.truncatedTo(ChronoUnit.SECONDS)
+ .plus(keepAlive, ChronoUnit.SECONDS);
+
+ // Elements with the farther deadline are at the head of
+ // the list. It's more likely that the new element will
+ // have the farthest deadline, and will need to be inserted
+ // at the head of the list, so we're using an ascending
+ // list iterator to find the right insertion point.
+ ListIterator<ExpiryEntry> li = list.listIterator();
+ while (li.hasNext()) {
+ ExpiryEntry entry = li.next();
+
+ if (then.isAfter(entry.expiry)) {
+ li.previous();
+ // insert here
+ li.add(new ExpiryEntry(conn, then));
+ mayContainEntries = true;
+ return;
+ }
+ }
+ // last (or first) element of list (the last element is
+ // the first when the list is empty)
+ list.add(new ExpiryEntry(conn, then));
+ mayContainEntries = true;
+ }
+
+ // should only be called while holding a synchronization
+ // lock on the ConnectionPool
+ void remove(HttpConnection c) {
+ if (c == null || list.isEmpty()) return;
+ ListIterator<ExpiryEntry> li = list.listIterator();
+ while (li.hasNext()) {
+ ExpiryEntry e = li.next();
+ if (e.connection.equals(c)) {
+ li.remove();
+ mayContainEntries = !list.isEmpty();
+ return;
+ }
}
}
- CacheCleaner cleaner = this.cleanerRef.get();
- if (expiryList.isEmpty() && cleaner != null) {
- this.cleanerRef.compareAndSet(cleaner, null);
- cleaner.stopCleaner();
- cleaner.interrupt();
- }
- }
+
+ // should only be called while holding a synchronization
+ // lock on the ConnectionPool.
+ // Purge all elements whose deadline is before now (now included).
+ List<HttpConnection> purgeUntil(Instant now) {
+ if (list.isEmpty()) return Collections.emptyList();
- private void cleanCache(CacheCleaner cleaner) {
- long now = System.currentTimeMillis() / 1000;
- LinkedList<HttpConnection> closelist = new LinkedList<>();
+ List<HttpConnection> closelist = new ArrayList<>();
- synchronized (this) {
- ListIterator<ExpiryEntry> li = expiryList.listIterator();
+ // elements with the closest deadlines are at the tail
+ // of the queue, so we're going to use a descending iterator
+ // to remove them, and stop when we find the first element
+ // that has not expired yet.
+ Iterator<ExpiryEntry> li = list.descendingIterator();
while (li.hasNext()) {
ExpiryEntry entry = li.next();
- if (entry.expiry <= now) {
+ // use !isAfter instead of isBefore in order to
+ // remove the entry if its expiry == now
+ if (!entry.expiry.isAfter(now)) {
li.remove();
HttpConnection c = entry.connection;
closelist.add(c);
- if (c instanceof PlainHttpConnection) {
- removeFromPool(c, plainPool);
- } else {
- removeFromPool(c, sslPool);
- }
- }
+ } else break; // the list is sorted
}
- if (expiryList.isEmpty() && cleaner != null) {
- this.cleanerRef.compareAndSet(cleaner, null);
- cleaner.stopCleaner();
- }
+ mayContainEntries = !list.isEmpty();
+ return closelist;
}
- for (HttpConnection c : closelist) {
- //System.out.println ("KAC: closing " + c);
- c.close();
+
+ // should only be called while holding a synchronization
+ // lock on the ConnectionPool
+ java.util.stream.Stream<ExpiryEntry> stream() {
+ return list.stream();
+ }
+
+ // should only be called while holding a synchronization
+ // lock on the ConnectionPool
+ void clear() {
+ list.clear();
+ mayContainEntries = false;
}
}
- private synchronized void addToExpiryList(HttpConnection conn) {
- long now = System.currentTimeMillis() / 1000;
- long then = now + KEEP_ALIVE;
- if (expiryList.isEmpty()) {
- CacheCleaner cleaner = new CacheCleaner(this);
- if (this.cleanerRef.compareAndSet(null, cleaner)) {
- cleaner.start();
+ void cleanup(HttpConnection c, Throwable error) {
+ debug.log(Level.DEBUG,
+ "%s : ConnectionPool.cleanup(%s)",
+ String.valueOf(c.getConnectionFlow()),
+ error);
+ synchronized(this) {
+ if (c instanceof PlainHttpConnection) {
+ removeFromPool(c, plainPool);
+ } else {
+ assert c.isSecure();
+ removeFromPool(c, sslPool);
}
- expiryList.add(new ExpiryEntry(conn, then));
- return;
+ expiryList.remove(c);
+ }
+ c.close();
+ }
+
+ /**
+ * An object that subscribes to the flow while the connection is in
+ * the pool. Anything that comes in will cause the connection to be closed
+ * and removed from the pool.
+ */
+ private final class CleanupTrigger implements
+ FlowTube.TubeSubscriber, FlowTube.TubePublisher,
+ Flow.Subscription {
+
+ private final HttpConnection connection;
+ private volatile boolean done;
+
+ public CleanupTrigger(HttpConnection connection) {
+ this.connection = connection;
}
- ListIterator<ExpiryEntry> li = expiryList.listIterator();
- while (li.hasNext()) {
- ExpiryEntry entry = li.next();
+ public boolean isDone() { return done;}
+
+ private void triggerCleanup(Throwable error) {
+ done = true;
+ cleanup(connection, error);
+ }
+
+ @Override public void request(long n) {}
+ @Override public void cancel() {}
- if (then > entry.expiry) {
- li.previous();
- // insert here
- li.add(new ExpiryEntry(conn, then));
- return;
- }
+ @Override
+ public void onSubscribe(Flow.Subscription subscription) {
+ subscription.request(1);
+ }
+ @Override
+ public void onError(Throwable error) { triggerCleanup(error); }
+ @Override
+ public void onComplete() { triggerCleanup(null); }
+ @Override
+ public void onNext(List<ByteBuffer> item) {
+ triggerCleanup(new IOException("Data received while in pool"));
}
- // first element of list
- expiryList.add(new ExpiryEntry(conn, then));
+
+ @Override
+ public void subscribe(Flow.Subscriber<? super List<ByteBuffer>> subscriber) {
+ subscriber.onSubscribe(this);
+ }
+
+ @Override
+ public String toString() {
+ return "CleanupTrigger(" + connection.getConnectionFlow() + ")";
+ }
+
}
+
}
--- a/src/jdk.incubator.httpclient/share/classes/jdk/incubator/http/DefaultPublisher.java Sun Nov 05 17:05:57 2017 +0000
+++ /dev/null Thu Jan 01 00:00:00 1970 +0000
@@ -1,143 +0,0 @@
-/*
- * Copyright (c) 2016, Oracle and/or its affiliates. All rights reserved.
- * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER.
- *
- * This code is free software; you can redistribute it and/or modify it
- * under the terms of the GNU General Public License version 2 only, as
- * published by the Free Software Foundation. Oracle designates this
- * particular file as subject to the "Classpath" exception as provided
- * by Oracle in the LICENSE file that accompanied this code.
- *
- * This code is distributed in the hope that it will be useful, but WITHOUT
- * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or
- * FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License
- * version 2 for more details (a copy is included in the LICENSE file that
- * accompanied this code).
- *
- * You should have received a copy of the GNU General Public License version
- * 2 along with this work; if not, write to the Free Software Foundation,
- * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA.
- *
- * Please contact Oracle, 500 Oracle Parkway, Redwood Shores, CA 94065 USA
- * or visit www.oracle.com if you need additional information or have any
- * questions.
- */
-
-package jdk.incubator.http;
-
-import java.util.Objects;
-import java.util.Optional;
-import java.util.concurrent.Executor;
-import java.util.concurrent.Flow;
-import java.util.concurrent.RejectedExecutionException;
-import java.util.concurrent.atomic.AtomicBoolean;
-import java.util.concurrent.atomic.AtomicLong;
-import java.util.concurrent.locks.Condition;
-import java.util.concurrent.locks.Lock;
-import java.util.concurrent.locks.ReentrantLock;
-import java.util.function.Supplier;
-
-class DefaultPublisher<T> implements Flow.Publisher<T> {
-
- private final Supplier<Optional<T>> supplier;
- // this executor will be wrapped in another executor
- // which may override it and just run in the calling thread
- // if it knows the user call is blocking
- private final Executor executor;
-
- /**
- * Supplier returns non empty Optionals until final
- */
- DefaultPublisher(Supplier<Optional<T>> supplier, Executor executor) {
- this.supplier = supplier;
- this.executor = executor;
- }
-
- @Override
- public void subscribe(Flow.Subscriber<? super T> subscriber) {
- try {
- subscriber.onSubscribe(new Subscription(subscriber));
- } catch (RejectedExecutionException e) {
- subscriber.onError(new IllegalStateException(e));
- }
- }
-
- private class Subscription implements Flow.Subscription {
-
- private final Flow.Subscriber<? super T> subscriber;
- private final AtomicBoolean done = new AtomicBoolean();
-
- private final AtomicLong demand = new AtomicLong();
-
- private final Lock consumerLock = new ReentrantLock();
- private final Condition consumerAlarm = consumerLock.newCondition();
-
- Subscription(Flow.Subscriber<? super T> subscriber) {
- this.subscriber = subscriber;
-
- executor.execute(() -> {
- try {
- while (!done.get()) {
- consumerLock.lock();
- try {
- while (!done.get() && demand.get() == 0) {
- consumerAlarm.await();
- }
- } finally {
- consumerLock.unlock();
- }
-
- long nbItemsDemanded = demand.getAndSet(0);
- for (long i = 0; i < nbItemsDemanded && !done.get(); i++) {
- try {
- Optional<T> item = Objects.requireNonNull(supplier.get());
- if (item.isPresent()) {
- subscriber.onNext(item.get());
- } else {
- if (done.compareAndSet(false, true)) {
- subscriber.onComplete();
- }
- }
- } catch (RuntimeException e) {
- if (done.compareAndSet(false, true)) {
- subscriber.onError(e);
- }
- }
- }
- }
- } catch (InterruptedException e) {
- Thread.currentThread().interrupt();
- if (done.compareAndSet(false, true)) {
- subscriber.onError(e);
- }
- }
- });
- }
-
- @Override
- public void request(long n) {
- if (!done.get() && n > 0) {
- demand.updateAndGet(d -> (d + n > 0) ? d + n : Long.MAX_VALUE);
- wakeConsumer();
- } else if (done.compareAndSet(false, true)) {
- subscriber.onError(new IllegalArgumentException("request(" + n + ")"));
- }
- }
-
- @Override
- public void cancel() {
- done.set(true);
- wakeConsumer();
- }
-
- private void wakeConsumer() {
- consumerLock.lock();
- try {
- consumerAlarm.signal();
- } finally {
- consumerLock.unlock();
- }
- }
-
- }
-}
--- a/src/jdk.incubator.httpclient/share/classes/jdk/incubator/http/Exchange.java Sun Nov 05 17:05:57 2017 +0000
+++ b/src/jdk.incubator.httpclient/share/classes/jdk/incubator/http/Exchange.java Sun Nov 05 17:32:13 2017 +0000
@@ -26,7 +26,7 @@
package jdk.incubator.http;
import java.io.IOException;
-import java.io.UncheckedIOException;
+import java.lang.System.Logger.Level;
import java.net.InetSocketAddress;
import java.net.ProxySelector;
import java.net.SocketPermission;
@@ -40,7 +40,10 @@
import java.security.PrivilegedExceptionAction;
import java.util.LinkedList;
import java.util.List;
+import java.util.Map;
+import java.util.Set;
import java.util.concurrent.CompletableFuture;
+import java.util.concurrent.CompletionException;
import java.util.concurrent.Executor;
import jdk.incubator.http.internal.common.MinimalFuture;
import jdk.incubator.http.internal.common.Utils;
@@ -61,19 +64,22 @@
*/
final class Exchange<T> {
+ static final boolean DEBUG = Utils.DEBUG; // Revisit: temporary dev flag.
+ final System.Logger debug = Utils.getDebugLogger(this::dbgString, DEBUG);
+
final HttpRequestImpl request;
final HttpClientImpl client;
volatile ExchangeImpl<T> exchImpl;
// used to record possible cancellation raised before the exchImpl
// has been established.
private volatile IOException failed;
- final List<SocketPermission> permissions = new LinkedList<>();
final AccessControlContext acc;
final MultiExchange<?,T> multi;
final Executor parentExecutor;
- final HttpRequest.BodyProcessor requestProcessor;
+ final HttpRequest.BodyPublisher requestPublisher;
boolean upgrading; // to HTTP/2
final PushGroup<?,T> pushGroup;
+ final String dbgTag;
Exchange(HttpRequestImpl request, MultiExchange<?,T> multi) {
this.request = request;
@@ -82,8 +88,9 @@
this.multi = multi;
this.acc = multi.acc;
this.parentExecutor = multi.executor;
- this.requestProcessor = request.requestProcessor;
+ this.requestPublisher = request.requestPublisher;
this.pushGroup = multi.pushGroup;
+ this.dbgTag = "Exchange";
}
/* If different AccessControlContext to be used */
@@ -97,8 +104,9 @@
this.client = multi.client();
this.multi = multi;
this.parentExecutor = multi.executor;
- this.requestProcessor = request.requestProcessor;
+ this.requestPublisher = request.requestPublisher;
this.pushGroup = multi.pushGroup;
+ this.dbgTag = "Exchange";
}
PushGroup<?,T> getPushGroup() {
@@ -117,18 +125,25 @@
return client;
}
- public Response response() throws IOException, InterruptedException {
- return responseImpl(null);
- }
-
- public T readBody(HttpResponse.BodyHandler<T> responseHandler) throws IOException {
- // The connection will not be returned to the pool in the case of WebSocket
- return exchImpl.readBody(responseHandler, !request.isWebSocket());
- }
public CompletableFuture<T> readBodyAsync(HttpResponse.BodyHandler<T> handler) {
// The connection will not be returned to the pool in the case of WebSocket
- return exchImpl.readBodyAsync(handler, !request.isWebSocket(), parentExecutor);
+ return exchImpl.readBodyAsync(handler, !request.isWebSocket(), parentExecutor)
+ .whenComplete((r,t) -> exchImpl.completed());
+ }
+
+ /**
+ * Called when a new exchange is created to replace this exchange.
+ * At this point it is guaranteed that readBody/readBodyAsync will
+ * not be called.
+ */
+ public void released() {
+ ExchangeImpl<?> impl = exchImpl;
+ if (impl != null) impl.released();
+ // Don't set exchImpl to null here. We need to keep
+ // it alive until it's replaced by a Stream in wrapForUpgrade.
+ // Setting it to null here might get it GC'ed too early, because
+ // the Http1Response is now only weakly referenced by the Selector.
}
public void cancel() {
@@ -153,19 +168,15 @@
ExchangeImpl<?> impl = exchImpl;
if (impl != null) {
// propagate the exception to the impl
+ debug.log(Level.DEBUG, "Cancelling exchImpl: %s", exchImpl);
impl.cancel(cause);
} else {
- try {
- // no impl yet. record the exception
- failed = cause;
- // now call checkCancelled to recheck the impl.
- // if the failed state is set and the impl is not null, reset
- // the failed state and propagate the exception to the impl.
- checkCancelled(false);
- } catch (IOException x) {
- // should not happen - we passed 'false' above
- throw new UncheckedIOException(x);
- }
+ // no impl yet. record the exception
+ failed = cause;
+ // now call checkCancelled to recheck the impl.
+ // if the failed state is set and the impl is not null, reset
+ // the failed state and propagate the exception to the impl.
+ checkCancelled();
}
}
@@ -175,36 +186,30 @@
// will persist until the exception can be raised and the failed state
// can be cleared.
// Takes care of possible race conditions.
- private void checkCancelled(boolean throwIfNoImpl) throws IOException {
+ private void checkCancelled() {
ExchangeImpl<?> impl = null;
IOException cause = null;
if (failed != null) {
synchronized(this) {
cause = failed;
impl = exchImpl;
- if (throwIfNoImpl || impl != null) {
- // The exception will be raised by one of the two methods
- // below: reset the failed state.
- failed = null;
- }
}
}
if (cause == null) return;
if (impl != null) {
// The exception is raised by propagating it to the impl.
+ debug.log(Level.DEBUG, "Cancelling exchImpl: %s", impl);
impl.cancel(cause);
- } else if (throwIfNoImpl) {
- // The exception is raised by throwing it immediately
- throw cause;
+ failed = null;
} else {
Log.logTrace("Exchange: request [{0}/timeout={1}ms] no impl is set."
+ "\n\tCan''t cancel yet with {2}",
request.uri(),
- request.duration() == null ? -1 :
+ request.timeout().isPresent() ?
// calling duration.toMillis() can throw an exception.
// this is just debugging, we don't care if it overflows.
- (request.duration().getSeconds() * 1000
- + request.duration().getNano() / 1000000),
+ (request.timeout().get().getSeconds() * 1000
+ + request.timeout().get().getNano() / 1000000) : -1,
cause);
}
}
@@ -216,90 +221,34 @@
static final SocketPermission[] SOCKET_ARRAY = new SocketPermission[0];
- Response responseImpl(HttpConnection connection)
- throws IOException, InterruptedException
- {
- SecurityException e = securityCheck(acc);
- if (e != null) {
- throw e;
- }
-
- if (permissions.size() > 0) {
- try {
- return AccessController.doPrivileged(
- (PrivilegedExceptionAction<Response>)() ->
- responseImpl0(connection),
- null,
- permissions.toArray(SOCKET_ARRAY));
- } catch (Throwable ee) {
- if (ee instanceof PrivilegedActionException) {
- ee = ee.getCause();
- }
- if (ee instanceof IOException) {
- throw (IOException) ee;
- } else {
- throw new RuntimeException(ee); // TODO: fix
- }
- }
- } else {
- return responseImpl0(connection);
- }
+ synchronized IOException getCancelCause() {
+ return failed;
}
// get/set the exchange impl, solving race condition issues with
// potential concurrent calls to cancel() or cancel(IOException)
- private void establishExchange(HttpConnection connection)
- throws IOException, InterruptedException
- {
+ private CompletableFuture<? extends ExchangeImpl<T>>
+ establishExchange(HttpConnection connection) {
// check if we have been cancelled first.
- checkCancelled(true);
- // not yet cancelled: create/get a new impl
- exchImpl = ExchangeImpl.get(this, connection);
- // recheck for cancelled, in case of race conditions
- checkCancelled(true);
- // now we're good to go. because exchImpl is no longer null
- // cancel() will be able to propagate directly to the impl
- // after this point.
- }
-
- private Response responseImpl0(HttpConnection connection)
- throws IOException, InterruptedException
- {
- establishExchange(connection);
- if (request.expectContinue()) {
- Log.logTrace("Sending Expect: 100-Continue");
- request.addSystemHeader("Expect", "100-Continue");
- exchImpl.sendHeadersOnly();
-
- Log.logTrace("Waiting for 407-Expectation-Failed or 100-Continue");
- Response resp = exchImpl.getResponse();
- HttpResponseImpl.logResponse(resp);
- int rcode = resp.statusCode();
- if (rcode != 100) {
- Log.logTrace("Expectation failed: Received {0}",
- rcode);
- if (upgrading && rcode == 101) {
- throw new IOException(
- "Unable to handle 101 while waiting for 100-Continue");
- }
- return resp;
- }
-
- Log.logTrace("Received 100-Continue: sending body");
- exchImpl.sendBody();
-
- Log.logTrace("Body sent: waiting for response");
- resp = exchImpl.getResponse();
- HttpResponseImpl.logResponse(resp);
-
- return checkForUpgrade(resp, exchImpl);
- } else {
- exchImpl.sendHeadersOnly();
- exchImpl.sendBody();
- Response resp = exchImpl.getResponse();
- HttpResponseImpl.logResponse(resp);
- return checkForUpgrade(resp, exchImpl);
+ Throwable t = getCancelCause();
+ checkCancelled();
+ if (t != null) {
+ return MinimalFuture.failedFuture(t);
}
+ CompletableFuture<? extends ExchangeImpl<T>> cf = ExchangeImpl.get(this, connection);
+ return cf.thenCompose((eimpl) -> {
+ // recheck for cancelled, in case of race conditions
+ exchImpl = eimpl;
+ IOException tt = getCancelCause();
+ checkCancelled();
+ if (tt != null) {
+ return MinimalFuture.failedFuture(tt);
+ } else {
+ // Now we're good to go. Because exchImpl is no longer
+ // null cancel() will be able to propagate directly to
+ // the impl after this point ( if needed ).
+ return MinimalFuture.completedFuture(eimpl);
+ } });
}
// Completed HttpResponse will be null if response succeeded
@@ -310,35 +259,23 @@
}
CompletableFuture<Response> responseAsyncImpl(HttpConnection connection) {
- SecurityException e = securityCheck(acc);
+ SecurityException e = checkPermissions();
if (e != null) {
return MinimalFuture.failedFuture(e);
- }
- if (permissions.size() > 0) {
- return AccessController.doPrivileged(
- (PrivilegedAction<CompletableFuture<Response>>)() ->
- responseAsyncImpl0(connection),
- null,
- permissions.toArray(SOCKET_ARRAY));
} else {
return responseAsyncImpl0(connection);
}
}
CompletableFuture<Response> responseAsyncImpl0(HttpConnection connection) {
- try {
- establishExchange(connection);
- } catch (IOException | InterruptedException e) {
- return MinimalFuture.failedFuture(e);
- }
if (request.expectContinue()) {
request.addSystemHeader("Expect", "100-Continue");
Log.logTrace("Sending Expect: 100-Continue");
- return exchImpl
- .sendHeadersAsync()
+ return establishExchange(connection)
+ .thenCompose((ex) -> ex.sendHeadersAsync())
.thenCompose(v -> exchImpl.getResponseAsync(parentExecutor))
.thenCompose((Response r1) -> {
- HttpResponseImpl.logResponse(r1);
+ Log.logResponse(r1::toString);
int rcode = r1.statusCode();
if (rcode == 100) {
Log.logTrace("Received 100-Continue: sending body");
@@ -361,8 +298,8 @@
}
});
} else {
- CompletableFuture<Response> cf = exchImpl
- .sendHeadersAsync()
+ CompletableFuture<Response> cf = establishExchange(connection)
+ .thenCompose((ex) -> ex.sendHeadersAsync())
.thenCompose(ExchangeImpl::sendBodyAsync)
.thenCompose(exIm -> exIm.getResponseAsync(parentExecutor));
cf = wrapForUpgrade(cf);
@@ -381,15 +318,15 @@
private CompletableFuture<Response> wrapForLog(CompletableFuture<Response> cf) {
if (Log.requests()) {
return cf.thenApply(response -> {
- HttpResponseImpl.logResponse(response);
+ Log.logResponse(response::toString);
return response;
});
}
return cf;
}
- HttpResponse.BodyProcessor<T> ignoreBody(int status, HttpHeaders hdrs) {
- return HttpResponse.BodyProcessor.discard((T)null);
+ HttpResponse.BodySubscriber<T> ignoreBody(int status, HttpHeaders hdrs) {
+ return HttpResponse.BodySubscriber.discard((T)null);
}
// if this response was received in reply to an upgrade
@@ -406,50 +343,59 @@
// check for 101 switching protocols
// 101 responses are not supposed to contain a body.
// => should we fail if there is one?
+ debug.log(Level.DEBUG, "Upgrading async %s" + e.connection());
return e.readBodyAsync(this::ignoreBody, false, parentExecutor)
- .thenCompose((T v) -> // v is null
- Http2Connection.createAsync(e.connection(),
+ .thenCompose((T v) -> {// v is null
+ debug.log(Level.DEBUG, "Ignored body");
+ // we pass e::getBuffer to allow the ByteBuffers to accumulate
+ // while we build the Http2Connection
+ return Http2Connection.createAsync(e.connection(),
client.client2(),
- this, e.getBuffer())
+ this, e::getBuffer)
.thenCompose((Http2Connection c) -> {
c.putConnection();
Stream<T> s = c.getStream(1);
- exchImpl = s;
+ if (s == null) {
+ // s can be null if an exception occurred
+ // asynchronously while sending the preface.
+ Throwable t = c.getRecordedCause();
+ if (t != null) {
+ return MinimalFuture.failedFuture(
+ new IOException("Can't get stream 1: " + t, t));
+ }
+ }
+ exchImpl.released();
+ Throwable t;
+ // There's a race condition window where an external
+ // thread (SelectorManager) might complete the
+ // exchange in timeout at the same time where we're
+ // trying to switch the exchange impl.
+ // 'failed' will be reset to null after
+ // exchImpl.cancel() has completed, so either we
+ // will observe failed != null here, or we will
+ // observe e.getCancelCause() != null, or the
+ // timeout exception will be routed to 's'.
+ // Either way, we need to relay it to s.
+ synchronized (this) {
+ exchImpl = s;
+ t = failed;
+ }
+ // Check whether the HTTP/1.1 was cancelled.
+ if (t == null) t = e.getCancelCause();
+ // if HTTP/1.1 exchange was timed out, don't
+ // try to go further.
+ if (t instanceof HttpTimeoutException) {
+ s.cancelImpl(t);
+ return MinimalFuture.failedFuture(t);
+ }
+ debug.log(Level.DEBUG, "Getting response async %s" + s);
return s.getResponseAsync(null);
- })
+ });}
);
}
return MinimalFuture.completedFuture(resp);
}
- private Response checkForUpgrade(Response resp,
- ExchangeImpl<T> ex)
- throws IOException, InterruptedException
- {
- int rcode = resp.statusCode();
- if (upgrading && (rcode == 101)) {
- Http1Exchange<T> e = (Http1Exchange<T>) ex;
-
- // 101 responses are not supposed to contain a body.
- // => should we fail if there is one?
- // => readBody called here by analogy with
- // checkForUpgradeAsync above
- e.readBody(this::ignoreBody, false);
-
- // must get connection from Http1Exchange
- Http2Connection h2con = new Http2Connection(e.connection(),
- client.client2(),
- this, e.getBuffer());
- h2con.putConnection();
- Stream<T> s = h2con.getStream(1);
- exchImpl = s;
- Response xx = s.getResponse();
- HttpResponseImpl.logResponse(xx);
- return xx;
- }
- return resp;
- }
-
private URI getURIForSecurityCheck() {
URI u;
String method = request.method();
@@ -476,12 +422,50 @@
}
/**
- * Do the security check and return any exception.
- * Return null if no check needed or passes.
- *
- * Also adds any generated permissions to the "permissions" list.
+ * Returns the security permission required for the given details.
+ * If method is CONNECT, then uri must be of form "scheme://host:port"
*/
- private SecurityException securityCheck(AccessControlContext acc) {
+ private static URLPermission getPermissionFor(URI uri,
+ String method,
+ Map<String, List<String>> headers) {
+ StringBuilder sb = new StringBuilder();
+
+ String urlstring, actionstring;
+
+ if (method.equals("CONNECT")) {
+ urlstring = uri.toString();
+ actionstring = "CONNECT";
+ } else {
+ sb.append(uri.getScheme()).append("://")
+ .append(uri.getAuthority())
+ .append(uri.getPath());
+ urlstring = sb.toString();
+
+ sb = new StringBuilder();
+ sb.append(method);
+ if (headers != null && !headers.isEmpty()) {
+ sb.append(':');
+ Set<String> keys = headers.keySet();
+ boolean first = true;
+ for (String key : keys) {
+ if (!first) {
+ sb.append(',');
+ }
+ sb.append(key);
+ first = false;
+ }
+ }
+ actionstring = sb.toString();
+ }
+ return new URLPermission(urlstring, actionstring);
+ }
+
+ /**
+ * Performs the necessary security permission checks required to retrieve
+ * the response. Returns a security exception representing the denied
+ * permission, or null if all checks pass or there is no security manager.
+ */
+ private SecurityException checkPermissions() {
SecurityManager sm = System.getSecurityManager();
if (sm == null) {
return null;
@@ -490,12 +474,11 @@
String method = request.method();
HttpHeaders userHeaders = request.getUserHeaders();
URI u = getURIForSecurityCheck();
- URLPermission p = Utils.getPermission(u, method, userHeaders.map());
+ URLPermission p = getPermissionFor(u, method, userHeaders.map());
try {
assert acc != null;
sm.checkPermission(p, acc);
- permissions.add(getSocketPermissionFor(u));
} catch (SecurityException e) {
return e;
}
@@ -516,13 +499,8 @@
try {
sm.checkPermission(p, acc);
} catch (SecurityException e) {
- permissions.clear();
return e;
}
- String sockperm = proxy.getHostString() +
- ":" + Integer.toString(proxy.getPort());
-
- permissions.add(new SocketPermission(sockperm, "connect,resolve"));
}
}
return null;
@@ -563,4 +541,8 @@
AccessControlContext getAccessControlContext() {
return acc;
}
+
+ String dbgString() {
+ return dbgTag;
+ }
}
--- a/src/jdk.incubator.httpclient/share/classes/jdk/incubator/http/ExchangeImpl.java Sun Nov 05 17:05:57 2017 +0000
+++ b/src/jdk.incubator.httpclient/share/classes/jdk/incubator/http/ExchangeImpl.java Sun Nov 05 17:32:13 2017 +0000
@@ -26,10 +26,13 @@
package jdk.incubator.http;
import java.io.IOException;
+import java.lang.System.Logger.Level;
import java.util.concurrent.CompletableFuture;
import java.util.concurrent.Executor;
+import java.util.function.Function;
import jdk.incubator.http.internal.common.MinimalFuture;
import static jdk.incubator.http.HttpClient.Version.HTTP_1_1;
+import jdk.incubator.http.internal.common.Utils;
/**
* Splits request so that headers and body can be sent separately with optional
@@ -46,6 +49,10 @@
*/
abstract class ExchangeImpl<T> {
+ static final boolean DEBUG = Utils.DEBUG; // Revisit: temporary dev flag.
+ private static final System.Logger DEBUG_LOGGER =
+ Utils.getDebugLogger("ExchangeImpl"::toString, DEBUG);
+
final Exchange<T> exchange;
ExchangeImpl(Exchange<T> e) {
@@ -68,94 +75,125 @@
* Initiates a new exchange and assigns it to a connection if one exists
* already. connection usually null.
*/
- static <U> ExchangeImpl<U> get(Exchange<U> exchange, HttpConnection connection)
- throws IOException, InterruptedException
+ static <U> CompletableFuture<? extends ExchangeImpl<U>>
+ get(Exchange<U> exchange, HttpConnection connection)
{
HttpRequestImpl req = exchange.request();
if (exchange.version() == HTTP_1_1) {
- return new Http1Exchange<>(exchange, connection);
+ DEBUG_LOGGER.log(Level.DEBUG, "get: HTTP/1.1: new Http1Exchange");
+ return createHttp1Exchange(exchange, connection);
} else {
Http2ClientImpl c2 = exchange.client().client2(); // TODO: improve
HttpRequestImpl request = exchange.request();
- Http2Connection c;
- try {
- c = c2.getConnectionFor(request);
- } catch (Http2Connection.ALPNException e) {
- // failed to negotiate "h2"
- AbstractAsyncSSLConnection as = e.getConnection();
- as.stopAsyncReading();
- HttpConnection sslc = as.downgrade();
- ExchangeImpl<U> ex = new Http1Exchange<>(exchange, sslc);
+ CompletableFuture<Http2Connection> c2f = c2.getConnectionFor(request);
+ DEBUG_LOGGER.log(Level.DEBUG, "get: Trying to get HTTP/2 connection");
+ return c2f.handle((h2c, t) -> createExchangeImpl(h2c, t, exchange, connection))
+ .thenCompose(Function.identity());
+ }
+ }
+
+ private static <U> CompletableFuture<? extends ExchangeImpl<U>>
+ createExchangeImpl(Http2Connection c,
+ Throwable t,
+ Exchange<U> exchange,
+ HttpConnection connection)
+ {
+ DEBUG_LOGGER.log(Level.DEBUG, "handling HTTP/2 connection creation result");
+ if (t != null) {
+ DEBUG_LOGGER.log(Level.DEBUG,
+ "handling HTTP/2 connection creation failed: %s",
+ (Object)t);
+ t = Utils.getCompletionCause(t);
+ if (t instanceof Http2Connection.ALPNException) {
+ Http2Connection.ALPNException ee = (Http2Connection.ALPNException)t;
+ AbstractAsyncSSLConnection as = ee.getConnection();
+ DEBUG_LOGGER.log(Level.DEBUG, "downgrading to HTTP/1.1 with: %s", as);
+ CompletableFuture<? extends ExchangeImpl<U>> ex =
+ createHttp1Exchange(exchange, as);
return ex;
+ } else {
+ DEBUG_LOGGER.log(Level.DEBUG, "HTTP/2 connection creation failed "
+ + "with unexpected exception: %s", (Object)t);
+ return CompletableFuture.failedFuture(t);
}
- if (c == null) {
- // no existing connection. Send request with HTTP 1 and then
- // upgrade if successful
- ExchangeImpl<U> ex = new Http1Exchange<>(exchange, connection);
- exchange.h2Upgrade();
- return ex;
- }
- return c.createStream(exchange);
+ }
+ if (c == null) {
+ // no existing connection. Send request with HTTP 1 and then
+ // upgrade if successful
+ DEBUG_LOGGER.log(Level.DEBUG, "new Http1Exchange, try to upgrade");
+ return createHttp1Exchange(exchange, connection)
+ .thenApply((e) -> {
+ exchange.h2Upgrade();
+ return e;
+ });
+ } else {
+ DEBUG_LOGGER.log(Level.DEBUG, "creating HTTP/2 streams");
+ Stream<U> s = c.createStream(exchange);
+ CompletableFuture<? extends ExchangeImpl<U>> ex = MinimalFuture.completedFuture(s);
+ return ex;
+ }
+ }
+
+ private static <T> CompletableFuture<Http1Exchange<T>>
+ createHttp1Exchange(Exchange<T> ex, HttpConnection as)
+ {
+ try {
+ return MinimalFuture.completedFuture(new Http1Exchange<>(ex, as));
+ } catch (Throwable e) {
+ return MinimalFuture.failedFuture(e);
}
}
/* The following methods have separate HTTP/1.1 and HTTP/2 implementations */
- /**
- * Sends the request headers only. May block until all sent.
- */
- abstract void sendHeadersOnly() throws IOException, InterruptedException;
-
- // Blocking impl but in async style
+ abstract CompletableFuture<ExchangeImpl<T>> sendHeadersAsync();
- CompletableFuture<ExchangeImpl<T>> sendHeadersAsync() {
- // this is blocking. cf will already be completed.
- return MinimalFuture.supply(() -> {
- sendHeadersOnly();
- return this;
- });
- }
-
- /**
- * Gets response by blocking if necessary. This may be an
- * intermediate response (like 101) or a final response 200 etc. Returns
- * before body is read.
- */
- abstract Response getResponse() throws IOException;
-
- abstract T readBody(HttpResponse.BodyHandler<T> handler,
- boolean returnConnectionToPool) throws IOException;
+ /** Sends a request body, after request headers have been sent. */
+ abstract CompletableFuture<ExchangeImpl<T>> sendBodyAsync();
abstract CompletableFuture<T> readBodyAsync(HttpResponse.BodyHandler<T> handler,
boolean returnConnectionToPool,
Executor executor);
- /**
- * Async version of getResponse. Completes before body is read.
- */
+ /** Gets the response headers. Completes before body is read. */
abstract CompletableFuture<Response> getResponseAsync(Executor executor);
- /**
- * Sends a request body after request headers.
- */
- abstract void sendBody() throws IOException, InterruptedException;
- // Async version of sendBody(). This only used when body sent separately
- // to headers (100 continue)
- CompletableFuture<ExchangeImpl<T>> sendBodyAsync() {
- return MinimalFuture.supply(() -> {
- sendBody();
- return this;
- });
- }
-
- /**
- * Cancels a request. Not currently exposed through API.
- */
+ /** Cancels a request. Not currently exposed through API. */
abstract void cancel();
/**
* Cancels a request with a cause. Not currently exposed through API.
*/
abstract void cancel(IOException cause);
+
+ /**
+ * Called when the exchange is released, so that cleanup actions may be
+ * performed - such as deregistering callbacks.
+ * Typically released is called during upgrade, when an HTTP/2 stream
+ * takes over from an Http1Exchange, or when a new exchange is created
+ * during a multi exchange before the final response body was received.
+ */
+ abstract void released();
+
+ /**
+ * Called when the exchange is completed, so that cleanup actions may be
+ * performed - such as deregistering callbacks.
+ * Typically, completed is called at the end of the exchange, when the
+ * final response body has been received (or an error has caused the
+ * completion of the exchange).
+ */
+ abstract void completed();
+
+ /**
+ * Returns true if this exchange was canceled.
+ * @return true if this exchange was canceled.
+ */
+ abstract boolean isCanceled();
+
+ /**
+ * Returns the cause for which this exchange was canceled, if available.
+ * @return the cause for which this exchange was canceled, if available.
+ */
+ abstract Throwable getCancelCause();
}
--- a/src/jdk.incubator.httpclient/share/classes/jdk/incubator/http/ExecutorWrapper.java Sun Nov 05 17:05:57 2017 +0000
+++ /dev/null Thu Jan 01 00:00:00 1970 +0000
@@ -1,105 +0,0 @@
-/*
- * Copyright (c) 2015, 2016, Oracle and/or its affiliates. All rights reserved.
- * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER.
- *
- * This code is free software; you can redistribute it and/or modify it
- * under the terms of the GNU General Public License version 2 only, as
- * published by the Free Software Foundation. Oracle designates this
- * particular file as subject to the "Classpath" exception as provided
- * by Oracle in the LICENSE file that accompanied this code.
- *
- * This code is distributed in the hope that it will be useful, but WITHOUT
- * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or
- * FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License
- * version 2 for more details (a copy is included in the LICENSE file that
- * accompanied this code).
- *
- * You should have received a copy of the GNU General Public License version
- * 2 along with this work; if not, write to the Free Software Foundation,
- * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA.
- *
- * Please contact Oracle, 500 Oracle Parkway, Redwood Shores, CA 94065 USA
- * or visit www.oracle.com if you need additional information or have any
- * questions.
- */
-
-package jdk.incubator.http;
-
-import java.net.SocketPermission;
-import java.security.AccessControlContext;
-import java.security.AccessController;
-import java.security.PrivilegedAction;
-import java.util.concurrent.Executor;
-import jdk.internal.misc.InnocuousThread;
-
-/**
- * Wraps the supplied user Executor
- *
- * when a Security manager set, the correct access control context is used to execute task
- *
- * The access control context is captured at creation time of this object
- */
-class ExecutorWrapper {
-
- final Executor userExecutor; // the undeerlying executor provided by user
- final Executor executor; // the executur which wraps the user's one
- final AccessControlContext acc;
- final ClassLoader ccl;
-
- public ExecutorWrapper(Executor userExecutor, AccessControlContext acc) {
- this.userExecutor = userExecutor;
- this.acc = acc;
- this.ccl = getCCL();
- if (System.getSecurityManager() == null) {
- this.executor = userExecutor;
- } else {
- this.executor = this::run;
- }
- }
-
- private ClassLoader getCCL() {
- return AccessController.doPrivileged(
- (PrivilegedAction<ClassLoader>) () -> {
- return Thread.currentThread().getContextClassLoader();
- }
- );
- }
-
- /**
- * This is only used for the default HttpClient to deal with
- * different application contexts that might be using it.
- * The default client uses InnocuousThreads in its Executor.
- */
- private void prepareThread() {
- final Thread me = Thread.currentThread();
- if (!(me instanceof InnocuousThread))
- return;
- InnocuousThread innocuousMe = (InnocuousThread)me;
-
- AccessController.doPrivileged(
- (PrivilegedAction<Void>) () -> {
- innocuousMe.setContextClassLoader(ccl);
- innocuousMe.eraseThreadLocals();
- return null;
- }
- );
- }
-
-
- void run(Runnable r) {
- prepareThread();
- try {
- userExecutor.execute(r); // all throwables must be caught
- } catch (Throwable t) {
- t.printStackTrace();
- }
- }
-
- public Executor userExecutor() {
- return userExecutor;
- }
-
- public Executor executor() {
- return executor;
- }
-}
--- /dev/null Thu Jan 01 00:00:00 1970 +0000
+++ b/src/jdk.incubator.httpclient/share/classes/jdk/incubator/http/Http1AsyncReceiver.java Sun Nov 05 17:32:13 2017 +0000
@@ -0,0 +1,643 @@
+/*
+ * Copyright (c) 2017, Oracle and/or its affiliates. All rights reserved.
+ * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER.
+ *
+ * This code is free software; you can redistribute it and/or modify it
+ * under the terms of the GNU General Public License version 2 only, as
+ * published by the Free Software Foundation. Oracle designates this
+ * particular file as subject to the "Classpath" exception as provided
+ * by Oracle in the LICENSE file that accompanied this code.
+ *
+ * This code is distributed in the hope that it will be useful, but WITHOUT
+ * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or
+ * FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License
+ * version 2 for more details (a copy is included in the LICENSE file that
+ * accompanied this code).
+ *
+ * You should have received a copy of the GNU General Public License version
+ * 2 along with this work; if not, write to the Free Software Foundation,
+ * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA.
+ *
+ * Please contact Oracle, 500 Oracle Parkway, Redwood Shores, CA 94065 USA
+ * or visit www.oracle.com if you need additional information or have any
+ * questions.
+ */
+
+package jdk.incubator.http;
+
+import java.io.EOFException;
+import java.io.IOException;
+import java.lang.System.Logger.Level;
+import java.nio.ByteBuffer;
+import java.util.Arrays;
+import java.util.HashSet;
+import java.util.List;
+import java.util.Set;
+import java.util.concurrent.ConcurrentLinkedDeque;
+import java.util.concurrent.Executor;
+import java.util.concurrent.Flow;
+import java.util.concurrent.atomic.AtomicBoolean;
+import java.util.concurrent.atomic.AtomicLong;
+import java.util.concurrent.atomic.AtomicReference;
+import java.util.function.Supplier;
+import java.util.stream.Collectors;
+import jdk.incubator.http.internal.common.Demand;
+import jdk.incubator.http.internal.common.FlowTube.TubeSubscriber;
+import jdk.incubator.http.internal.common.SequentialScheduler;
+import jdk.incubator.http.internal.common.SequentialScheduler.SynchronizedRestartableTask;
+import jdk.incubator.http.internal.common.ConnectionExpiredException;
+import jdk.incubator.http.internal.common.Utils;
+
+
+/**
+ * A helper class that will queue up incoming data until the receiving
+ * side is ready to handle it.
+ */
+class Http1AsyncReceiver {
+
+ static final boolean DEBUG = Utils.DEBUG; // Revisit: temporary dev flag.
+ final System.Logger debug = Utils.getDebugLogger(this::dbgString, DEBUG);
+
+ /**
+ * A delegate that can asynchronously receive data from an upstream flow,
+ * parse, it, then possibly transform it and either store it (response
+ * headers) or possibly pass it to a downstream subscriber (response body).
+ * Usually, there will be one Http1AsyncDelegate in charge of receiving
+ * and parsing headers, and another one in charge of receiving, parsing,
+ * and forwarding body. Each will sequentially subscribe with the
+ * Http1AsyncReceiver in turn. There may be additional delegates which
+ * subscribe to the Http1AsyncReceiver, mainly for the purpose of handling
+ * errors while the connection is busy transmitting the request body and the
+ * Http1Exchange::readBody method hasn't been called yet, and response
+ * delegates haven't subscribed yet.
+ */
+ static interface Http1AsyncDelegate {
+ /**
+ * Receives and handles a byte buffer reference.
+ * @param ref A byte buffer reference coming from upstream.
+ * @return false, if the byte buffer reference should be kept in the queue.
+ * Usually, this means that either the byte buffer reference
+ * was handled and parsing is finished, or that the receiver
+ * didn't handle the byte reference at all.
+ * There may or may not be any remaining data in the
+ * byte buffer, and the byte buffer reference must not have
+ * been cleared.
+ * true, if the byte buffer reference was fully read and
+ * more data can be received.
+ */
+ public boolean tryAsyncReceive(ByteBuffer ref);
+
+ /**
+ * Called when an exception is raised.
+ * @param ex The raised Throwable.
+ */
+ public void onReadError(Throwable ex);
+
+ /**
+ * Must be called before any other method on the delegate.
+ * The subscription can be either used directly by the delegate
+ * to request more data (e.g. if the delegate is a header parser),
+ * or can be forwarded to a downstream subscriber (if the delegate
+ * is a body parser that wraps a response BodyProcessor).
+ * In all cases, it is the responsibility of the delegate to ensure
+ * that request(n) and demand.tryDecrement() are called appropriately.
+ * No data will be sent to {@code tryAsyncReceive} unless
+ * the subscription has some demand.
+ *
+ * @param s A subscription that allows the delegate to control the
+ * data flow.
+ */
+ public void onSubscribe(AbstractSubscription s);
+
+ /**
+ * Returns the subscription that was passed to {@code onSubscribe}
+ * @return the subscription that was passed to {@code onSubscribe}..
+ */
+ public AbstractSubscription subscription();
+
+ }
+
+ /**
+ * A simple subclass of AbstractSubscription that ensures the
+ * SequentialScheduler will be run when request() is called and demand
+ * becomes positive again.
+ */
+ private static final class Http1AsyncDelegateSubscription
+ extends AbstractSubscription
+ {
+ private final Runnable onCancel;
+ private final SequentialScheduler scheduler;
+ Http1AsyncDelegateSubscription(SequentialScheduler scheduler,
+ Runnable onCancel) {
+ this.scheduler = scheduler;
+ this.onCancel = onCancel;
+ }
+ @Override
+ public void request(long n) {
+ final Demand demand = demand();
+ if (demand.increase(n)) {
+ scheduler.runOrSchedule();
+ }
+ }
+ @Override
+ public void cancel() { onCancel.run();}
+ }
+
+ private final ConcurrentLinkedDeque<ByteBuffer> queue
+ = new ConcurrentLinkedDeque<>();
+ private final SequentialScheduler scheduler =
+ new SequentialScheduler(new SynchronizedRestartableTask(this::flush));
+ private final Executor executor;
+ private final Http1TubeSubscriber subscriber = new Http1TubeSubscriber();
+ private final AtomicReference<Http1AsyncDelegate> pendingDelegateRef;
+ private final AtomicLong received = new AtomicLong();
+ final AtomicBoolean canRequestMore = new AtomicBoolean();
+
+ private volatile Throwable error;
+ private volatile Http1AsyncDelegate delegate;
+ // This reference is only used to prevent early GC of the exchange.
+ private volatile Http1Exchange<?> owner;
+ // Only used for checking whether we run on the selector manager thread.
+ private final HttpClientImpl client;
+ private boolean retry;
+
+ public Http1AsyncReceiver(Executor executor, Http1Exchange<?> owner) {
+ this.pendingDelegateRef = new AtomicReference<>();
+ this.executor = executor;
+ this.owner = owner;
+ this.client = owner.client;
+ }
+
+ // This is the main loop called by the SequentialScheduler.
+ // It attempts to empty the queue until the scheduler is stopped,
+ // or the delegate is unregistered, or the delegate is unable to
+ // process the data (because it's not ready or already done), which
+ // it signals by returning 'true';
+ private void flush() {
+ ByteBuffer buf;
+ try {
+ assert !client.isSelectorThread() :
+ "Http1AsyncReceiver::flush should not run in the selector: "
+ + Thread.currentThread().getName();
+
+ // First check whether we have a pending delegate that has
+ // just subscribed, and if so, create a Subscription for it
+ // and call onSubscribe.
+ handlePendingDelegate();
+
+ // Then start emptying the queue, if possible.
+ while ((buf = queue.peek()) != null) {
+ Http1AsyncDelegate delegate = this.delegate;
+ debug.log(Level.DEBUG, "Got %s bytes for delegate %s",
+ buf.remaining(), delegate);
+ if (!hasDemand(delegate)) {
+ // The scheduler will be invoked again later when the demand
+ // becomes positive.
+ return;
+ }
+
+ assert delegate != null;
+ debug.log(Level.DEBUG, "Forwarding %s bytes to delegate %s",
+ buf.remaining(), delegate);
+ // The delegate has demand: feed it the next buffer.
+ if (!delegate.tryAsyncReceive(buf)) {
+ final long remaining = buf.remaining();
+ debug.log(Level.DEBUG, () -> {
+ // If the scheduler is stopped, the queue may already
+ // be empty and the reference may already be released.
+ String remstr = scheduler.isStopped() ? "" :
+ " remaining in ref: "
+ + remaining;
+ remstr = remstr
+ + " total remaining: " + remaining();
+ return "Delegate done: " + remaining;
+ });
+ canRequestMore.set(false);
+ // The last buffer parsed may have remaining unparsed bytes.
+ // Don't take it out of the queue.
+ return; // done.
+ }
+
+ // removed parsed buffer from queue, and continue with next
+ // if available
+ ByteBuffer parsed = queue.remove();
+ canRequestMore.set(queue.isEmpty());
+ assert parsed == buf;
+ }
+
+ // queue is empty: let's see if we should request more
+ checkRequestMore();
+
+ } catch (Throwable t) {
+ Throwable x = error;
+ if (x == null) error = t; // will be handled in the finally block
+ debug.log(Level.DEBUG, "Unexpected error caught in flush()", t);
+ } finally {
+ // Handles any pending error.
+ // The most recently subscribed delegate will get the error.
+ checkForErrors();
+ }
+ }
+
+ /**
+ * Must be called from within the scheduler main loop.
+ * Handles any pending errors by calling delegate.onReadError().
+ * If the error can be forwarded to the delegate, stops the scheduler.
+ */
+ private void checkForErrors() {
+ // Handles any pending error.
+ // The most recently subscribed delegate will get the error.
+ // If the delegate is null, the error will be handled by the next
+ // delegate that subscribes.
+ // If the queue is not empty, wait until it it is empty before
+ // handling the error.
+ Http1AsyncDelegate delegate = pendingDelegateRef.get();
+ if (delegate == null) delegate = this.delegate;
+ Throwable x = error;
+ if (delegate != null && x != null && queue.isEmpty()) {
+ // forward error only after emptying the queue.
+ final Object captured = delegate;
+ debug.log(Level.DEBUG, () -> "flushing " + x
+ + "\n\t delegate: " + captured
+ + "\t\t queue.isEmpty: " + queue.isEmpty());
+ scheduler.stop();
+ delegate.onReadError(x);
+ }
+ }
+
+ /**
+ * Must be called from within the scheduler main loop.
+ * Figure out whether more data should be requested from the
+ * Http1TubeSubscriber.
+ */
+ private void checkRequestMore() {
+ Http1AsyncDelegate delegate = this.delegate;
+ boolean more = this.canRequestMore.get();
+ boolean hasDemand = hasDemand(delegate);
+ debug.log(Level.DEBUG, () -> "checkRequestMore: "
+ + "canRequestMore=" + more + ", hasDemand=" + hasDemand
+ + (delegate == null ? ", delegate=null" : ""));
+ if (hasDemand) {
+ subscriber.requestMore();
+ }
+ }
+
+ /**
+ * Must be called from within the scheduler main loop.
+ * Return true if the delegate is not null and has some demand.
+ * @param delegate The Http1AsyncDelegate delegate
+ * @return true if the delegate is not null and has some demand
+ */
+ private boolean hasDemand(Http1AsyncDelegate delegate) {
+ if (delegate == null) return false;
+ AbstractSubscription subscription = delegate.subscription();
+ long demand = subscription.demand().get();
+ debug.log(Level.DEBUG, "downstream subscription demand is %s", demand);
+ return demand > 0;
+ }
+
+ /**
+ * Must be called from within the scheduler main loop.
+ * Handles pending delegate subscription.
+ * Return true if there was some pending delegate subscription and a new
+ * delegate was subscribed, false otherwise.
+ *
+ * @return true if there was some pending delegate subscription and a new
+ * delegate was subscribed, false otherwise.
+ */
+ private boolean handlePendingDelegate() {
+ Http1AsyncDelegate pending = pendingDelegateRef.get();
+ if (pending != null && pendingDelegateRef.compareAndSet(pending, null)) {
+ Http1AsyncDelegate delegate = this.delegate;
+ if (delegate != null) unsubscribe(delegate);
+ Runnable cancel = () -> {
+ debug.log(Level.DEBUG, "Downstream subscription cancelled by %s", pending);
+ unsubscribe(pending);
+ };
+ // The subscription created by a delegate is only loosely
+ // coupled with the upstream subscription. This is partly because
+ // the header/body parser work with a flow of ByteBuffer, whereas
+ // we have a flow List<ByteBuffer> upstream.
+ Http1AsyncDelegateSubscription subscription =
+ new Http1AsyncDelegateSubscription(scheduler, cancel);
+ pending.onSubscribe(subscription);
+ this.delegate = delegate = pending;
+ final Object captured = delegate;
+ debug.log(Level.DEBUG, () -> "delegate is now " + captured
+ + ", demand=" + subscription.demand().get()
+ + ", canRequestMore=" + canRequestMore.get()
+ + ", queue.isEmpty=" + queue.isEmpty());
+ return true;
+ }
+ return false;
+ }
+
+ synchronized void setRetryOnError(boolean retry) {
+ this.retry = retry;
+ }
+
+ void clear() {
+ debug.log(Level.DEBUG, "cleared");
+ this.pendingDelegateRef.set(null);
+ this.delegate = null;
+ this.owner = null;
+ }
+
+ void subscribe(Http1AsyncDelegate delegate) {
+ synchronized(this) {
+ pendingDelegateRef.set(delegate);
+ }
+ if (queue.isEmpty()) {
+ canRequestMore.set(true);
+ }
+ debug.log(Level.DEBUG, () ->
+ "Subscribed pending " + delegate + " queue.isEmpty: "
+ + queue.isEmpty());
+ // Everything may have been received already. Make sure
+ // we parse it.
+ if (client.isSelectorThread()) {
+ scheduler.deferOrSchedule(executor);
+ } else {
+ scheduler.runOrSchedule();
+ }
+ }
+
+ // Used for debugging only!
+ long remaining() {
+ return Utils.remaining(queue.toArray(new ByteBuffer[0]));
+ }
+
+ void unsubscribe(Http1AsyncDelegate delegate) {
+ synchronized(this) {
+ if (this.delegate == delegate) {
+ debug.log(Level.DEBUG, "Unsubscribed %s", delegate);
+ this.delegate = null;
+ }
+ }
+ }
+
+ // Callback: Consumer of ByteBufferReference
+ private void asyncReceive(ByteBuffer buf) {
+ debug.log(Level.DEBUG, "Putting %s bytes into the queue", buf.remaining());
+ received.addAndGet(buf.remaining());
+ queue.offer(buf);
+
+ // This callback is called from within the selector thread.
+ // Use an executor here to avoid doing the heavy lifting in the
+ // selector.
+ scheduler.deferOrSchedule(executor);
+ }
+
+ // Callback: Consumer of Throwable
+ void onReadError(Throwable ex) {
+ Http1AsyncDelegate delegate;
+ Throwable recorded;
+ debug.log(Level.DEBUG, "onError: %s", (Object) ex);
+ synchronized (this) {
+ delegate = this.delegate;
+ recorded = error;
+ if (recorded == null) {
+ // retry is set to true by HttpExchange when the connection is
+ // already connected, which means it's been retrieved from
+ // the pool.
+ if (retry && (ex instanceof IOException)) {
+ // could be either EOFException, or
+ // IOException("connection reset by peer), or
+ // SSLHandshakeException resulting from the server having
+ // closed the SSL session.
+ if (received.get() == 0) {
+ // If we receive such an exception before having
+ // received any byte, then in this case, we will
+ // throw ConnectionExpiredException
+ // to try & force a retry of the request.
+ retry = false;
+ ex = new ConnectionExpiredException(
+ "subscription is finished", ex);
+ }
+ }
+ error = ex;
+ }
+ final Throwable t = (recorded == null ? ex : recorded);
+ debug.log(Level.DEBUG, () -> "recorded " + t
+ + "\n\t delegate: " + delegate
+ + "\t\t queue.isEmpty: " + queue.isEmpty(), ex);
+ }
+ if (queue.isEmpty() || pendingDelegateRef.get() != null) {
+ // This callback is called from within the selector thread.
+ // Use an executor here to avoid doing the heavy lifting in the
+ // selector.
+ scheduler.deferOrSchedule(executor);
+ }
+ }
+
+ void stop() {
+ debug.log(Level.DEBUG, "stopping");
+ scheduler.stop();
+ delegate = null;
+ owner = null;
+ }
+
+ /**
+ * Returns the TubeSubscriber for reading from the connection flow.
+ * @return the TubeSubscriber for reading from the connection flow.
+ */
+ TubeSubscriber subscriber() {
+ return subscriber;
+ }
+
+ /**
+ * A simple tube subscriber for reading from the connection flow.
+ */
+ final class Http1TubeSubscriber implements TubeSubscriber {
+ volatile Flow.Subscription subscription;
+ volatile boolean completed;
+ volatile boolean dropped;
+
+ public void onSubscribe(Flow.Subscription subscription) {
+ // supports being called multiple time.
+ // doesn't cancel the previous subscription, since that is
+ // most probably the same as the new subscription.
+ assert this.subscription == null || dropped == false;
+ this.subscription = subscription;
+ dropped = false;
+ canRequestMore.set(true);
+ if (delegate != null) {
+ scheduler.deferOrSchedule(executor);
+ }
+ }
+
+ void requestMore() {
+ Flow.Subscription s = subscription;
+ if (s == null) return;
+ if (canRequestMore.compareAndSet(true, false)) {
+ if (!completed && !dropped) {
+ debug.log(Level.DEBUG,
+ "Http1TubeSubscriber: requesting one more from upstream");
+ s.request(1);
+ return;
+ }
+ }
+ debug.log(Level.DEBUG, "Http1TubeSubscriber: no need to request more");
+ }
+
+ @Override
+ public void onNext(List<ByteBuffer> item) {
+ canRequestMore.set(item.isEmpty());
+ for (ByteBuffer buffer : item) {
+ asyncReceive(buffer);
+ }
+ }
+
+ @Override
+ public void onError(Throwable throwable) {
+ onReadError(throwable);
+ completed = true;
+ }
+
+ @Override
+ public void onComplete() {
+ onReadError(new EOFException("EOF reached while reading"));
+ completed = true;
+ }
+
+ public void dropSubscription() {
+ debug.log(Level.DEBUG, "Http1TubeSubscriber: dropSubscription");
+ // we could probably set subscription to null here...
+ // then we might not need the 'dropped' boolean?
+ dropped = true;
+ }
+
+ }
+
+ // Drains the content of the queue into a single ByteBuffer.
+ // The scheduler must be permanently stopped before calling drain().
+ ByteBuffer drain(ByteBuffer initial) {
+ // Revisit: need to clean that up.
+ //
+ ByteBuffer b = initial = (initial == null ? Utils.EMPTY_BYTEBUFFER : initial);
+ assert scheduler.isStopped();
+
+ if (queue.isEmpty()) return b;
+
+ // sanity check: we shouldn't have queued the same
+ // buffer twice.
+ ByteBuffer[] qbb = queue.toArray(new ByteBuffer[queue.size()]);
+ assert java.util.stream.Stream.of(qbb)
+ .collect(Collectors.toSet())
+ .size() == qbb.length : debugQBB(qbb);
+
+ // compute the number of bytes in the queue, the number of bytes
+ // in the initial buffer
+ // TODO: will need revisiting - as it is not guaranteed that all
+ // data will fit in single BB!
+ int size = Utils.remaining(qbb, Integer.MAX_VALUE);
+ int remaining = b.remaining();
+ int free = b.capacity() - b.position() - remaining;
+ debug.log(Level.DEBUG,
+ "Flushing %s bytes from queue into initial buffer (remaining=%s, free=%s)",
+ size, remaining, free);
+
+ // check whether the initial buffer has enough space
+ if (size > free) {
+ debug.log(Level.DEBUG,
+ "Allocating new buffer for initial: %s", (size + remaining));
+ // allocates a new buffer and copy initial to it
+ b = ByteBuffer.allocate(size + remaining);
+ Utils.copy(initial, b);
+ assert b.position() == remaining;
+ b.flip();
+ assert b.position() == 0;
+ assert b.limit() == remaining;
+ assert b.remaining() == remaining;
+ }
+
+ // store position and limit
+ int pos = b.position();
+ int limit = b.limit();
+ assert limit - pos == remaining;
+ assert b.capacity() >= remaining + size
+ : "capacity: " + b.capacity()
+ + ", remaining: " + b.remaining()
+ + ", size: " + size;
+
+ // prepare to copy the content of the queue
+ b.position(limit);
+ b.limit(pos + remaining + size);
+ assert b.remaining() >= size :
+ "remaining: " + b.remaining() + ", size: " + size;
+
+ // copy the content of the queue
+ int count = 0;
+ for (int i=0; i<qbb.length; i++) {
+ ByteBuffer b2 = qbb[i];
+ int r = b2.remaining();
+ assert b.remaining() >= r : "need at least " + r + " only "
+ + b.remaining() + " available";
+ int copied = Utils.copy(b2, b);
+ assert copied == r : "copied="+copied+" available="+r;
+ assert b2.remaining() == 0;
+ count += copied;
+ }
+ assert count == size;
+ assert b.position() == pos + remaining + size :
+ "b.position="+b.position()+" != "+pos+"+"+remaining+"+"+size;
+
+ // reset limit and position
+ b.limit(limit+size);
+ b.position(pos);
+
+ // we can clear the refs
+ queue.clear();
+ final ByteBuffer bb = b;
+ debug.log(Level.DEBUG, () -> "Initial buffer now has " + bb.remaining()
+ + " pos=" + bb.position() + " limit=" + bb.limit());
+
+ return b;
+ }
+
+ private String debugQBB(ByteBuffer[] qbb) {
+ StringBuilder msg = new StringBuilder();
+ List<ByteBuffer> lbb = Arrays.asList(qbb);
+ Set<ByteBuffer> sbb = new HashSet<>(Arrays.asList(qbb));
+
+ int uniquebb = sbb.size();
+ msg.append("qbb: ").append(lbb.size())
+ .append(" (unique: ").append(uniquebb).append("), ")
+ .append("duplicates: ");
+ String sep = "";
+ for (ByteBuffer b : lbb) {
+ if (!sbb.remove(b)) {
+ msg.append(sep)
+ .append(String.valueOf(b))
+ .append("[remaining=")
+ .append(b.remaining())
+ .append(", position=")
+ .append(b.position())
+ .append(", capacity=")
+ .append(b.capacity())
+ .append("]");
+ sep = ", ";
+ }
+ }
+ return msg.toString();
+ }
+
+ volatile String dbgTag;
+ String dbgString() {
+ String tag = dbgTag;
+ if (tag == null) {
+ String flowTag = null;
+ Http1Exchange<?> exchg = owner;
+ Object flow = (exchg != null)
+ ? exchg.connection().getConnectionFlow()
+ : null;
+ flowTag = tag = flow == null ? null: (String.valueOf(flow));
+ if (flowTag != null) {
+ dbgTag = tag = flowTag + " Http1AsyncReceiver";
+ } else {
+ tag = "Http1AsyncReceiver";
+ }
+ }
+ return tag;
+ }
+}
--- a/src/jdk.incubator.httpclient/share/classes/jdk/incubator/http/Http1Exchange.java Sun Nov 05 17:05:57 2017 +0000
+++ b/src/jdk.incubator.httpclient/share/classes/jdk/incubator/http/Http1Exchange.java Sun Nov 05 17:32:13 2017 +0000
@@ -26,40 +26,127 @@
package jdk.incubator.http;
import java.io.IOException;
+import java.lang.System.Logger.Level;
import java.net.InetSocketAddress;
import jdk.incubator.http.HttpResponse.BodyHandler;
-import jdk.incubator.http.HttpResponse.BodyProcessor;
+import jdk.incubator.http.HttpResponse.BodySubscriber;
import java.nio.ByteBuffer;
+import java.util.Objects;
import java.util.concurrent.CompletableFuture;
-import java.util.Collections;
import java.util.LinkedList;
import java.util.List;
-import java.util.concurrent.CompletionException;
+import java.util.concurrent.ConcurrentLinkedDeque;
import java.util.concurrent.Executor;
+import java.util.concurrent.Flow;
+import jdk.incubator.http.internal.common.Demand;
import jdk.incubator.http.internal.common.Log;
-import jdk.incubator.http.internal.common.MinimalFuture;
+import jdk.incubator.http.internal.common.FlowTube;
+import jdk.incubator.http.internal.common.SequentialScheduler;
+import jdk.incubator.http.internal.common.SequentialScheduler.SynchronizedRestartableTask;
import jdk.incubator.http.internal.common.Utils;
+import static jdk.incubator.http.HttpClient.Version.HTTP_1_1;
/**
- * Encapsulates one HTTP/1.1 request/responseAsync exchange.
+ * Encapsulates one HTTP/1.1 request/response exchange.
*/
class Http1Exchange<T> extends ExchangeImpl<T> {
+ static final boolean DEBUG = Utils.DEBUG; // Revisit: temporary dev flag.
+ final System.Logger debug = Utils.getDebugLogger(this::dbgString, DEBUG);
+ private static final System.Logger DEBUG_LOGGER =
+ Utils.getDebugLogger("Http1Exchange"::toString, DEBUG);
+
final HttpRequestImpl request; // main request
- private final List<CompletableFuture<?>> operations; // used for cancel
final Http1Request requestAction;
private volatile Http1Response<T> response;
- // use to record possible cancellation raised before any operation
- // has been initiated.
- private IOException failed;
final HttpConnection connection;
final HttpClientImpl client;
final Executor executor;
- volatile ByteBuffer buffer; // used for receiving
+ private volatile ByteBuffer buffer; // used for receiving
+ private final Http1AsyncReceiver asyncReceiver;
+
+ /** Records a possible cancellation raised before any operation
+ * has been initiated, or an error received while sending the request. */
+ private Throwable failed;
+ private final List<CompletableFuture<?>> operations; // used for cancel
+
+ /** Must be held when operating on any internal state or data. */
+ private final Object lock = new Object();
+
+ /** Holds the outgoing data, either the headers or a request body part. Or
+ * an error from the request body publisher. At most there can be ~2 pieces
+ * of outgoing data ( onComplete|onError can be invoked without demand ).*/
+ final ConcurrentLinkedDeque<DataPair> outgoing = new ConcurrentLinkedDeque<>();
+
+ /** The write publisher, responsible for writing the complete request ( both
+ * headers and body ( if any ). */
+ private final Http1Publisher writePublisher = new Http1Publisher();
+
+ /** Completed when the header have been published, or there is an error */
+ private volatile CompletableFuture<ExchangeImpl<T>> headersSentCF = new CompletableFuture<>();
+ /** Completed when the body has been published, or there is an error */
+ private volatile CompletableFuture<ExchangeImpl<T>> bodySentCF = new CompletableFuture<>();
+
+ /** The subscriber to the request's body published. Maybe null. */
+ private volatile Http1BodySubscriber bodySubscriber;
+
+ enum State { INITIAL,
+ HEADERS,
+ BODY,
+ ERROR, // terminal state
+ COMPLETING,
+ COMPLETED } // terminal state
+
+ private State state = State.INITIAL;
+
+ /** A carrier for either data or an error. Used to carry data, and communicate
+ * errors from the request ( both headers and body ) to the exchange. */
+ static class DataPair {
+ Throwable throwable;
+ List<ByteBuffer> data;
+ DataPair(List<ByteBuffer> data, Throwable throwable){
+ this.data = data;
+ this.throwable = throwable;
+ }
+ @Override
+ public String toString() {
+ return "DataPair [data=" + data + ", throwable=" + throwable + "]";
+ }
+ }
+
+ /** An abstract supertype for HTTP/1.1 body subscribers. There are two
+ * concrete implementations: {@link Http1Request.StreamSubscriber}, and
+ * {@link Http1Request.FixedContentSubscriber}, for receiving chunked and
+ * fixed length bodies, respectively. */
+ static abstract class Http1BodySubscriber implements Flow.Subscriber<ByteBuffer> {
+ protected volatile Flow.Subscription subscription;
+ protected volatile boolean complete;
+
+ /** Final sentinel in the stream of request body. */
+ static final List<ByteBuffer> COMPLETED = List.of(ByteBuffer.allocate(0));
+
+ void request(long n) {
+ DEBUG_LOGGER.log(Level.DEBUG, () ->
+ "Http1BodySubscriber requesting " + n + ", from " + subscription);
+ subscription.request(n);
+ }
+
+ static Http1BodySubscriber completeSubscriber() {
+ return new Http1BodySubscriber() {
+ @Override public void onSubscribe(Flow.Subscription subscription) { error(); }
+ @Override public void onNext(ByteBuffer item) { error(); }
+ @Override public void onError(Throwable throwable) { error(); }
+ @Override public void onComplete() { error(); }
+ private void error() {
+ throw new InternalError("should not reach here");
+ }
+ };
+ }
+ }
@Override
public String toString() {
- return request.toString();
+ return "HTTP/1.1 " + request.toString();
}
HttpRequestImpl request() {
@@ -74,44 +161,157 @@
this.client = exchange.client();
this.executor = exchange.executor();
this.operations = new LinkedList<>();
- this.buffer = Utils.EMPTY_BYTEBUFFER;
+ operations.add(headersSentCF);
+ operations.add(bodySentCF);
+ this.buffer = Utils.EMPTY_BYTEBUFFER; // TODO: need to check left over data?
if (connection != null) {
this.connection = connection;
} else {
InetSocketAddress addr = request.getAddress(client);
- this.connection = HttpConnection.getConnection(addr, client, request);
+ this.connection = HttpConnection.getConnection(addr, client, request, HTTP_1_1);
}
- this.requestAction = new Http1Request(request, client, this.connection);
+ this.requestAction = new Http1Request(request, client, this);
+ this.asyncReceiver = new Http1AsyncReceiver(executor, this);
+ asyncReceiver.subscribe(new InitialErrorReceiver());
}
+ /** An initial receiver that handles no data, but cancels the request if
+ * it receives an error. Will be replaced when reading response body. */
+ final class InitialErrorReceiver implements Http1AsyncReceiver.Http1AsyncDelegate {
+ volatile AbstractSubscription s;
+ @Override
+ public boolean tryAsyncReceive(ByteBuffer ref) {
+ return false; // no data has been processed, leave it in the queue
+ }
+ @Override
+ public void onReadError(Throwable ex) {
+ cancelImpl(ex);
+ }
+
+ @Override
+ public void onSubscribe(AbstractSubscription s) {
+ this.s = s;
+ }
+
+ public AbstractSubscription subscription() {
+ return s;
+ }
+ }
+
+ @Override
HttpConnection connection() {
return connection;
}
+ private void connectFlows(HttpConnection connection) {
+ FlowTube tube = connection.getConnectionFlow();
+ debug.log(Level.DEBUG, "%s connecting flows", tube);
+
+ // Connect the flow to our Http1TubeSubscriber:
+ // asyncReceiver.subscriber().
+ tube.connectFlows(writePublisher,
+ asyncReceiver.subscriber());
+ }
@Override
- T readBody(BodyHandler<T> handler, boolean returnConnectionToPool)
- throws IOException
- {
- BodyProcessor<T> processor = handler.apply(response.responseCode(),
- response.responseHeaders());
- CompletableFuture<T> bodyCF = response.readBody(processor,
- returnConnectionToPool,
- this::executeInline);
- try {
- return bodyCF.join();
- } catch (CompletionException e) {
- throw Utils.getIOException(e);
+ CompletableFuture<ExchangeImpl<T>> sendHeadersAsync() {
+ // create the response before sending the request headers, so that
+ // the response can set the appropriate receivers.
+ debug.log(Level.DEBUG, "Sending headers only");
+ if (response == null) {
+ response = new Http1Response<>(connection, this, asyncReceiver);
}
+
+ debug.log(Level.DEBUG, "response created in advance");
+ // If the first attempt to read something triggers EOF, or
+ // IOException("channel reset by peer"), we're going to retry.
+ // Instruct the asyncReceiver to throw ConnectionExpiredException
+ // to force a retry.
+ asyncReceiver.setRetryOnError(true);
+
+ CompletableFuture<Void> connectCF;
+ if (!connection.connected()) {
+ debug.log(Level.DEBUG, "initiating connect async");
+ connectCF = connection.connectAsync();
+ synchronized (lock) {
+ operations.add(connectCF);
+ }
+ } else {
+ connectCF = new CompletableFuture<>();
+ connectCF.complete(null);
+ }
+
+ return connectCF
+ .thenCompose(unused -> {
+ CompletableFuture<Void> cf = new CompletableFuture<>();
+ try {
+ connectFlows(connection);
+
+ debug.log(Level.DEBUG, "requestAction.headers");
+ List<ByteBuffer> data = requestAction.headers();
+ synchronized (lock) {
+ state = State.HEADERS;
+ }
+ debug.log(Level.DEBUG, "setting outgoing with headers");
+ assert outgoing.isEmpty() : "Unexpected outgoing:" + outgoing;
+ appendToOutgoing(data);
+ cf.complete(null);
+ return cf;
+ } catch (Throwable t) {
+ debug.log(Level.DEBUG, "Failed to send headers: %s", t);
+ connection.close();
+ cf.completeExceptionally(t);
+ return cf;
+ } })
+ .thenCompose(unused -> headersSentCF);
}
- private void executeInline(Runnable r) {
- r.run();
+ @Override
+ CompletableFuture<ExchangeImpl<T>> sendBodyAsync() {
+ assert headersSentCF.isDone();
+ try {
+ bodySubscriber = requestAction.continueRequest();
+ if (bodySubscriber == null) {
+ bodySubscriber = Http1BodySubscriber.completeSubscriber();
+ appendToOutgoing(Http1BodySubscriber.COMPLETED);
+ } else {
+ bodySubscriber.request(1); // start
+ }
+ } catch (Throwable t) {
+ connection.close();
+ bodySentCF.completeExceptionally(t);
+ }
+ return bodySentCF;
}
- synchronized ByteBuffer getBuffer() {
- return buffer;
+ @Override
+ CompletableFuture<Response> getResponseAsync(Executor executor) {
+ CompletableFuture<Response> cf = response.readHeadersAsync(executor);
+ Throwable cause;
+ synchronized (lock) {
+ operations.add(cf);
+ cause = failed;
+ failed = null;
+ }
+
+ if (cause != null) {
+ Log.logTrace("Http1Exchange: request [{0}/timeout={1}ms]"
+ + "\n\tCompleting exceptionally with {2}\n",
+ request.uri(),
+ request.timeout().isPresent() ?
+ // calling duration.toMillis() can throw an exception.
+ // this is just debugging, we don't care if it overflows.
+ (request.timeout().get().getSeconds() * 1000
+ + request.timeout().get().getNano() / 1000000) : -1,
+ cause);
+ boolean acknowledged = cf.completeExceptionally(cause);
+ debug.log(Level.DEBUG,
+ () -> acknowledged
+ ? ("completed response with " + cause)
+ : ("response already completed, ignoring " + cause));
+ }
+ return cf;
}
@Override
@@ -119,53 +319,31 @@
boolean returnConnectionToPool,
Executor executor)
{
- BodyProcessor<T> processor = handler.apply(response.responseCode(),
- response.responseHeaders());
- CompletableFuture<T> bodyCF = response.readBody(processor,
+ BodySubscriber<T> bs = handler.apply(response.responseCode(),
+ response.responseHeaders());
+ CompletableFuture<T> bodyCF = response.readBody(bs,
returnConnectionToPool,
executor);
return bodyCF;
}
- @Override
- void sendHeadersOnly() throws IOException, InterruptedException {
- try {
- if (!connection.connected()) {
- connection.connect();
- }
- requestAction.sendHeadersOnly();
- } catch (Throwable e) {
- connection.close();
- throw e;
+ ByteBuffer getBuffer() {
+ synchronized (lock) {
+ asyncReceiver.stop();
+ this.buffer = asyncReceiver.drain(this.buffer);
+ return this.buffer;
}
}
- @Override
- void sendBody() throws IOException {
- try {
- requestAction.continueRequest();
- } catch (Throwable e) {
- connection.close();
- throw e;
- }
+ void released() {
+ Http1Response<T> resp = this.response;
+ if (resp != null) resp.completed();
+ asyncReceiver.clear();
}
- @Override
- Response getResponse() throws IOException {
- try {
- response = new Http1Response<>(connection, this);
- response.readHeaders();
- Response r = response.response();
- buffer = response.getBuffer();
- return r;
- } catch (Throwable t) {
- connection.close();
- throw t;
- }
- }
-
- private void closeConnection() {
- connection.close();
+ void completed() {
+ Http1Response<T> resp = this.response;
+ if (resp != null) resp.completed();
}
/**
@@ -174,7 +352,7 @@
*/
@Override
void cancel() {
- cancel(new IOException("Request cancelled"));
+ cancelImpl(new IOException("Request cancelled"));
}
/**
@@ -182,66 +360,255 @@
* If not it closes the connection and completes all pending operations
*/
@Override
- synchronized void cancel(IOException cause) {
- if (requestAction != null && requestAction.finished()
- && response != null && response.finished()) {
- return;
- }
- connection.close();
+ void cancel(IOException cause) {
+ cancelImpl(cause);
+ }
+
+ private void cancelImpl(Throwable cause) {
+ LinkedList<CompletableFuture<?>> toComplete = null;
int count = 0;
- if (operations.isEmpty()) {
- failed = cause;
- Log.logTrace("Http1Exchange: request [{0}/timeout={1}ms] no pending operation."
- + "\n\tCan''t cancel yet with {2}",
- request.uri(),
- request.duration() == null ? -1 :
- // calling duration.toMillis() can throw an exception.
- // this is just debugging, we don't care if it overflows.
- (request.duration().getSeconds() * 1000
- + request.duration().getNano() / 1000000),
- cause);
- } else {
- for (CompletableFuture<?> cf : operations) {
- cf.completeExceptionally(cause);
- count++;
+ synchronized (lock) {
+ if (failed == null)
+ failed = cause;
+ if (requestAction != null && requestAction.finished()
+ && response != null && response.finished()) {
+ return;
+ }
+ connection.close(); // TODO: ensure non-blocking if holding the lock
+ writePublisher.writeScheduler.stop();
+ if (operations.isEmpty()) {
+ Log.logTrace("Http1Exchange: request [{0}/timeout={1}ms] no pending operation."
+ + "\n\tCan''t cancel yet with {2}",
+ request.uri(),
+ request.timeout().isPresent() ?
+ // calling duration.toMillis() can throw an exception.
+ // this is just debugging, we don't care if it overflows.
+ (request.timeout().get().getSeconds() * 1000
+ + request.timeout().get().getNano() / 1000000) : -1,
+ cause);
+ } else {
+ for (CompletableFuture<?> cf : operations) {
+ if (!cf.isDone()) {
+ if (toComplete == null) toComplete = new LinkedList<>();
+ toComplete.add(cf);
+ count++;
+ }
+ }
+ operations.clear();
}
}
Log.logError("Http1Exchange.cancel: count=" + count);
+ if (toComplete != null) {
+ // We might be in the selector thread in case of timeout, when
+ // the SelectorManager calls purgeTimeoutsAndReturnNextDeadline()
+ // There may or may not be other places that reach here
+ // from the SelectorManager thread, so just make sure we
+ // don't complete any CF from within the selector manager
+ // thread.
+ Executor exec = client.isSelectorThread()
+ ? executor
+ : this::runInline;
+ while (!toComplete.isEmpty()) {
+ CompletableFuture<?> cf = toComplete.poll();
+ exec.execute(() -> {
+ if (cf.completeExceptionally(cause)) {
+ debug.log(Level.DEBUG, "completed cf with %s",
+ (Object) cause);
+ }
+ });
+ }
+ }
+ }
+
+ private void runInline(Runnable run) {
+ assert !client.isSelectorThread();
+ run.run();
}
- CompletableFuture<Response> getResponseAsyncImpl(Executor executor) {
- return MinimalFuture.supply( () -> {
- response = new Http1Response<>(connection, Http1Exchange.this);
- response.readHeaders();
- Response r = response.response();
- buffer = response.getBuffer();
- return r;
- }, executor);
+ /** Returns true if this exchange was canceled. */
+ boolean isCanceled() {
+ synchronized (lock) {
+ return failed != null;
+ }
+ }
+
+ /** Returns the cause for which this exchange was canceled, if available. */
+ Throwable getCancelCause() {
+ synchronized (lock) {
+ return failed;
+ }
+ }
+
+ /** Convenience for {@link #appendToOutgoing(DataPair)}, with just a Throwable. */
+ void appendToOutgoing(Throwable throwable) {
+ appendToOutgoing(new DataPair(null, throwable));
+ }
+
+ /** Convenience for {@link #appendToOutgoing(DataPair)}, with just data. */
+ void appendToOutgoing(List<ByteBuffer> item) {
+ appendToOutgoing(new DataPair(item, null));
+ }
+
+ private void appendToOutgoing(DataPair dp) {
+ debug.log(Level.DEBUG, "appending to outgoing " + dp);
+ outgoing.add(dp);
+ writePublisher.writeScheduler.runOrSchedule();
+ }
+
+ /** Tells whether, or not, there is any outgoing data that can be published,
+ * or if there is an error. */
+ private boolean hasOutgoing() {
+ return !outgoing.isEmpty();
}
- @Override
- CompletableFuture<Response> getResponseAsync(Executor executor) {
- CompletableFuture<Response> cf =
- connection.whenReceivingResponse()
- .thenCompose((v) -> getResponseAsyncImpl(executor));
- IOException cause;
- synchronized(this) {
- operations.add(cf);
- cause = failed;
- failed = null;
+ // Invoked only by the publisher
+ // ALL tasks should execute off the Selector-Manager thread
+ /** Returns the next portion of the HTTP request, or the error. */
+ private DataPair getOutgoing() {
+ final Executor exec = client.theExecutor();
+ final DataPair dp = outgoing.pollFirst();
+
+ if (dp == null) // publisher has not published anything yet
+ return null;
+
+ synchronized (lock) {
+ if (dp.throwable != null) {
+ state = State.ERROR;
+ exec.execute(() -> {
+ connection.close();
+ headersSentCF.completeExceptionally(dp.throwable);
+ bodySentCF.completeExceptionally(dp.throwable);
+ });
+ return dp;
+ }
+
+ switch (state) {
+ case HEADERS:
+ state = State.BODY;
+ // completeAsync, since dependent tasks should run in another thread
+ debug.log(Level.DEBUG, "initiating completion of headersSentCF");
+ headersSentCF.completeAsync(() -> this, exec);
+ break;
+ case BODY:
+ if (dp.data == Http1BodySubscriber.COMPLETED) {
+ state = State.COMPLETING;
+ debug.log(Level.DEBUG, "initiating completion of bodySentCF");
+ bodySentCF.completeAsync(() -> this, exec);
+ } else {
+ debug.log(Level.DEBUG, "requesting more body from the subscriber");
+ exec.execute(() -> bodySubscriber.request(1));
+ }
+ break;
+ case INITIAL:
+ case ERROR:
+ case COMPLETING:
+ case COMPLETED:
+ default:
+ assert false : "Unexpected state:" + state;
+ }
+
+ return dp;
+ }
+ }
+
+ /** A Publisher of HTTP/1.1 headers and request body. */
+ final class Http1Publisher implements HttpConnection.HttpPublisher {
+
+ final System.Logger debug = Utils.getDebugLogger(this::dbgString);
+ volatile Flow.Subscriber<? super List<ByteBuffer>> subscriber;
+ volatile boolean cancelled;
+ final Http1WriteSubscription subscription = new Http1WriteSubscription();
+ final Demand demand = new Demand();
+ final SequentialScheduler writeScheduler = new SequentialScheduler(
+ new SynchronizedRestartableTask(new WriteTask()));
+
+ @Override
+ public void subscribe(Flow.Subscriber<? super List<ByteBuffer>> s) {
+ assert state == State.INITIAL;
+ Objects.requireNonNull(s);
+ assert subscriber == null;
+
+ subscriber = s;
+ debug.log(Level.DEBUG, "got subscriber: %s", s);
+ s.onSubscribe(subscription);
}
- if (cause != null) {
- Log.logTrace("Http1Exchange: request [{0}/timeout={1}ms]"
- + "\n\tCompleting exceptionally with {2}\n",
- request.uri(),
- request.duration() == null ? -1 :
- // calling duration.toMillis() can throw an exception.
- // this is just debugging, we don't care if it overflows.
- (request.duration().getSeconds() * 1000
- + request.duration().getNano() / 1000000),
- cause);
- cf.completeExceptionally(cause);
+
+ volatile String dbgTag;
+ String dbgString() {
+ String tag = dbgTag;
+ Object flow = connection.getConnectionFlow();
+ if (tag == null && flow != null) {
+ dbgTag = tag = "Http1Publisher(" + flow + ")";
+ } else if (tag == null) {
+ tag = "Http1Publisher(?)";
+ }
+ return tag;
}
- return cf;
+
+ final class WriteTask implements Runnable {
+ @Override
+ public void run() {
+ assert state != State.COMPLETED : "Unexpected state:" + state;
+ debug.log(Level.DEBUG, "WriteTask");
+ if (subscriber == null) {
+ debug.log(Level.DEBUG, "no subscriber yet");
+ return;
+ }
+ debug.log(Level.DEBUG, () -> "hasOutgoing = " + hasOutgoing());
+ if (hasOutgoing() && demand.tryDecrement()) {
+ DataPair dp = getOutgoing();
+
+ if (dp.throwable != null) {
+ debug.log(Level.DEBUG, "onError");
+ // Do not call the subscriber's onError, it is not required.
+ writeScheduler.stop();
+ } else {
+ List<ByteBuffer> data = dp.data;
+ if (data == Http1BodySubscriber.COMPLETED) {
+ synchronized (lock) {
+ assert state == State.COMPLETING : "Unexpected state:" + state;
+ state = State.COMPLETED;
+ }
+ debug.log(Level.DEBUG,
+ "completed, stopping %s", writeScheduler);
+ writeScheduler.stop();
+ // Do nothing more. Just do not publish anything further.
+ // The next Subscriber will eventually take over.
+
+ } else {
+ debug.log(Level.DEBUG, () ->
+ "onNext with " + Utils.remaining(data) + " bytes");
+ subscriber.onNext(data);
+ }
+ }
+ }
+ }
+ }
+
+ final class Http1WriteSubscription implements Flow.Subscription {
+
+ @Override
+ public void request(long n) {
+ if (cancelled)
+ return; //no-op
+ demand.increase(n);
+ debug.log(Level.DEBUG,
+ "subscription request(%d), demand=%s", n, demand);
+ writeScheduler.deferOrSchedule(client.theExecutor());
+ }
+
+ @Override
+ public void cancel() {
+ debug.log(Level.DEBUG, "subscription cancelled");
+ if (cancelled)
+ return; //no-op
+ cancelled = true;
+ writeScheduler.stop();
+ }
+ }
+ }
+
+ String dbgString() {
+ return "Http1Exchange";
}
}
--- /dev/null Thu Jan 01 00:00:00 1970 +0000
+++ b/src/jdk.incubator.httpclient/share/classes/jdk/incubator/http/Http1HeaderParser.java Sun Nov 05 17:32:13 2017 +0000
@@ -0,0 +1,257 @@
+/*
+ * Copyright (c) 2017, Oracle and/or its affiliates. All rights reserved.
+ * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER.
+ *
+ * This code is free software; you can redistribute it and/or modify it
+ * under the terms of the GNU General Public License version 2 only, as
+ * published by the Free Software Foundation. Oracle designates this
+ * particular file as subject to the "Classpath" exception as provided
+ * by Oracle in the LICENSE file that accompanied this code.
+ *
+ * This code is distributed in the hope that it will be useful, but WITHOUT
+ * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or
+ * FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License
+ * version 2 for more details (a copy is included in the LICENSE file that
+ * accompanied this code).
+ *
+ * You should have received a copy of the GNU General Public License version
+ * 2 along with this work; if not, write to the Free Software Foundation,
+ * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA.
+ *
+ * Please contact Oracle, 500 Oracle Parkway, Redwood Shores, CA 94065 USA
+ * or visit www.oracle.com if you need additional information or have any
+ * questions.
+ */
+
+package jdk.incubator.http;
+
+import java.net.ProtocolException;
+import java.nio.ByteBuffer;
+import java.util.ArrayList;
+import java.util.HashMap;
+import java.util.List;
+import java.util.Locale;
+import java.util.Map;
+import static java.lang.String.format;
+import static java.util.Objects.requireNonNull;
+
+class Http1HeaderParser {
+
+ private static final char CR = '\r';
+ private static final char LF = '\n';
+ private static final char HT = '\t';
+ private static final char SP = ' ';
+
+ private StringBuilder sb = new StringBuilder();
+ private String statusLine;
+ private int responseCode;
+ private HttpHeaders headers;
+ private Map<String,List<String>> privateMap = new HashMap<>();
+
+ enum State { STATUS_LINE,
+ STATUS_LINE_FOUND_CR,
+ STATUS_LINE_END,
+ STATUS_LINE_END_CR,
+ HEADER,
+ HEADER_FOUND_CR,
+ HEADER_FOUND_LF,
+ HEADER_FOUND_CR_LF,
+ HEADER_FOUND_CR_LF_CR,
+ FINISHED }
+
+ private State state = State.STATUS_LINE;
+
+ /** Returns the status-line. */
+ String statusLine() { return statusLine; }
+
+ /** Returns the response code. */
+ int responseCode() { return responseCode; }
+
+ /** Returns the headers, possibly empty. */
+ HttpHeaders headers() { assert state == State.FINISHED; return headers; }
+
+ /**
+ * Parses HTTP/1.X status-line and headers from the given bytes. Must be
+ * called successive times, with additional data, until returns true.
+ *
+ * All given ByteBuffers will be consumed, until ( possibly ) the last one
+ * ( when true is returned ), which may not be fully consumed.
+ *
+ * @param input the ( partial ) header data
+ * @return true iff the end of the headers block has been reached
+ */
+ boolean parse(ByteBuffer input) throws ProtocolException {
+ requireNonNull(input, "null input");
+
+ while (input.hasRemaining() && state != State.FINISHED) {
+ switch (state) {
+ case STATUS_LINE:
+ readResumeStatusLine(input);
+ break;
+ case STATUS_LINE_FOUND_CR:
+ readStatusLineFeed(input);
+ break;
+ case STATUS_LINE_END:
+ maybeStartHeaders(input);
+ break;
+ case STATUS_LINE_END_CR:
+ maybeEndHeaders(input);
+ break;
+ case HEADER:
+ readResumeHeader(input);
+ break;
+ // fallthrough
+ case HEADER_FOUND_CR:
+ case HEADER_FOUND_LF:
+ resumeOrLF(input);
+ break;
+ case HEADER_FOUND_CR_LF:
+ resumeOrSecondCR(input);
+ break;
+ case HEADER_FOUND_CR_LF_CR:
+ resumeOrEndHeaders(input);
+ break;
+ default:
+ throw new InternalError(
+ "Unexpected state: " + String.valueOf(state));
+ }
+ }
+
+ return state == State.FINISHED;
+ }
+
+ private void readResumeStatusLine(ByteBuffer input) {
+ char c = 0;
+ while (input.hasRemaining() && (c =(char)input.get()) != CR) {
+ sb.append(c);
+ }
+
+ if (c == CR) {
+ state = State.STATUS_LINE_FOUND_CR;
+ }
+ }
+
+ private void readStatusLineFeed(ByteBuffer input) throws ProtocolException {
+ char c = (char)input.get();
+ if (c != LF) {
+ throw protocolException("Bad trailing char, \"%s\", when parsing status-line, \"%s\"",
+ c, sb.toString());
+ }
+
+ statusLine = sb.toString();
+ sb = new StringBuilder();
+ if (!statusLine.startsWith("HTTP/1.")) {
+ throw protocolException("Invalid status line: \"%s\"", statusLine);
+ }
+ if (statusLine.length() < 12) {
+ throw protocolException("Invalid status line: \"%s\"", statusLine);
+ }
+ responseCode = Integer.parseInt(statusLine.substring(9, 12));
+
+ state = State.STATUS_LINE_END;
+ }
+
+ private void maybeStartHeaders(ByteBuffer input) {
+ assert state == State.STATUS_LINE_END;
+ assert sb.length() == 0;
+ char c = (char)input.get();
+ if (c == CR) {
+ state = State.STATUS_LINE_END_CR;
+ } else {
+ sb.append(c);
+ state = State.HEADER;
+ }
+ }
+
+ private void maybeEndHeaders(ByteBuffer input) throws ProtocolException {
+ assert state == State.STATUS_LINE_END_CR;
+ assert sb.length() == 0;
+ char c = (char)input.get();
+ if (c == LF) {
+ headers = ImmutableHeaders.of(privateMap);
+ privateMap = null;
+ state = State.FINISHED; // no headers
+ } else {
+ throw protocolException("Unexpected \"%s\", after status-line CR", c);
+ }
+ }
+
+ private void readResumeHeader(ByteBuffer input) {
+ assert state == State.HEADER;
+ assert input.hasRemaining();
+ while (input.hasRemaining()) {
+ char c = (char)input.get();
+ if (c == CR) {
+ state = State.HEADER_FOUND_CR;
+ break;
+ } else if (c == LF) {
+ state = State.HEADER_FOUND_LF;
+ break;
+ }
+
+ if (c == HT)
+ c = SP;
+ sb.append(c);
+ }
+ }
+
+ private void addHeaderFromString(String headerString) {
+ assert sb.length() == 0;
+ int idx = headerString.indexOf(':');
+ if (idx == -1)
+ return;
+ String name = headerString.substring(0, idx).trim();
+ if (name.isEmpty())
+ return;
+ String value = headerString.substring(idx + 1, headerString.length()).trim();
+
+ privateMap.computeIfAbsent(name.toLowerCase(Locale.US),
+ k -> new ArrayList<>()).add(value);
+ }
+
+ private void resumeOrLF(ByteBuffer input) {
+ assert state == State.HEADER_FOUND_CR || state == State.HEADER_FOUND_LF;
+ char c = (char)input.get();
+ if (c == LF && state == State.HEADER_FOUND_CR) {
+ String headerString = sb.toString();
+ sb = new StringBuilder();
+ addHeaderFromString(headerString);
+ state = State.HEADER_FOUND_CR_LF;
+ } else if (c == SP || c == HT) {
+ sb.append(SP); // parity with MessageHeaders
+ state = State.HEADER;
+ } else {
+ sb = new StringBuilder();
+ sb.append(c);
+ state = State.HEADER;
+ }
+ }
+
+ private void resumeOrSecondCR(ByteBuffer input) {
+ assert state == State.HEADER_FOUND_CR_LF;
+ assert sb.length() == 0;
+ char c = (char)input.get();
+ if (c == CR) {
+ state = State.HEADER_FOUND_CR_LF_CR;
+ } else {
+ sb.append(c);
+ state = State.HEADER;
+ }
+ }
+
+ private void resumeOrEndHeaders(ByteBuffer input) throws ProtocolException {
+ assert state == State.HEADER_FOUND_CR_LF_CR;
+ char c = (char)input.get();
+ if (c == LF) {
+ state = State.FINISHED;
+ headers = ImmutableHeaders.of(privateMap);
+ privateMap = null;
+ } else {
+ throw protocolException("Unexpected \"%s\", after CR LF CR", c);
+ }
+ }
+
+ private ProtocolException protocolException(String format, Object... args) {
+ return new ProtocolException(format(format, args));
+ }
+}
--- a/src/jdk.incubator.httpclient/share/classes/jdk/incubator/http/Http1Request.java Sun Nov 05 17:05:57 2017 +0000
+++ b/src/jdk.incubator.httpclient/share/classes/jdk/incubator/http/Http1Request.java Sun Nov 05 17:32:13 2017 +0000
@@ -1,5 +1,5 @@
/*
- * Copyright (c) 2015, 2016, Oracle and/or its affiliates. All rights reserved.
+ * Copyright (c) 2015, 2017, Oracle and/or its affiliates. All rights reserved.
* DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER.
*
* This code is free software; you can redistribute it and/or modify it
@@ -26,97 +26,76 @@
package jdk.incubator.http;
import java.io.IOException;
+import java.lang.System.Logger.Level;
import java.net.URI;
import java.nio.ByteBuffer;
+import java.util.ArrayList;
import java.util.List;
import java.util.Map;
-import java.util.Set;
import java.net.InetSocketAddress;
-import jdk.incubator.http.HttpConnection.Mode;
-import java.nio.charset.StandardCharsets;
-import static java.nio.charset.StandardCharsets.US_ASCII;
-import java.util.concurrent.CompletableFuture;
-import java.util.concurrent.CompletionException;
+import java.util.Objects;
import java.util.concurrent.Flow;
+import jdk.incubator.http.Http1Exchange.Http1BodySubscriber;
import jdk.incubator.http.internal.common.HttpHeadersImpl;
import jdk.incubator.http.internal.common.Log;
-import jdk.incubator.http.internal.common.MinimalFuture;
import jdk.incubator.http.internal.common.Utils;
+import static java.nio.charset.StandardCharsets.US_ASCII;
+
/**
- * A HTTP/1.1 request.
- *
- * send() -> Writes the request + body to the given channel, in one blocking
- * operation.
+ * An HTTP/1.1 request.
*/
class Http1Request {
- final HttpClientImpl client;
- final HttpRequestImpl request;
- final HttpConnection chan;
- // Multiple buffers are used to hold different parts of request
- // See line 206 and below for description
- final ByteBuffer[] buffers;
- final HttpRequest.BodyProcessor requestProc;
- final HttpHeaders userHeaders;
- final HttpHeadersImpl systemHeaders;
- boolean streaming;
- long contentLength;
- final CompletableFuture<Void> cf;
+ private final HttpClientImpl client;
+ private final HttpRequestImpl request;
+ private final Http1Exchange<?> http1Exchange;
+ private final HttpConnection connection;
+ private final HttpRequest.BodyPublisher requestPublisher;
+ private final HttpHeaders userHeaders;
+ private final HttpHeadersImpl systemHeaders;
+ private volatile boolean streaming;
+ private volatile long contentLength;
- Http1Request(HttpRequestImpl request, HttpClientImpl client, HttpConnection connection)
+ Http1Request(HttpRequestImpl request,
+ HttpClientImpl client,
+ Http1Exchange<?> http1Exchange)
throws IOException
{
this.client = client;
this.request = request;
- this.chan = connection;
- buffers = new ByteBuffer[5]; // TODO: check
- this.requestProc = request.requestProcessor;
+ this.http1Exchange = http1Exchange;
+ this.connection = http1Exchange.connection();
+ this.requestPublisher = request.requestPublisher; // may be null
this.userHeaders = request.getUserHeaders();
this.systemHeaders = request.getSystemHeaders();
- this.cf = new MinimalFuture<>();
- }
-
- private void logHeaders() throws IOException {
- StringBuilder sb = new StringBuilder(256);
- sb.append("REQUEST HEADERS:\n");
- Log.dumpHeaders(sb, " ", systemHeaders);
- Log.dumpHeaders(sb, " ", userHeaders);
- Log.logHeaders(sb.toString());
- }
-
- private void dummy(long x) {
- // not used in this class
}
- private void collectHeaders0() throws IOException {
+ private void logHeaders(String completeHeaders) {
if (Log.headers()) {
- logHeaders();
+ //StringBuilder sb = new StringBuilder(256);
+ //sb.append("REQUEST HEADERS:\n");
+ //Log.dumpHeaders(sb, " ", systemHeaders);
+ //Log.dumpHeaders(sb, " ", userHeaders);
+ //Log.logHeaders(sb.toString());
+
+ String s = completeHeaders.replaceAll("\r\n", "\n");
+ Log.logHeaders("REQUEST HEADERS:\n" + s);
}
- StringBuilder sb = new StringBuilder(256);
- collectHeaders1(sb, request, systemHeaders);
- collectHeaders1(sb, request, userHeaders);
- sb.append("\r\n");
- String headers = sb.toString();
- buffers[1] = ByteBuffer.wrap(headers.getBytes(StandardCharsets.US_ASCII));
}
- private void collectHeaders1(StringBuilder sb,
- HttpRequestImpl request,
- HttpHeaders headers)
- throws IOException
- {
- Map<String,List<String>> h = headers.map();
- Set<Map.Entry<String,List<String>>> entries = h.entrySet();
+ private void collectHeaders0(StringBuilder sb) {
+ collectHeaders1(sb, systemHeaders);
+ collectHeaders1(sb, userHeaders);
+ sb.append("\r\n");
+ }
- for (Map.Entry<String,List<String>> entry : entries) {
+ private void collectHeaders1(StringBuilder sb, HttpHeaders headers) {
+ for (Map.Entry<String,List<String>> entry : headers.map().entrySet()) {
String key = entry.getKey();
List<String> values = entry.getValue();
for (String value : values) {
- sb.append(key)
- .append(": ")
- .append(value)
- .append("\r\n");
+ sb.append(key).append(": ").append(value).append("\r\n");
}
}
}
@@ -182,24 +161,6 @@
return uri == null? authorityString(request.authority()) : uri.toString();
}
- void sendHeadersOnly() throws IOException {
- collectHeaders();
- chan.write(buffers, 0, 2);
- }
-
- void sendRequest() throws IOException {
- collectHeaders();
- chan.configureMode(Mode.BLOCKING);
- if (contentLength == 0) {
- chan.write(buffers, 0, 2);
- } else if (contentLength > 0) {
- writeFixedContent(true);
- } else {
- writeStreamedContent(true);
- }
- setFinished();
- }
-
private boolean finished;
synchronized boolean finished() {
@@ -210,7 +171,7 @@
finished = true;
}
- private void collectHeaders() throws IOException {
+ List<ByteBuffer> headers() {
if (Log.requests() && request != null) {
Log.logRequest(request.toString());
}
@@ -220,250 +181,183 @@
.append(' ')
.append(uriString)
.append(" HTTP/1.1\r\n");
- String cmd = sb.toString();
- buffers[0] = ByteBuffer.wrap(cmd.getBytes(StandardCharsets.US_ASCII));
URI uri = request.uri();
if (uri != null) {
systemHeaders.setHeader("Host", hostString());
}
- if (request == null) {
- // this is not a user request. No content
+ if (request == null || requestPublisher == null) {
+ // Not a user request, or maybe a method, e.g. GET, with no body.
contentLength = 0;
} else {
- contentLength = requestProc.contentLength();
+ contentLength = requestPublisher.contentLength();
}
if (contentLength == 0) {
systemHeaders.setHeader("Content-Length", "0");
- collectHeaders0();
} else if (contentLength > 0) {
- /* [0] request line [1] headers [2] body */
- systemHeaders.setHeader("Content-Length",
- Integer.toString((int) contentLength));
+ systemHeaders.setHeader("Content-Length", Long.toString(contentLength));
streaming = false;
- collectHeaders0();
- buffers[2] = getBuffer();
} else {
- /* Chunked:
- *
- * [0] request line [1] headers [2] chunk header [3] chunk data [4]
- * final chunk header and trailing CRLF of previous chunks
- *
- * 2,3,4 used repeatedly */
streaming = true;
systemHeaders.setHeader("Transfer-encoding", "chunked");
- collectHeaders0();
- buffers[3] = getBuffer();
}
+ collectHeaders0(sb);
+ String hs = sb.toString();
+ logHeaders(hs);
+ ByteBuffer b = ByteBuffer.wrap(hs.getBytes(US_ASCII));
+ return List.of(b);
}
- private ByteBuffer getBuffer() {
- return ByteBuffer.allocate(Utils.BUFSIZE);
+ Http1BodySubscriber continueRequest() {
+ Http1BodySubscriber subscriber;
+ if (streaming) {
+ subscriber = new StreamSubscriber();
+ requestPublisher.subscribe(subscriber);
+ } else {
+ if (contentLength == 0)
+ return null;
+
+ subscriber = new FixedContentSubscriber();
+ requestPublisher.subscribe(subscriber);
+ }
+ return subscriber;
}
- // The following two methods used by Http1Exchange to handle expect continue
-
- void continueRequest() throws IOException {
- if (streaming) {
- writeStreamedContent(false);
- } else {
- writeFixedContent(false);
- }
- setFinished();
- }
-
- class StreamSubscriber implements Flow.Subscriber<ByteBuffer> {
- volatile Flow.Subscription subscription;
- volatile boolean includeHeaders;
-
- StreamSubscriber(boolean includeHeaders) {
- this.includeHeaders = includeHeaders;
- }
+ class StreamSubscriber extends Http1BodySubscriber {
@Override
public void onSubscribe(Flow.Subscription subscription) {
if (this.subscription != null) {
- throw new IllegalStateException("already subscribed");
+ Throwable t = new IllegalStateException("already subscribed");
+ http1Exchange.appendToOutgoing(t);
+ } else {
+ this.subscription = subscription;
}
- this.subscription = subscription;
- subscription.request(1);
}
@Override
public void onNext(ByteBuffer item) {
- int startbuf, nbufs;
-
- if (cf.isDone()) {
- throw new IllegalStateException("subscription already completed");
- }
-
- if (includeHeaders) {
- startbuf = 0;
- nbufs = 5;
+ Objects.requireNonNull(item);
+ if (complete) {
+ Throwable t = new IllegalStateException("subscription already completed");
+ http1Exchange.appendToOutgoing(t);
} else {
- startbuf = 2;
- nbufs = 3;
- }
- int chunklen = item.remaining();
- buffers[3] = item;
- buffers[2] = getHeader(chunklen);
- buffers[4] = CRLF_BUFFER();
- try {
- chan.write(buffers, startbuf, nbufs);
- } catch (IOException e) {
- subscription.cancel();
- cf.completeExceptionally(e);
- }
- includeHeaders = false;
- subscription.request(1);
- }
-
- @Override
- public void onError(Throwable throwable) {
- if (cf.isDone()) {
- return;
- }
- subscription.cancel();
- cf.completeExceptionally(throwable);
- }
-
- @Override
- public void onComplete() {
- if (cf.isDone()) {
- throw new IllegalStateException("subscription already completed");
- }
- buffers[3] = EMPTY_CHUNK_HEADER();
- buffers[4] = CRLF_BUFFER();
- try {
- chan.write(buffers, 3, 2);
- } catch (IOException ex) {
- cf.completeExceptionally(ex);
- return;
- }
- cf.complete(null);
- }
- }
-
- private void waitForCompletion() throws IOException {
- try {
- cf.join();
- } catch (CompletionException e) {
- throw Utils.getIOException(e);
- }
- }
-
- /* Entire request is sent, or just body only */
- private void writeStreamedContent(boolean includeHeaders)
- throws IOException
- {
- StreamSubscriber subscriber = new StreamSubscriber(includeHeaders);
- requestProc.subscribe(subscriber);
- waitForCompletion();
- }
-
- class FixedContentSubscriber implements Flow.Subscriber<ByteBuffer>
- {
- volatile Flow.Subscription subscription;
- volatile boolean includeHeaders;
- volatile long contentWritten = 0;
-
- FixedContentSubscriber(boolean includeHeaders) {
- this.includeHeaders = includeHeaders;
- }
-
- @Override
- public void onSubscribe(Flow.Subscription subscription) {
- if (this.subscription != null) {
- throw new IllegalStateException("already subscribed");
- }
- this.subscription = subscription;
- subscription.request(1);
- }
-
- @Override
- public void onNext(ByteBuffer item) {
- int startbuf, nbufs;
- long headersLength;
-
- if (includeHeaders) {
- startbuf = 0;
- nbufs = 3;
- headersLength = buffers[0].remaining() + buffers[1].remaining();
- } else {
- startbuf = 2;
- nbufs = 1;
- headersLength = 0;
- }
- buffers[2] = item;
- try {
- long writing = buffers[2].remaining() + headersLength;
- contentWritten += buffers[2].remaining();
- chan.checkWrite(writing, buffers, startbuf, nbufs);
-
- if (contentWritten > contentLength) {
- String msg = "Too many bytes in request body. Expected: " +
- Long.toString(contentLength) + " Sent: " +
- Long.toString(contentWritten);
- throw new IOException(msg);
- }
- subscription.request(1);
- } catch (IOException e) {
- subscription.cancel();
- cf.completeExceptionally(e);
+ int chunklen = item.remaining();
+ ArrayList<ByteBuffer> l = new ArrayList<>(3);
+ l.add(getHeader(chunklen));
+ l.add(item);
+ l.add(ByteBuffer.wrap(CRLF));
+ http1Exchange.appendToOutgoing(l);
}
}
@Override
public void onError(Throwable throwable) {
- if (cf.isDone()) {
+ if (complete)
return;
- }
+
subscription.cancel();
- cf.completeExceptionally(throwable);
+ http1Exchange.appendToOutgoing(throwable);
}
@Override
public void onComplete() {
- if (cf.isDone()) {
- throw new IllegalStateException("subscription already completed");
- }
+ if (complete) {
+ Throwable t = new IllegalStateException("subscription already completed");
+ http1Exchange.appendToOutgoing(t);
+ } else {
+ ArrayList<ByteBuffer> l = new ArrayList<>(2);
+ l.add(ByteBuffer.wrap(EMPTY_CHUNK_BYTES));
+ l.add(ByteBuffer.wrap(CRLF));
+ complete = true;
+ //setFinished();
+ http1Exchange.appendToOutgoing(l);
+ http1Exchange.appendToOutgoing(COMPLETED);
+ setFinished(); // TODO: before or after,? does it matter?
- if (contentLength > contentWritten) {
- subscription.cancel();
- Exception e = new IOException("Too few bytes returned by the processor");
- cf.completeExceptionally(e);
- } else {
- cf.complete(null);
}
}
}
- /* Entire request is sent, or just body only */
- private void writeFixedContent(boolean includeHeaders)
- throws IOException {
- if (contentLength == 0) {
- return;
+ class FixedContentSubscriber extends Http1BodySubscriber {
+
+ private volatile long contentWritten;
+
+ @Override
+ public void onSubscribe(Flow.Subscription subscription) {
+ if (this.subscription != null) {
+ Throwable t = new IllegalStateException("already subscribed");
+ http1Exchange.appendToOutgoing(t);
+ } else {
+ this.subscription = subscription;
+ }
}
- FixedContentSubscriber subscriber = new FixedContentSubscriber(includeHeaders);
- requestProc.subscribe(subscriber);
- waitForCompletion();
+
+ @Override
+ public void onNext(ByteBuffer item) {
+ debug.log(Level.DEBUG, "onNext");
+ Objects.requireNonNull(item);
+ if (complete) {
+ Throwable t = new IllegalStateException("subscription already completed");
+ http1Exchange.appendToOutgoing(t);
+ } else {
+ long writing = item.remaining();
+ long written = (contentWritten += writing);
+
+ if (written > contentLength) {
+ subscription.cancel();
+ String msg = connection.getConnectionFlow()
+ + " [" + Thread.currentThread().getName() +"] "
+ + "Too many bytes in request body. Expected: "
+ + contentLength + ", got: " + written;
+ http1Exchange.appendToOutgoing(new IOException(msg));
+ } else {
+ http1Exchange.appendToOutgoing(List.of(item));
+ }
+ }
+ }
+
+ @Override
+ public void onError(Throwable throwable) {
+ debug.log(Level.DEBUG, "onError");
+ if (complete) // TODO: error?
+ return;
+
+ subscription.cancel();
+ http1Exchange.appendToOutgoing(throwable);
+ }
+
+ @Override
+ public void onComplete() {
+ debug.log(Level.DEBUG, "onComplete");
+ if (complete) {
+ Throwable t = new IllegalStateException("subscription already completed");
+ http1Exchange.appendToOutgoing(t);
+ } else {
+ complete = true;
+ long written = contentWritten;
+ if (contentLength > written) {
+ subscription.cancel();
+ Throwable t = new IOException(connection.getConnectionFlow()
+ + " [" + Thread.currentThread().getName() +"] "
+ + "Too few bytes returned by the publisher ("
+ + written + "/"
+ + contentLength + ")");
+ http1Exchange.appendToOutgoing(t);
+ } else {
+ http1Exchange.appendToOutgoing(COMPLETED);
+ }
+ }
+ }
}
private static final byte[] CRLF = {'\r', '\n'};
private static final byte[] EMPTY_CHUNK_BYTES = {'0', '\r', '\n'};
- private ByteBuffer CRLF_BUFFER() {
- return ByteBuffer.wrap(CRLF);
- }
-
- private ByteBuffer EMPTY_CHUNK_HEADER() {
- return ByteBuffer.wrap(EMPTY_CHUNK_BYTES);
- }
-
- /* Returns a header for a particular chunk size */
- private static ByteBuffer getHeader(int size){
- String hexStr = Integer.toHexString(size);
+ /** Returns a header for a particular chunk size */
+ private static ByteBuffer getHeader(int size) {
+ String hexStr = Integer.toHexString(size);
byte[] hexBytes = hexStr.getBytes(US_ASCII);
byte[] header = new byte[hexStr.length()+2];
System.arraycopy(hexBytes, 0, header, 0, hexBytes.length);
@@ -471,4 +365,8 @@
header[hexBytes.length+1] = CRLF[1];
return ByteBuffer.wrap(header);
}
+
+ static final boolean DEBUG = Utils.DEBUG; // Revisit: temporary dev flag.
+ final System.Logger debug = Utils.getDebugLogger(this::toString, DEBUG);
+
}
--- a/src/jdk.incubator.httpclient/share/classes/jdk/incubator/http/Http1Response.java Sun Nov 05 17:05:57 2017 +0000
+++ b/src/jdk.incubator.httpclient/share/classes/jdk/incubator/http/Http1Response.java Sun Nov 05 17:32:13 2017 +0000
@@ -25,16 +25,23 @@
package jdk.incubator.http;
-import java.io.IOException;
+import java.io.EOFException;
+import java.lang.System.Logger.Level;
import java.nio.ByteBuffer;
import java.util.concurrent.CompletableFuture;
import java.util.concurrent.Executor;
+import java.util.function.BiConsumer;
+import java.util.function.Consumer;
+import java.util.function.Function;
+import jdk.incubator.http.ResponseContent.BodyParser;
import jdk.incubator.http.internal.common.Log;
import static jdk.incubator.http.HttpClient.Version.HTTP_1_1;
+import jdk.incubator.http.internal.common.MinimalFuture;
+import jdk.incubator.http.internal.common.Utils;
/**
- * Handles a HTTP/1.1 response in two blocking calls. readHeaders() and
- * readBody(). There can be more than one of these per Http exchange.
+ * Handles a HTTP/1.1 response (headers + body).
+ * There can be more than one of these per Http exchange.
*/
class Http1Response<T> {
@@ -42,48 +49,72 @@
private final HttpRequestImpl request;
private Response response;
private final HttpConnection connection;
- private ResponseHeaders headers;
+ private HttpHeaders headers;
private int responseCode;
- private ByteBuffer buffer;
private final Http1Exchange<T> exchange;
private final boolean redirecting; // redirecting
private boolean return2Cache; // return connection to cache when finished
+ private final HeadersReader headersReader; // used to read the headers
+ private final BodyReader bodyReader; // used to read the body
+ private final Http1AsyncReceiver asyncReceiver;
+ private volatile boolean reading;
+ private volatile EOFException eof;
- Http1Response(HttpConnection conn, Http1Exchange<T> exchange) {
+ // Revisit: can we get rid of this?
+ static enum State {INITIAL, READING_HEADERS, READING_BODY, DONE}
+ private volatile State readProgress = State.INITIAL;
+ static final boolean DEBUG = Utils.DEBUG; // Revisit: temporary dev flag.
+ final System.Logger debug = Utils.getDebugLogger(this.getClass()::getSimpleName, DEBUG);
+
+
+ Http1Response(HttpConnection conn,
+ Http1Exchange<T> exchange,
+ Http1AsyncReceiver asyncReceiver) {
+ this.readProgress = State.INITIAL;
this.request = exchange.request();
this.exchange = exchange;
this.connection = conn;
this.redirecting = false;
- buffer = exchange.getBuffer();
+ this.asyncReceiver = asyncReceiver;
+ headersReader = new HeadersReader(this::advance);
+ bodyReader = new BodyReader(this::advance);
}
- @SuppressWarnings("unchecked")
- public void readHeaders() throws IOException {
- String statusline = readStatusLine();
- if (statusline == null) {
- if (Log.errors()) {
- Log.logError("Connection closed. Retry");
- }
- connection.close();
- // connection was closed
- throw new IOException("Connection closed");
- }
- if (!statusline.startsWith("HTTP/1.")) {
- throw new IOException("Invalid status line: " + statusline);
+ public CompletableFuture<Response> readHeadersAsync(Executor executor) {
+ debug.log(Level.DEBUG, () -> "Reading Headers: (remaining: "
+ + asyncReceiver.remaining() +") " + readProgress);
+ // with expect continue we will resume reading headers + body.
+ asyncReceiver.unsubscribe(bodyReader);
+ bodyReader.reset();
+ Http1HeaderParser hd = new Http1HeaderParser();
+ readProgress = State.READING_HEADERS;
+ headersReader.start(hd);
+ asyncReceiver.subscribe(headersReader);
+ CompletableFuture<State> cf = headersReader.completion();
+ assert cf != null : "parsing not started";
+
+ Function<State, Response> lambda = (State completed) -> {
+ assert completed == State.READING_HEADERS;
+ debug.log(Level.DEBUG, () ->
+ "Reading Headers: creating Response object;"
+ + " state is now " + readProgress);
+ asyncReceiver.unsubscribe(headersReader);
+ responseCode = hd.responseCode();
+ headers = hd.headers();
+
+ response = new Response(request,
+ exchange.getExchange(),
+ headers,
+ responseCode,
+ HTTP_1_1);
+ return response;
+ };
+
+ if (executor != null) {
+ return cf.thenApplyAsync(lambda, executor);
+ } else {
+ return cf.thenApply(lambda);
}
- if (Log.trace()) {
- Log.logTrace("Statusline: {0}", statusline);
- }
- char c = statusline.charAt(7);
- responseCode = Integer.parseInt(statusline.substring(9, 12));
-
- headers = new ResponseHeaders(connection, buffer);
- if (Log.headers()) {
- logHeaders(headers);
- }
- response = new Response(
- request, exchange.getExchange(),
- headers, responseCode, HTTP_1_1);
}
private boolean finished;
@@ -96,10 +127,6 @@
return finished;
}
- ByteBuffer getBuffer() {
- return buffer;
- }
-
int fixupContentLen(int clen) {
if (request.method().equalsIgnoreCase("HEAD")) {
return 0;
@@ -114,62 +141,113 @@
return clen;
}
- public CompletableFuture<T> readBody(
- HttpResponse.BodyProcessor<T> p,
- boolean return2Cache,
- Executor executor) {
- final BlockingPushPublisher<ByteBuffer> publisher = new BlockingPushPublisher<>();
- return readBody(p, return2Cache, publisher, executor);
- }
-
- private CompletableFuture<T> readBody(
- HttpResponse.BodyProcessor<T> p,
- boolean return2Cache,
- AbstractPushPublisher<ByteBuffer> publisher,
- Executor executor) {
+ public CompletableFuture<T> readBody(HttpResponse.BodySubscriber<T> p,
+ boolean return2Cache,
+ Executor executor) {
this.return2Cache = return2Cache;
- final jdk.incubator.http.HttpResponse.BodyProcessor<T> pusher = p;
+ final HttpResponse.BodySubscriber<T> pusher = p;
final CompletableFuture<T> cf = p.getBody().toCompletableFuture();
- int clen0;
- try {
- clen0 = headers.getContentLength();
- } catch (IOException ex) {
- cf.completeExceptionally(ex);
- return cf;
- }
+ int clen0 = (int)headers.firstValueAsLong("Content-Length").orElse(-1);
+
final int clen = fixupContentLen(clen0);
+ // expect-continue reads headers and body twice.
+ // if we reach here, we must reset the headersReader state.
+ asyncReceiver.unsubscribe(headersReader);
+ headersReader.reset();
+
executor.execute(() -> {
try {
+ HttpClientImpl client = connection.client();
content = new ResponseContent(
connection, clen, headers, pusher,
- publisher.asDataConsumer(),
- (t -> {
- publisher.acceptError(t);
- connection.close();
- cf.completeExceptionally(t);
- }),
- () -> onFinished()
+ this::onFinished
);
- publisher.subscribe(p);
if (cf.isCompletedExceptionally()) {
// if an error occurs during subscription
connection.close();
return;
}
- content.pushBody(buffer);
+ // increment the reference count on the HttpClientImpl
+ // to prevent the SelectorManager thread from exiting until
+ // the body is fully read.
+ client.reference();
+ bodyReader.start(content.getBodyParser(
+ (t) -> {
+ try {
+ if (t != null) {
+ pusher.onError(t);
+ connection.close();
+ if (!cf.isDone())
+ cf.completeExceptionally(t);
+ }
+ } finally {
+ // decrement the reference count on the HttpClientImpl
+ // to allow the SelectorManager thread to exit if no
+ // other operation is pending and the facade is no
+ // longer referenced.
+ client.unreference();
+ bodyReader.onComplete(t);
+ }
+ }));
+ CompletableFuture<State> bodyReaderCF = bodyReader.completion();
+ asyncReceiver.subscribe(bodyReader);
+ assert bodyReaderCF != null : "parsing not started";
+ // Make sure to keep a reference to asyncReceiver from
+ // within this
+ CompletableFuture<?> trailingOp = bodyReaderCF.whenComplete((s,t) -> {
+ t = Utils.getCompletionCause(t);
+ try {
+ if (t != null) {
+ debug.log(Level.DEBUG, () ->
+ "Finished reading body: " + s);
+ assert s == State.READING_BODY;
+ }
+ if (t != null && !cf.isDone()) {
+ pusher.onError(t);
+ cf.completeExceptionally(t);
+ }
+ } catch (Throwable x) {
+ // not supposed to happen
+ asyncReceiver.onReadError(x);
+ }
+ });
+ connection.addTrailingOperation(trailingOp);
} catch (Throwable t) {
- cf.completeExceptionally(t);
+ debug.log(Level.DEBUG, () -> "Failed reading body: " + t);
+ try {
+ if (!cf.isDone()) {
+ pusher.onError(t);
+ cf.completeExceptionally(t);
+ }
+ } finally {
+ asyncReceiver.onReadError(t);
+ }
}
});
- return cf;
+ BiConsumer<Object,Throwable> whenComplete = (r,t) -> {
+ if (t != null) {
+ asyncReceiver.unsubscribe(bodyReader);
+ bodyReader.reset();
+ }
+ };
+ return cf.whenComplete(whenComplete);
}
+
private void onFinished() {
+ asyncReceiver.clear();
if (return2Cache) {
- Log.logTrace("Returning connection to the pool: {0}", connection);
- connection.returnToCache(headers);
+ Log.logTrace("Attempting to return connection to the pool: {0}", connection);
+ reading = false;
+ // TODO: need to do something here?
+ // connection.setAsyncCallbacks(null, null, null);
+
+ // don't return the connection to the cache if EOF happened.
+ debug.log(Level.DEBUG, () -> connection.getConnectionFlow()
+ + ": return to HTTP/1.1 pool");
+ connection.closeOrReturnToCache(eof == null ? headers : null);
}
}
@@ -198,47 +276,251 @@
static final char CR = '\r';
static final char LF = '\n';
- private int obtainBuffer() throws IOException {
- int n = buffer.remaining();
+// ================ Support for plugging into AsyncConnection =================
+// ============================================================================
+
+ // Callback: Error receiver: Consumer of Throwable.
+ void onReadError(Throwable t) {
+ Log.logError(t);
+ Receiver<?> receiver = receiver(readProgress);
+ if (t instanceof EOFException) {
+ debug.log(Level.DEBUG, "onReadError: received EOF");
+ eof = (EOFException) t;
+ }
+ CompletableFuture<?> cf = receiver == null ? null : receiver.completion();
+ debug.log(Level.DEBUG, () -> "onReadError: cf is "
+ + (cf == null ? "null"
+ : (cf.isDone() ? "already completed"
+ : "not yet completed")));
+ if (cf != null && !cf.isDone()) cf.completeExceptionally(t);
+ else { debug.log(Level.DEBUG, "onReadError", t); }
+ debug.log(Level.DEBUG, () -> "closing connection: cause is " + t);
+ connection.close();
+ }
+
+ // ========================================================================
+
+ private State advance(State previous) {
+ assert readProgress == previous;
+ switch(previous) {
+ case READING_HEADERS:
+ asyncReceiver.unsubscribe(headersReader);
+ return readProgress = State.READING_BODY;
+ case READING_BODY:
+ asyncReceiver.unsubscribe(bodyReader);
+ return readProgress = State.DONE;
+ default:
+ throw new InternalError("can't advance from " + previous);
+ }
+ }
- if (n == 0) {
- buffer = connection.read();
- if (buffer == null) {
- return -1;
- }
- n = buffer.remaining();
+ Receiver<?> receiver(State state) {
+ switch(state) {
+ case READING_HEADERS: return headersReader;
+ case READING_BODY: return bodyReader;
+ default: return null;
}
- return n;
+
+ }
+
+ static abstract class Receiver<T>
+ implements Http1AsyncReceiver.Http1AsyncDelegate {
+ abstract void start(T parser);
+ abstract CompletableFuture<State> completion();
+ // accepts a buffer from upstream.
+ // this should be implemented as a simple call to
+ // accept(ref, parser, cf)
+ public abstract boolean tryAsyncReceive(ByteBuffer buffer);
+ public abstract void onReadError(Throwable t);
+ // handle a byte buffer received from upstream.
+ // this method should set the value of Http1Response.buffer
+ // to ref.get() before beginning parsing.
+ abstract void handle(ByteBuffer buf, T parser,
+ CompletableFuture<State> cf);
+ // resets this objects state so that it can be reused later on
+ // typically puts the reference to parser and completion to null
+ abstract void reset();
+
+ // accepts a byte buffer received from upstream
+ // returns true if the buffer is fully parsed and more data can
+ // be accepted, false otherwise.
+ final boolean accept(ByteBuffer buf, T parser,
+ CompletableFuture<State> cf) {
+ if (cf == null || parser == null || cf.isDone()) return false;
+ handle(buf, parser, cf);
+ return !cf.isDone();
+ }
+ public abstract void onSubscribe(AbstractSubscription s);
+ public abstract AbstractSubscription subscription();
+
}
- String readStatusLine() throws IOException {
- boolean cr = false;
- StringBuilder statusLine = new StringBuilder(128);
- while ((obtainBuffer()) != -1) {
- byte[] buf = buffer.array();
- int offset = buffer.position();
- int len = buffer.limit() - offset;
+ // Invoked with each new ByteBuffer when reading headers...
+ final class HeadersReader extends Receiver<Http1HeaderParser> {
+ final Consumer<State> onComplete;
+ volatile Http1HeaderParser parser;
+ volatile CompletableFuture<State> cf;
+ volatile long count; // bytes parsed (for debug)
+ volatile AbstractSubscription subscription;
+
+ HeadersReader(Consumer<State> onComplete) {
+ this.onComplete = onComplete;
+ }
+
+ @Override
+ public AbstractSubscription subscription() {
+ return subscription;
+ }
+
+ @Override
+ public void onSubscribe(AbstractSubscription s) {
+ this.subscription = s;
+ s.request(1);
+ }
- for (int i = 0; i < len; i++) {
- char c = (char) buf[i+offset];
+ @Override
+ void reset() {
+ cf = null;
+ parser = null;
+ count = 0;
+ subscription = null;
+ }
+
+ // Revisit: do we need to support restarting?
+ @Override
+ final void start(Http1HeaderParser hp) {
+ count = 0;
+ cf = new MinimalFuture<>();
+ parser = hp;
+ }
+
+ @Override
+ CompletableFuture<State> completion() {
+ return cf;
+ }
+
+ @Override
+ public final boolean tryAsyncReceive(ByteBuffer ref) {
+ boolean hasDemand = subscription.demand().tryDecrement();
+ assert hasDemand;
+ boolean needsMore = accept(ref, parser, cf);
+ if (needsMore) subscription.request(1);
+ return needsMore;
+ }
+
+ @Override
+ public final void onReadError(Throwable t) {
+ Http1Response.this.onReadError(t);
+ }
- if (cr) {
- if (c == LF) {
- buffer.position(i + 1 + offset);
- return statusLine.toString();
- } else {
- throw new IOException("invalid status line");
- }
+ @Override
+ final void handle(ByteBuffer b,
+ Http1HeaderParser parser,
+ CompletableFuture<State> cf) {
+ assert cf != null : "parsing not started";
+ assert parser != null : "no parser";
+ try {
+ count += b.remaining();
+ debug.log(Level.DEBUG, () -> "Sending " + b.remaining()
+ + "/" + b.capacity() + " bytes to header parser");
+ if (parser.parse(b)) {
+ count -= b.remaining();
+ debug.log(Level.DEBUG, () ->
+ "Parsing headers completed. bytes=" + count);
+ onComplete.accept(State.READING_HEADERS);
+ cf.complete(State.READING_HEADERS);
}
- if (c == CR) {
- cr = true;
- } else {
- statusLine.append(c);
+ } catch (Throwable t) {
+ debug.log(Level.DEBUG,
+ () -> "Header parser failed to handle buffer: " + t);
+ cf.completeExceptionally(t);
+ }
+ }
+ }
+
+ // Invoked with each new ByteBuffer when reading bodies...
+ final class BodyReader extends Receiver<BodyParser> {
+ final Consumer<State> onComplete;
+ volatile BodyParser parser;
+ volatile CompletableFuture<State> cf;
+ volatile AbstractSubscription subscription;
+ BodyReader(Consumer<State> onComplete) {
+ this.onComplete = onComplete;
+ }
+
+ @Override
+ void reset() {
+ parser = null;
+ cf = null;
+ subscription = null;
+ }
+
+ // Revisit: do we need to support restarting?
+ @Override
+ final void start(BodyParser parser) {
+ cf = new MinimalFuture<>();
+ this.parser = parser;
+ }
+
+ @Override
+ CompletableFuture<State> completion() {
+ return cf;
+ }
+
+ @Override
+ public final boolean tryAsyncReceive(ByteBuffer b) {
+ return accept(b, parser, cf);
+ }
+
+ @Override
+ public final void onReadError(Throwable t) {
+ Http1Response.this.onReadError(t);
+ }
+
+ @Override
+ public AbstractSubscription subscription() {
+ return subscription;
+ }
+
+ @Override
+ public void onSubscribe(AbstractSubscription s) {
+ this.subscription = s;
+ parser.onSubscribe(s);
+ }
+
+ @Override
+ final void handle(ByteBuffer b,
+ BodyParser parser,
+ CompletableFuture<State> cf) {
+ assert cf != null : "parsing not started";
+ assert parser != null : "no parser";
+ try {
+ debug.log(Level.DEBUG, () -> "Sending " + b.remaining()
+ + "/" + b.capacity() + " bytes to body parser");
+ parser.accept(b);
+ } catch (Throwable t) {
+ debug.log(Level.DEBUG,
+ () -> "Body parser failed to handle buffer: " + t);
+ if (!cf.isDone()) {
+ cf.completeExceptionally(t);
}
}
- // unlikely, but possible, that multiple reads required
- buffer.position(buffer.limit());
}
- return null;
+
+ final void onComplete(Throwable closedExceptionally) {
+ if (cf.isDone()) return;
+ if (closedExceptionally != null) {
+ cf.completeExceptionally(closedExceptionally);
+ } else {
+ onComplete.accept(State.READING_BODY);
+ cf.complete(State.READING_BODY);
+ }
+ }
+
+ @Override
+ public String toString() {
+ return super.toString() + "/parser=" + String.valueOf(parser);
+ }
+
}
}
--- a/src/jdk.incubator.httpclient/share/classes/jdk/incubator/http/Http2ClientImpl.java Sun Nov 05 17:05:57 2017 +0000
+++ b/src/jdk.incubator.httpclient/share/classes/jdk/incubator/http/Http2ClientImpl.java Sun Nov 05 17:32:13 2017 +0000
@@ -1,5 +1,5 @@
/*
- * Copyright (c) 2015, 2016, Oracle and/or its affiliates. All rights reserved.
+ * Copyright (c) 2015, 2017, Oracle and/or its affiliates. All rights reserved.
* DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER.
*
* This code is free software; you can redistribute it and/or modify it
@@ -25,17 +25,18 @@
package jdk.incubator.http;
-import java.io.IOException;
+import java.lang.System.Logger.Level;
import java.net.InetSocketAddress;
import java.net.URI;
import java.util.Base64;
import java.util.Collections;
+import java.util.HashSet;
import java.util.HashMap;
-import java.util.HashSet;
import java.util.Map;
import java.util.Set;
import java.util.concurrent.ConcurrentHashMap;
-
+import java.util.concurrent.CompletableFuture;
+import jdk.incubator.http.internal.common.MinimalFuture;
import jdk.incubator.http.internal.common.Utils;
import jdk.incubator.http.internal.frame.SettingsFrame;
import static jdk.incubator.http.internal.frame.SettingsFrame.INITIAL_WINDOW_SIZE;
@@ -49,6 +50,10 @@
*/
class Http2ClientImpl {
+ static final boolean DEBUG = Utils.DEBUG; // Revisit: temporary dev flag.
+ final static System.Logger debug =
+ Utils.getDebugLogger("Http2ClientImpl"::toString, DEBUG);
+
private final HttpClientImpl client;
Http2ClientImpl(HttpClientImpl client) {
@@ -59,13 +64,26 @@
private final Map<String,Http2Connection> connections = new ConcurrentHashMap<>();
private final Set<String> opening = Collections.synchronizedSet(new HashSet<>());
+ private final Map<String,Set<CompletableFuture<Http2Connection>>> waiting =
+ Collections.synchronizedMap(new HashMap<>());
+
+ private void addToWaiting(String key, CompletableFuture<Http2Connection> cf) {
+ synchronized (waiting) {
+ Set<CompletableFuture<Http2Connection>> waiters = waiting.get(key);
+ if (waiters == null) {
+ waiters = new HashSet<>();
+ waiting.put(key, waiters);
+ }
+ waiters.add(cf);
+ }
+ }
boolean haveConnectionFor(URI uri, InetSocketAddress proxy) {
return connections.containsKey(Http2Connection.keyFor(uri,proxy));
}
/**
- * If a https request then blocks and waits until a connection is opened.
+ * If a https request then async waits until a connection is opened.
* Returns null if the request is 'http' as a different (upgrade)
* mechanism is used.
*
@@ -78,50 +96,59 @@
* In latter case, when the Http2Connection is connected, putConnection() must
* be called to store it.
*/
- Http2Connection getConnectionFor(HttpRequestImpl req)
- throws IOException, InterruptedException {
+ CompletableFuture<Http2Connection> getConnectionFor(HttpRequestImpl req) {
URI uri = req.uri();
InetSocketAddress proxy = req.proxy(client);
String key = Http2Connection.keyFor(uri, proxy);
- Http2Connection connection = connections.get(key);
- if (connection != null) { // fast path if connection already exists
- return connection;
- }
+
synchronized (opening) {
- while ((connection = connections.get(key)) == null) {
- if (!req.secure()) {
- return null;
- }
- if (!opening.contains(key)) {
- opening.add(key);
- break;
- } else {
- opening.wait();
- }
+ Http2Connection connection = connections.get(key);
+ if (connection != null) { // fast path if connection already exists
+ return CompletableFuture.completedFuture(connection);
+ }
+
+ if (!req.secure()) {
+ return MinimalFuture.completedFuture(null);
+ }
+
+ if (!opening.contains(key)) {
+ debug.log(Level.DEBUG, "Opening: %s", key);
+ opening.add(key);
+ } else {
+ CompletableFuture<Http2Connection> cf = new MinimalFuture<>();
+ addToWaiting(key, cf);
+ return cf;
}
}
- if (connection != null) {
- return connection;
- }
- // we are opening the connection here blocking until it is done.
- try {
- connection = new Http2Connection(req, this);
- } catch (Throwable t) {
- synchronized (opening) {
- opening.remove(key);
- opening.notifyAll();
- }
- throw t;
- }
- synchronized (opening) {
- connections.put(key, connection);
- opening.remove(key);
- opening.notifyAll();
- }
- return connection;
+ return Http2Connection
+ .createAsync(req, this)
+ .whenComplete((conn, t) -> {
+ debug.log(Level.DEBUG,
+ "waking up dependents with created connection");
+ synchronized (opening) {
+ Set<CompletableFuture<Http2Connection>> waiters = waiting.remove(key);
+ debug.log(Level.DEBUG, "Opening completed: %s", key);
+ opening.remove(key);
+ final Throwable cause = Utils.getCompletionCause(t);
+ if (waiters == null) {
+ debug.log(Level.DEBUG, "no dependent to wake up");
+ return;
+ } else if (cause instanceof Http2Connection.ALPNException) {
+ waiters.forEach((cf1) -> cf1.completeAsync(() -> null,
+ client.theExecutor()));
+ } else if (cause != null) {
+ debug.log(Level.DEBUG,
+ () -> "waking up dependants: failed: " + cause);
+ waiters.forEach((cf1) -> cf1.completeExceptionally(cause));
+ } else {
+ debug.log(Level.DEBUG, "waking up dependants: succeeded");
+ waiters.forEach((cf1) -> cf1.completeAsync(() -> conn,
+ client.theExecutor()));
+ }
+ }
+ });
}
-
/*
* TODO: If there isn't a connection to the same destination, then
* store it. If there is already a connection, then close it
@@ -134,6 +161,16 @@
connections.remove(c.key());
}
+ void stop() {
+ debug.log(Level.DEBUG, "stopping");
+ connections.values().stream().forEach(this::close);
+ connections.clear();
+ }
+
+ private void close(Http2Connection h2c) {
+ try { h2c.close(); } catch (Throwable t) {}
+ }
+
HttpClientImpl client() {
return client;
}
--- a/src/jdk.incubator.httpclient/share/classes/jdk/incubator/http/Http2Connection.java Sun Nov 05 17:05:57 2017 +0000
+++ b/src/jdk.incubator.httpclient/share/classes/jdk/incubator/http/Http2Connection.java Sun Nov 05 17:32:13 2017 +0000
@@ -25,26 +25,32 @@
package jdk.incubator.http;
+import java.io.EOFException;
import java.io.IOException;
+import java.lang.System.Logger.Level;
import java.net.InetSocketAddress;
import java.net.URI;
-import jdk.incubator.http.HttpConnection.Mode;
import java.nio.ByteBuffer;
import java.nio.charset.StandardCharsets;
-import java.util.HashMap;
import java.util.Iterator;
import java.util.LinkedList;
import java.util.List;
import java.util.Map;
import java.util.concurrent.CompletableFuture;
import java.util.ArrayList;
-import java.util.Collections;
import java.util.Formatter;
+import java.util.Objects;
import java.util.concurrent.ConcurrentHashMap;
-import java.util.concurrent.CountDownLatch;
+import java.util.concurrent.ConcurrentLinkedQueue;
+import java.util.concurrent.Flow;
+import java.util.function.Function;
+import java.util.function.Supplier;
import java.util.stream.Collectors;
import javax.net.ssl.SSLEngine;
import jdk.incubator.http.internal.common.*;
+import jdk.incubator.http.internal.common.SequentialScheduler;
+import jdk.incubator.http.internal.common.SequentialScheduler.SynchronizedRestartableTask;
+import jdk.incubator.http.internal.common.FlowTube.TubeSubscriber;
import jdk.incubator.http.internal.frame.*;
import jdk.incubator.http.internal.hpack.Encoder;
import jdk.incubator.http.internal.hpack.Decoder;
@@ -83,11 +89,21 @@
* stream are provided by calling Stream.incoming().
*/
class Http2Connection {
+
+ static final boolean DEBUG = Utils.DEBUG; // Revisit: temporary dev flag.
+ static final boolean DEBUG_HPACK = Utils.DEBUG_HPACK; // Revisit: temporary dev flag.
+ final System.Logger debug = Utils.getDebugLogger(this::dbgString, DEBUG);
+ final static System.Logger DEBUG_LOGGER =
+ Utils.getDebugLogger("Http2Connection"::toString, DEBUG);
+ private final System.Logger debugHpack =
+ Utils.getHpackLogger(this::dbgString, DEBUG_HPACK);
+ static final ByteBuffer EMPTY_TRIGGER = ByteBuffer.allocate(0);
+
/*
* ByteBuffer pooling strategy for HTTP/2 protocol:
*
* In general there are 4 points where ByteBuffers are used:
- * - incoming/outgoing frames from/to ByteBufers plus incoming/outgoing encrypted data
+ * - incoming/outgoing frames from/to ByteBuffers plus incoming/outgoing encrypted data
* in case of SSL connection.
*
* 1. Outgoing frames encoded to ByteBuffers.
@@ -123,10 +139,15 @@
{
// if preface is not sent, buffers data in the pending list
if (!prefaceSent) {
+ debug.log(Level.DEBUG, "Preface is not sent: buffering %d",
+ buf.get().remaining());
synchronized (this) {
if (!prefaceSent) {
if (pending == null) pending = new ArrayList<>();
pending.add(buf);
+ debug.log(Level.DEBUG, () -> "there are now "
+ + Utils.remaining(pending.toArray(new ByteBufferReference[0]))
+ + " bytes buffered waiting for preface to be sent");
return false;
}
}
@@ -143,13 +164,18 @@
this.pending = null;
if (pending != null) {
// flush pending data
+ debug.log(Level.DEBUG, () -> "Processing buffered data: "
+ + Utils.remaining(pending.toArray(new ByteBufferReference[0])));
for (ByteBufferReference b : pending) {
decoder.decode(b);
}
}
-
+ ByteBuffer b = buf.get();
// push the received buffer to the frames decoder.
- decoder.decode(buf);
+ if (b != EMPTY_TRIGGER) {
+ debug.log(Level.DEBUG, "Processing %d", buf.get().remaining());
+ decoder.decode(buf);
+ }
return true;
}
@@ -167,7 +193,9 @@
//-------------------------------------
final HttpConnection connection;
- private final HttpClientImpl client;
+ // only keep a strong reference to Http2ClientImpl, which only has
+ // a weak reference on HttpClientImpl, to avoid strong references
+ // from the selector thread to HttpClientImpl (via attachments).
private final Http2ClientImpl client2;
private final Map<Integer,Stream<?>> streams = new ConcurrentHashMap<>();
private int nextstreamid;
@@ -186,7 +214,10 @@
*/
private final WindowController windowController = new WindowController();
private final FramesController framesController = new FramesController();
+ private final Http2TubeSubscriber subscriber = new Http2TubeSubscriber();
final WindowUpdateSender windowUpdater;
+ private volatile Throwable cause;
+ private volatile Supplier<ByteBuffer> initial;
static final int DEFAULT_FRAME_SIZE = 16 * 1024;
@@ -199,7 +230,6 @@
int nextstreamid,
String key) {
this.connection = connection;
- this.client = client2.client();
this.client2 = client2;
this.nextstreamid = nextstreamid;
this.key = key;
@@ -209,102 +239,151 @@
this.serverSettings = SettingsFrame.getDefaultSettings();
this.hpackOut = new Encoder(serverSettings.getParameter(HEADER_TABLE_SIZE));
this.hpackIn = new Decoder(clientSettings.getParameter(HEADER_TABLE_SIZE));
- this.windowUpdater = new ConnectionWindowUpdateSender(this, client.getReceiveBufferSize());
+ debugHpack.log(Level.DEBUG, () -> "For the record:" + super.toString());
+ debugHpack.log(Level.DEBUG, "Decoder created: %s", hpackIn);
+ debugHpack.log(Level.DEBUG, "Encoder created: %s", hpackOut);
+ this.windowUpdater = new ConnectionWindowUpdateSender(this, client().getReceiveBufferSize());
}
/**
* Case 1) Create from upgraded HTTP/1.1 connection.
- * Is ready to use. Will not be SSL. exchange is the Exchange
+ * Is ready to use. Can be SSL. exchange is the Exchange
* that initiated the connection, whose response will be delivered
* on a Stream.
*/
- Http2Connection(HttpConnection connection,
+ private Http2Connection(HttpConnection connection,
Http2ClientImpl client2,
Exchange<?> exchange,
- ByteBuffer initial)
+ Supplier<ByteBuffer> initial)
throws IOException, InterruptedException
{
this(connection,
client2,
3, // stream 1 is registered during the upgrade
keyFor(connection));
- assert !(connection instanceof SSLConnection);
Log.logTrace("Connection send window size {0} ", windowController.connectionWindowSize());
Stream<?> initialStream = createStream(exchange);
initialStream.registerStream(1);
windowController.registerStream(1, getInitialSendWindowSize());
initialStream.requestSent();
+ // Upgrading:
+ // set callbacks before sending preface - makes sure anything that
+ // might be sent by the server will come our way.
+ this.initial = initial;
+ connectFlows(connection);
sendConnectionPreface();
- // start reading and writing
- // start reading
- AsyncConnection asyncConn = (AsyncConnection)connection;
- asyncConn.setAsyncCallbacks(this::asyncReceive, this::shutdown, this::getReadBuffer);
- connection.configureMode(Mode.ASYNC); // set mode only AFTER setAsyncCallbacks to provide visibility.
- asyncReceive(ByteBufferReference.of(initial));
- asyncConn.startReading();
}
// async style but completes immediately
static CompletableFuture<Http2Connection> createAsync(HttpConnection connection,
Http2ClientImpl client2,
Exchange<?> exchange,
- ByteBuffer initial) {
+ Supplier<ByteBuffer> initial)
+ {
return MinimalFuture.supply(() -> new Http2Connection(connection, client2, exchange, initial));
}
+ // Requires TLS handshake. So, is really async
+ static CompletableFuture<Http2Connection> createAsync(HttpRequestImpl request,
+ Http2ClientImpl h2client) {
+ assert request.secure();
+ AbstractAsyncSSLConnection connection = (AbstractAsyncSSLConnection)
+ HttpConnection.getConnection(request.getAddress(h2client.client()),
+ h2client.client(),
+ request,
+ HttpClient.Version.HTTP_2);
+
+ return connection.connectAsync()
+ .thenCompose(unused -> checkSSLConfig(connection))
+ .thenCompose(notused-> {
+ CompletableFuture<Http2Connection> cf = new CompletableFuture<>();
+ try {
+ Http2Connection hc = new Http2Connection(request, h2client, connection);
+ cf.complete(hc);
+ } catch (IOException e) {
+ cf.completeExceptionally(e);
+ }
+ return cf; } );
+ }
+
/**
* Cases 2) 3)
*
* request is request to be sent.
*/
- Http2Connection(HttpRequestImpl request, Http2ClientImpl h2client)
- throws IOException, InterruptedException
+ private Http2Connection(HttpRequestImpl request,
+ Http2ClientImpl h2client,
+ HttpConnection connection)
+ throws IOException
{
- this(HttpConnection.getConnection(request.getAddress(h2client.client()), h2client.client(), request, true),
- h2client,
- 1,
- keyFor(request.uri(), request.proxy(h2client.client())));
+ this(connection,
+ h2client,
+ 1,
+ keyFor(request.uri(), request.proxy(h2client.client())));
+
Log.logTrace("Connection send window size {0} ", windowController.connectionWindowSize());
- // start reading
- AsyncConnection asyncConn = (AsyncConnection)connection;
- asyncConn.setAsyncCallbacks(this::asyncReceive, this::shutdown, this::getReadBuffer);
- connection.connect();
- checkSSLConfig();
// safe to resume async reading now.
- asyncConn.enableCallback();
+ connectFlows(connection);
sendConnectionPreface();
}
+ private void connectFlows(HttpConnection connection) {
+ FlowTube tube = connection.getConnectionFlow();
+ // Connect the flow to our Http2TubeSubscriber:
+ // Using connection.publisher() here is a hack that
+ // allows us to continue calling connection.writeAsync()
+ // and connection.flushAsync() transparently.
+ // We will eventually need to implement our own publisher
+ // to write to the flow instead.
+ tube.connectFlows(connection.publisher(), // hack
+ subscriber);
+ }
+
+ final HttpClientImpl client() {
+ return client2.client();
+ }
+
/**
* Throws an IOException if h2 was not negotiated
*/
- private void checkSSLConfig() throws IOException {
- AbstractAsyncSSLConnection aconn = (AbstractAsyncSSLConnection)connection;
- SSLEngine engine = aconn.getEngine();
- String alpn = engine.getApplicationProtocol();
- if (alpn == null || !alpn.equals("h2")) {
- String msg;
- if (alpn == null) {
- Log.logSSL("ALPN not supported");
- msg = "ALPN not supported";
- } else switch (alpn) {
- case "":
- Log.logSSL("No ALPN returned");
- msg = "No ALPN negotiated";
- break;
- case "http/1.1":
- Log.logSSL("HTTP/1.1 ALPN returned");
- msg = "HTTP/1.1 ALPN returned";
- break;
- default:
- Log.logSSL("unknown ALPN returned");
- msg = "Unexpected ALPN: " + alpn;
- throw new IOException(msg);
+ private static CompletableFuture<?> checkSSLConfig(AbstractAsyncSSLConnection aconn) {
+ assert aconn.isSecure();
+
+ Function<String, CompletableFuture<Void>> checkAlpnCF = (alpn) -> {
+ CompletableFuture<Void> cf = new MinimalFuture<>();
+ SSLEngine engine = aconn.getEngine();
+ assert Objects.equals(alpn, engine.getApplicationProtocol());
+
+ DEBUG_LOGGER.log(Level.DEBUG, "checkSSLConfig: alpn: %s", alpn );
+
+ if (alpn == null || !alpn.equals("h2")) {
+ String msg;
+ if (alpn == null) {
+ Log.logSSL("ALPN not supported");
+ msg = "ALPN not supported";
+ } else {
+ switch (alpn) {
+ case "":
+ Log.logSSL(msg = "No ALPN negotiated");
+ break;
+ case "http/1.1":
+ Log.logSSL( msg = "HTTP/1.1 ALPN returned");
+ break;
+ default:
+ Log.logSSL(msg = "Unexpected ALPN: " + alpn);
+ cf.completeExceptionally(new IOException(msg));
+ }
+ }
+ cf.completeExceptionally(new ALPNException(msg, aconn));
+ return cf;
}
- throw new ALPNException(msg, aconn);
- }
+ cf.complete(null);
+ return cf;
+ };
+
+ return aconn.getALPN().thenCompose(checkAlpnCF);
}
static String keyFor(HttpConnection connection) {
@@ -322,7 +401,7 @@
String host;
int port;
- if (isProxy) {
+ if (proxy != null) {
host = proxy.getHostString();
port = proxy.getPort();
} else {
@@ -381,7 +460,11 @@
return words.stream().collect(Collectors.joining(" "));
}
- private void decodeHeaders(HeaderFrame frame, DecodingCallback decoder) {
+ private void decodeHeaders(HeaderFrame frame, DecodingCallback decoder)
+ throws IOException
+ {
+ debugHpack.log(Level.DEBUG, "decodeHeaders(%s)", decoder);
+
boolean endOfHeaders = frame.getFlag(HeaderFrame.END_HEADERS);
ByteBufferReference[] buffers = frame.getHeaderBlock();
@@ -390,7 +473,7 @@
}
}
- int getInitialSendWindowSize() {
+ final int getInitialSendWindowSize() {
return serverSettings.getParameter(INITIAL_WINDOW_SIZE);
}
@@ -400,7 +483,7 @@
sendFrame(f);
}
- private ByteBufferPool readBufferPool = new ByteBufferPool();
+ private final ByteBufferPool readBufferPool = new ByteBufferPool();
// provides buffer to read data (default size)
public ByteBufferReference getReadBuffer() {
@@ -409,7 +492,8 @@
private final Object readlock = new Object();
- public void asyncReceive(ByteBufferReference buffer) {
+ long count;
+ public final void asyncReceive(ByteBufferReference buffer) {
// We don't need to read anything and
// we don't want to send anything back to the server
// until the connection preface has been sent.
@@ -419,11 +503,45 @@
// SettingsFrame sent by the server) before the connection
// preface is fully sent might result in the server
// sending a GOAWAY frame with 'invalid_preface'.
+ //
+ // Note: asyncReceive is only called from the Http2TubeSubscriber
+ // sequential scheduler. Only asyncReceive uses the readLock.
+ // Therefore synchronizing on the readlock here should be
+ // safe.
+ //
synchronized (readlock) {
try {
+ Supplier<ByteBuffer> bs = initial;
+ // ensure that we always handle the initial buffer first,
+ // if any.
+ if (bs != null) {
+ initial = null;
+ ByteBuffer b = bs.get();
+ if (b.hasRemaining()) {
+ long c = ++count;
+ debug.log(Level.DEBUG, () -> "H2 Receiving Initial("
+ + c +"): " + b.remaining());
+ framesController.processReceivedData(framesDecoder,
+ ByteBufferReference.of(b));
+ }
+ }
+ ByteBuffer b = buffer.get();
// the readlock ensures that the order of incoming buffers
// is preserved.
- framesController.processReceivedData(framesDecoder, buffer);
+ if (b == EMPTY_TRIGGER) {
+ debug.log(Level.DEBUG, "H2 Received EMPTY_TRIGGER");
+ boolean prefaceSent = framesController.prefaceSent;
+ assert prefaceSent;
+ // call framesController.processReceivedData to potentially
+ // trigger the processing of all the data buffered there.
+ framesController.processReceivedData(framesDecoder, buffer);
+ debug.log(Level.DEBUG, "H2 processed buffered data");
+ } else {
+ long c = ++count;
+ debug.log(Level.DEBUG, "H2 Receiving(%d): %d", c, b.remaining());
+ framesController.processReceivedData(framesDecoder, buffer);
+ debug.log(Level.DEBUG, "H2 processed(%d)", c);
+ }
} catch (Throwable e) {
String msg = Utils.stackTrace(e);
Log.logTrace(msg);
@@ -432,10 +550,20 @@
}
}
+ Throwable getRecordedCause() {
+ return cause;
+ }
void shutdown(Throwable t) {
+ debug.log(Level.DEBUG, () -> "Shutting down h2c: " + t);
+ if (closed == true) return;
+ synchronized (this) {
+ if (closed == true) return;
+ closed = true;
+ }
Log.logError(t);
- closed = true;
+ Throwable initialCause = this.cause;
+ if (initialCause == null) this.cause = t;
client2.deleteConnection(this);
List<Stream<?>> c = new LinkedList<>(streams.values());
for (Stream<?> s : c) {
@@ -457,8 +585,11 @@
if (frame instanceof MalformedFrame) {
Log.logError(((MalformedFrame) frame).getMessage());
if (streamid == 0) {
- protocolError(((MalformedFrame) frame).getErrorCode());
+ protocolError(((MalformedFrame) frame).getErrorCode(),
+ ((MalformedFrame) frame).getMessage());
} else {
+ debug.log(Level.DEBUG, () -> "Reset stream: "
+ + ((MalformedFrame) frame).getMessage());
resetStream(streamid, ((MalformedFrame) frame).getErrorCode());
}
return;
@@ -476,6 +607,13 @@
if (stream == null) {
// Should never receive a frame with unknown stream id
+ if (frame instanceof HeaderFrame) {
+ // always decode the headers as they may affect
+ // connection-level HPACK decoding state
+ HeaderDecoder decoder = new LoggingHeaderDecoder(new HeaderDecoder());
+ decodeHeaders((HeaderFrame) frame, decoder);
+ }
+
// To avoid looping, an endpoint MUST NOT send a RST_STREAM in
// response to a RST_STREAM frame.
if (!(frame instanceof ResetFrame)) {
@@ -499,6 +637,11 @@
private <T> void handlePushPromise(Stream<T> parent, PushPromiseFrame pp)
throws IOException
{
+ // always decode the headers as they may affect connection-level HPACK
+ // decoding state
+ HeaderDecoder decoder = new LoggingHeaderDecoder(new HeaderDecoder());
+ decodeHeaders(pp, decoder);
+
HttpRequestImpl parentReq = parent.request;
int promisedStreamid = pp.getPromisedStream();
if (promisedStreamid != nextPushStream) {
@@ -507,8 +650,7 @@
} else {
nextPushStream += 2;
}
- HeaderDecoder decoder = new HeaderDecoder();
- decodeHeaders(pp, decoder);
+
HttpHeadersImpl headers = decoder.headers();
HttpRequestImpl pushReq = HttpRequestImpl.createPushRequest(parentReq, headers);
Exchange<T> pushExch = new Exchange<>(pushReq, parent.exchange.multi);
@@ -549,7 +691,15 @@
}
void closeStream(int streamid) {
+ debug.log(Level.DEBUG, "Closed stream %d", streamid);
Stream<?> s = streams.remove(streamid);
+ if (s != null) {
+ // decrement the reference count on the HttpClientImpl
+ // to allow the SelectorManager thread to exit if no
+ // other operation is pending and the facade is no
+ // longer referenced.
+ client().unreference();
+ }
// ## Remove s != null. It is a hack for delayed cancellation,reset
if (s != null && !(s instanceof Stream.PushedStream)) {
// Since PushStreams have no request body, then they have no
@@ -579,9 +729,15 @@
private void protocolError(int errorCode)
throws IOException
{
+ protocolError(errorCode, null);
+ }
+
+ private void protocolError(int errorCode, String msg)
+ throws IOException
+ {
GoAwayFrame frame = new GoAwayFrame(0, errorCode);
sendFrame(frame);
- shutdown(new IOException("protocol error"));
+ shutdown(new IOException("protocol error" + (msg == null?"":(": " + msg))));
}
private void handleSettings(SettingsFrame frame)
@@ -655,7 +811,8 @@
ByteBufferReference ref = framesEncoder.encodeConnectionPreface(PREFACE_BYTES, sf);
Log.logFrames(sf, "OUT");
// send preface bytes and SettingsFrame together
- connection.write(ref.get());
+ connection.writeAsync(new ByteBufferReference[] {ref});
+ connection.flushAsync();
// mark preface sent.
framesController.markPrefaceSent();
Log.logTrace("PREFACE_BYTES sent");
@@ -669,6 +826,9 @@
// cause any pending data stored before the preface was sent to be
// flushed (see PrefaceController).
Log.logTrace("finished sending connection preface");
+ debug.log(Level.DEBUG, "Triggering processing of buffered data"
+ + " after sending connection preface");
+ subscriber.onNext(List.of(EMPTY_TRIGGER));
}
/**
@@ -682,22 +842,32 @@
/**
* Creates Stream with given id.
*/
- <T> Stream<T> createStream(Exchange<T> exchange) {
- Stream<T> stream = new Stream<>(client, this, exchange, windowController);
+ final <T> Stream<T> createStream(Exchange<T> exchange) {
+ Stream<T> stream = new Stream<>(client(), this, exchange, windowController);
return stream;
}
<T> Stream.PushedStream<?,T> createPushStream(Stream<T> parent, Exchange<T> pushEx) {
PushGroup<?,T> pg = parent.exchange.getPushGroup();
- return new Stream.PushedStream<>(pg, client, this, parent, pushEx);
+ return new Stream.PushedStream<>(pg, client(), this, parent, pushEx);
}
<T> void putStream(Stream<T> stream, int streamid) {
+ // increment the reference count on the HttpClientImpl
+ // to prevent the SelectorManager thread from exiting until
+ // the stream is closed.
+ client().reference();
streams.put(streamid, stream);
}
void deleteStream(int streamid) {
- streams.remove(streamid);
+ if (streams.remove(streamid) != null) {
+ // decrement the reference count on the HttpClientImpl
+ // to allow the SelectorManager thread to exit if no
+ // other operation is pending and the facade is no
+ // longer referenced.
+ client().unreference();
+ }
windowController.removeStream(streamid);
}
@@ -728,7 +898,7 @@
// There can be no concurrent access to this buffer as all access to this buffer
// and its content happen within a single critical code block section protected
// by the sendLock. / (see sendFrame())
- private ByteBufferPool headerEncodingPool = new ByteBufferPool();
+ private final ByteBufferPool headerEncodingPool = new ByteBufferPool();
private ByteBufferReference getHeaderBuffer(int maxFrameSize) {
ByteBufferReference ref = headerEncodingPool.get(maxFrameSize);
@@ -872,6 +1042,208 @@
}
}
+ /**
+ * Returns the TubeSubscriber for reading from the connection flow.
+ * @return the TubeSubscriber for reading from the connection flow.
+ */
+ TubeSubscriber subscriber() {
+ return subscriber;
+ }
+
+ /**
+ * A simple tube subscriber for reading from the connection flow.
+ */
+ final class Http2TubeSubscriber implements TubeSubscriber {
+ volatile Flow.Subscription subscription;
+ volatile boolean completed;
+ volatile boolean dropped;
+ volatile Throwable error;
+ final ConcurrentLinkedQueue<ByteBuffer> queue
+ = new ConcurrentLinkedQueue<>();
+ final SequentialScheduler scheduler = new SequentialScheduler(
+ new SynchronizedRestartableTask(this::processQueue));
+
+ final void processQueue() {
+ try {
+ while (!queue.isEmpty() && !scheduler.isStopped()) {
+ ByteBuffer buffer = queue.poll();
+ debug.log(Level.DEBUG,
+ "sending %d to Http2Connection.asyncReceive",
+ buffer.remaining());
+ asyncReceive(ByteBufferReference.of(buffer));
+ }
+ } catch (Throwable t) {
+ Throwable x = error;
+ if (x == null) error = t;
+ } finally {
+ Throwable x = error;
+ if (x != null) {
+ debug.log(Level.DEBUG, "Stopping scheduler", x);
+ scheduler.stop();
+ Http2Connection.this.shutdown(x);
+ }
+ }
+ }
+
+
+ public void onSubscribe(Flow.Subscription subscription) {
+ // supports being called multiple time.
+ // doesn't cancel the previous subscription, since that is
+ // most probably the same as the new subscription.
+ assert this.subscription == null || dropped == false;
+ this.subscription = subscription;
+ dropped = false;
+ // TODO FIXME: request(1) should be done by the delegate.
+ if (!completed) {
+ debug.log(Level.DEBUG, "onSubscribe: requesting Long.MAX_VALUE for reading");
+ subscription.request(Long.MAX_VALUE);
+ } else {
+ debug.log(Level.DEBUG, "onSubscribe: already completed");
+ }
+ }
+
+ @Override
+ public void onNext(List<ByteBuffer> item) {
+ debug.log(Level.DEBUG, () -> "onNext: got " + Utils.remaining(item)
+ + " bytes in " + item.size() + " buffers");
+ queue.addAll(item);
+ scheduler.deferOrSchedule(client().theExecutor());
+ }
+
+ @Override
+ public void onError(Throwable throwable) {
+ debug.log(Level.DEBUG, () -> "onError: " + throwable);
+ error = throwable;
+ completed = true;
+ scheduler.deferOrSchedule(client().theExecutor());
+ }
+
+ @Override
+ public void onComplete() {
+ debug.log(Level.DEBUG, "EOF");
+ error = new EOFException("EOF reached while reading");
+ completed = true;
+ scheduler.deferOrSchedule(client().theExecutor());
+ }
+
+ public void dropSubscription() {
+ debug.log(Level.DEBUG, "dropSubscription");
+ // we could probably set subscription to null here...
+ // then we might not need the 'dropped' boolean?
+ dropped = true;
+ }
+ }
+
+ @Override
+ public final String toString() {
+ return dbgString();
+ }
+
+ final String dbgString() {
+ return "Http2Connection("
+ + connection.getConnectionFlow() + ")";
+ }
+
+ final class LoggingHeaderDecoder extends HeaderDecoder {
+
+ private final HeaderDecoder delegate;
+ private final System.Logger debugHpack =
+ Utils.getHpackLogger(this::dbgString, DEBUG_HPACK);
+
+ LoggingHeaderDecoder(HeaderDecoder delegate) {
+ this.delegate = delegate;
+ }
+
+ String dbgString() {
+ return Http2Connection.this.dbgString() + "/LoggingHeaderDecoder";
+ }
+
+ @Override
+ public void onDecoded(CharSequence name, CharSequence value) {
+ delegate.onDecoded(name, value);
+ }
+
+ @Override
+ public void onIndexed(int index,
+ CharSequence name,
+ CharSequence value) {
+ debugHpack.log(Level.DEBUG, "onIndexed(%s, %s, %s)%n",
+ index, name, value);
+ delegate.onIndexed(index, name, value);
+ }
+
+ @Override
+ public void onLiteral(int index,
+ CharSequence name,
+ CharSequence value,
+ boolean valueHuffman) {
+ debugHpack.log(Level.DEBUG, "onLiteral(%s, %s, %s, %s)%n",
+ index, name, value, valueHuffman);
+ delegate.onLiteral(index, name, value, valueHuffman);
+ }
+
+ @Override
+ public void onLiteral(CharSequence name,
+ boolean nameHuffman,
+ CharSequence value,
+ boolean valueHuffman) {
+ debugHpack.log(Level.DEBUG, "onLiteral(%s, %s, %s, %s)%n",
+ name, nameHuffman, value, valueHuffman);
+ delegate.onLiteral(name, nameHuffman, value, valueHuffman);
+ }
+
+ @Override
+ public void onLiteralNeverIndexed(int index,
+ CharSequence name,
+ CharSequence value,
+ boolean valueHuffman) {
+ debugHpack.log(Level.DEBUG, "onLiteralNeverIndexed(%s, %s, %s, %s)%n",
+ index, name, value, valueHuffman);
+ delegate.onLiteralNeverIndexed(index, name, value, valueHuffman);
+ }
+
+ @Override
+ public void onLiteralNeverIndexed(CharSequence name,
+ boolean nameHuffman,
+ CharSequence value,
+ boolean valueHuffman) {
+ debugHpack.log(Level.DEBUG, "onLiteralNeverIndexed(%s, %s, %s, %s)%n",
+ name, nameHuffman, value, valueHuffman);
+ delegate.onLiteralNeverIndexed(name, nameHuffman, value, valueHuffman);
+ }
+
+ @Override
+ public void onLiteralWithIndexing(int index,
+ CharSequence name,
+ CharSequence value,
+ boolean valueHuffman) {
+ debugHpack.log(Level.DEBUG, "onLiteralWithIndexing(%s, %s, %s, %s)%n",
+ index, name, value, valueHuffman);
+ delegate.onLiteralWithIndexing(index, name, value, valueHuffman);
+ }
+
+ @Override
+ public void onLiteralWithIndexing(CharSequence name,
+ boolean nameHuffman,
+ CharSequence value,
+ boolean valueHuffman) {
+ debugHpack.log(Level.DEBUG, "onLiteralWithIndexing(%s, %s, %s, %s)%n",
+ name, nameHuffman, value, valueHuffman);
+ delegate.onLiteralWithIndexing(name, nameHuffman, value, valueHuffman);
+ }
+
+ @Override
+ public void onSizeUpdate(int capacity) {
+ debugHpack.log(Level.DEBUG, "onSizeUpdate(%s)%n", capacity);
+ delegate.onSizeUpdate(capacity);
+ }
+
+ @Override
+ HttpHeadersImpl headers() {
+ return delegate.headers();
+ }
+ }
+
static class HeaderDecoder implements DecodingCallback {
HttpHeadersImpl headers;
--- a/src/jdk.incubator.httpclient/share/classes/jdk/incubator/http/HttpClient.java Sun Nov 05 17:05:57 2017 +0000
+++ b/src/jdk.incubator.httpclient/share/classes/jdk/incubator/http/HttpClient.java Sun Nov 05 17:32:13 2017 +0000
@@ -1,5 +1,5 @@
/*
- * Copyright (c) 2015, 2016, Oracle and/or its affiliates. All rights reserved.
+ * Copyright (c) 2015, 2017, Oracle and/or its affiliates. All rights reserved.
* DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER.
*
* This code is free software; you can redistribute it and/or modify it
@@ -34,6 +34,8 @@
import java.util.Optional;
import java.util.concurrent.CompletableFuture;
import java.util.concurrent.Executor;
+import java.util.concurrent.Executors;
+import java.util.concurrent.ThreadFactory;
import javax.net.ssl.SSLContext;
import javax.net.ssl.SSLParameters;
@@ -103,28 +105,31 @@
public abstract Builder cookieManager(CookieManager cookieManager);
/**
- * Sets an {@code SSLContext}. If a security manager is set, then the caller
- * must have the {@link java.net.NetPermission NetPermission}
- * ({@code "setSSLContext"})
+ * Sets an {@code SSLContext}.
*
- * <p> The effect of not calling this method, is that a default {@link
- * javax.net.ssl.SSLContext} is used, which is normally adequate for
- * client applications that do not need to specify protocols, or require
- * client authentication.
+ * <p> If this method is not invoked before {@linkplain #build() build},
+ * then the {@link SSLContext#getDefault() default context} is used,
+ * which is normally adequate for client applications that do not need
+ * to specify protocols, or require client authentication.
*
* @param sslContext the SSLContext
* @return this builder
- * @throws SecurityException if a security manager is set and the
- * caller does not have any required permission
+ * @throws SecurityException if a security manager has been installed
+ * and it denies {@linkplain java.net.NetPermission}
+ * ({@code "setSSLContext"})
*/
public abstract Builder sslContext(SSLContext sslContext);
/**
- * Sets an {@code SSLParameters}. If this method is not called, then a default
- * set of parameters are used. The contents of the given object are
- * copied. Some parameters which are used internally by the HTTP protocol
- * implementation (such as application protocol list) should not be set
- * by callers, as they are ignored.
+ * Sets an {@code SSLParameters}.
+ *
+ * <p> If this method is not invoked before {@linkplain #build() build},
+ * then a default, implementation specific, set of parameters are used.
+ *
+ * <p> Some parameters which are used internally by the HTTP Client
+ * implementation (such as the application protocol list) should not be
+ * set by callers, as they may be ignored. The contents of the given
+ * object are copied.
*
* @param sslParameters the SSLParameters
* @return this builder
@@ -132,10 +137,17 @@
public abstract Builder sslParameters(SSLParameters sslParameters);
/**
- * Sets the executor to be used for asynchronous tasks. If this method is
- * not called, a default executor is set, which is the one returned from {@link
- * java.util.concurrent.Executors#newCachedThreadPool()
- * Executors.newCachedThreadPool}.
+ * Sets the executor to be used for asynchronous and dependent tasks.
+ *
+ * <p> If this method is not invoked before {@linkplain #build() build},
+ * a default executor is created for each newly built {@code HttpClient}.
+ * The default executor uses a {@linkplain
+ * Executors#newCachedThreadPool(ThreadFactory) cached thread pool}, with
+ * a custom thread factory.
+ *
+ * @implNote If a security manager has been installed, the thread
+ * factory creates threads that run with an access control context that
+ * has no permissions.
*
* @param executor the Executor
* @return this builder
@@ -144,8 +156,9 @@
/**
* Specifies whether requests will automatically follow redirects issued
- * by the server. This setting can be overridden on each request. The
- * default value for this setting is {@link Redirect#NEVER NEVER}
+ * by the server. The default redirection policy for clients built by
+ * this builder, if this method has not been invoked, is {@link
+ * Redirect#NEVER NEVER}.
*
* @param policy the redirection policy
* @return this builder
@@ -180,12 +193,12 @@
public abstract Builder priority(int priority);
/**
- * Sets a {@link java.net.ProxySelector} for this client. If no selector
- * is set, then no proxies are used. If a {@code null} parameter is
- * given then the system wide default proxy selector is used.
+ * Sets a {@link java.net.ProxySelector}.
*
- * @implNote {@link java.net.ProxySelector#of(InetSocketAddress)}
- * provides a {@code ProxySelector} which uses one proxy for all requests.
+ * @implNote {@link ProxySelector#of(InetSocketAddress)}
+ * provides a {@code ProxySelector} which uses a single proxy for all
+ * requests. The system-wide proxy selector can be retrieved by
+ * {@link ProxySelector#getDefault()}.
*
* @param selector the ProxySelector
* @return this builder
@@ -211,50 +224,55 @@
/**
- * Returns an {@code Optional} which contains this client's {@link
- * CookieManager}. If no {@code CookieManager} was set in this client's builder,
- * then the {@code Optional} is empty.
+ * Returns an {@code Optional} containing this client's {@link
+ * CookieManager}. If no {@code CookieManager} was set in this client's
+ * builder, then the {@code Optional} is empty.
*
* @return an {@code Optional} containing this client's {@code CookieManager}
*/
public abstract Optional<CookieManager> cookieManager();
/**
- * Returns the follow-redirects setting for this client. The default value
- * for this setting is {@link HttpClient.Redirect#NEVER}
+ * Returns the follow redirects policy for this client. The default value
+ * for client's built by builders that do not specify a redirect policy is
+ * {@link HttpClient.Redirect#NEVER NEVER}.
*
* @return this client's follow redirects setting
*/
public abstract Redirect followRedirects();
/**
- * Returns an {@code Optional} containing the {@code ProxySelector} for this client.
- * If no proxy is set then the {@code Optional} is empty.
+ * Returns an {@code Optional} containing this client's {@code ProxySelector}.
+ * If no proxy selector was set in this client's builder, then the {@code
+ * Optional} is empty.
*
* @return an {@code Optional} containing this client's proxy selector
*/
public abstract Optional<ProxySelector> proxy();
/**
- * Returns the {@code SSLContext}, if one was set on this client. If a security
- * manager is set, then the caller must have the
- * {@link java.net.NetPermission NetPermission}("getSSLContext") permission.
- * If no {@code SSLContext} was set, then the default context is returned.
+ * Returns this client's {@code SSLContext}.
+ *
+ * <p> If no {@code SSLContext} was set in this client's builder, then the
+ * {@linkplain SSLContext#getDefault() default context} is returned.
*
* @return this client's SSLContext
- * @throws SecurityException if the caller does not have permission to get
- * the SSLContext
+ * @throws SecurityException if a security manager has been installed
+ * and it denies {@linkplain java.net.NetPermission}
+ * ({@code "getSSLContext"})
*/
public abstract SSLContext sslContext();
/**
- * Returns an {@code Optional} containing the {@link SSLParameters} set on
- * this client. If no {@code SSLParameters} were set in the client's builder,
- * then the {@code Optional} is empty.
+ * Returns a copy of this client's {@link SSLParameters}.
*
- * @return an {@code Optional} containing this client's {@code SSLParameters}
+ * <p> If no {@code SSLParameters} were set in the client's builder, then an
+ * implementation specific default set of parameters, that the client will
+ * use, is returned.
+ *
+ * @return this client's {@code SSLParameters}
*/
- public abstract Optional<SSLParameters> sslParameters();
+ public abstract SSLParameters sslParameters();
/**
* Returns an {@code Optional} containing the {@link Authenticator} set on
@@ -274,14 +292,18 @@
public abstract HttpClient.Version version();
/**
- * Returns the {@code Executor} set on this client. If an {@code
- * Executor} was not set on the client's builder, then a default
- * object is returned. The default {@code Executor} is created independently
- * for each client.
+ * Returns an {@code Optional} containing this client's {@linkplain
+ * Executor}. If no {@code Executor} was set in the client's builder,
+ * then the {@code Optional} is empty.
*
- * @return this client's Executor
+ * <p> Even though this method may return an empty optional, the {@code
+ * HttpClient} may still have an non-exposed {@linkplain
+ * HttpClient.Builder#executor(Executor) default executor} that is used for
+ * executing asynchronous and dependent tasks.
+ *
+ * @return an {@code Optional} containing this client's {@code Executor}
*/
- public abstract Executor executor();
+ public abstract Optional<Executor> executor();
/**
* The HTTP protocol version.
@@ -351,6 +373,11 @@
* @return the response body
* @throws java.io.IOException if an I/O error occurs when sending or receiving
* @throws java.lang.InterruptedException if the operation is interrupted
+ * @throws SecurityException If a security manager has been installed
+ * and it denies {@link java.net.URLPermission access} to the
+ * URL in the given request, or proxy if one is configured.
+ * See HttpRequest for further information about
+ * <a href="HttpRequest.html#securitychecks">security checks</a>.
*/
public abstract <T> HttpResponse<T>
send(HttpRequest req, HttpResponse.BodyHandler<T> responseBodyHandler)
@@ -360,6 +387,12 @@
* Sends the given request asynchronously using this client and the given
* response handler.
*
+ * <p> The returned completable future is completed with a SecurityException
+ * if a security manager has been installed and it denies {@link
+ * java.net.URLPermission access} to the URI in the given request, or proxy
+ * if one is configured. See HttpRequest for further information about
+ * <a href="HttpRequest.html#securitychecks">security checks</a>.
+ *
* @param <T> the response body type
* @param req the request
* @param responseBodyHandler the response body handler
@@ -372,14 +405,20 @@
* Sends the given request asynchronously using this client and the given
* multi response handler.
*
+ * <p> The returned completable future is completed with a SecurityException
+ * if a security manager has been installed and it denies {@link
+ * java.net.URLPermission access} to the URI in the given request, or proxy
+ * if one is configured. See HttpRequest for further information about
+ * <a href="HttpRequest.html#securitychecks">security checks</a>.
+ *
* @param <U> a type representing the aggregated results
* @param <T> a type representing all of the response bodies
* @param req the request
- * @param multiProcessor the MultiProcessor for the request
+ * @param multiSubscriber the multiSubscriber for the request
* @return a {@code CompletableFuture<U>}
*/
public abstract <U, T> CompletableFuture<U>
- sendAsync(HttpRequest req, HttpResponse.MultiProcessor<U, T> multiProcessor);
+ sendAsync(HttpRequest req, HttpResponse.MultiSubscriber<U, T> multiSubscriber);
/**
* Creates a builder of {@link WebSocket} instances connected to the given
--- a/src/jdk.incubator.httpclient/share/classes/jdk/incubator/http/HttpClientBuilderImpl.java Sun Nov 05 17:05:57 2017 +0000
+++ b/src/jdk.incubator.httpclient/share/classes/jdk/incubator/http/HttpClientBuilderImpl.java Sun Nov 05 17:32:13 2017 +0000
@@ -1,5 +1,5 @@
/*
- * Copyright (c) 2015, 2016, Oracle and/or its affiliates. All rights reserved.
+ * Copyright (c) 2015, 2017, Oracle and/or its affiliates. All rights reserved.
* DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER.
*
* This code is free software; you can redistribute it and/or modify it
@@ -27,6 +27,7 @@
import java.net.Authenticator;
import java.net.CookieManager;
+import java.net.NetPermission;
import java.net.ProxySelector;
import java.util.concurrent.Executor;
import javax.net.ssl.SSLContext;
@@ -58,7 +59,11 @@
@Override
public HttpClientBuilderImpl sslContext(SSLContext sslContext) {
requireNonNull(sslContext);
- Utils.checkNetPermission("setSSLContext");
+ SecurityManager sm = System.getSecurityManager();
+ if (sm != null) {
+ NetPermission np = new NetPermission("setSSLContext");
+ sm.checkPermission(np);
+ }
this.sslContext = sslContext;
return this;
}
@@ -67,7 +72,7 @@
@Override
public HttpClientBuilderImpl sslParameters(SSLParameters sslParameters) {
requireNonNull(sslParameters);
- this.sslParams = sslParameters;
+ this.sslParams = Utils.copySSLParameters(sslParameters);
return this;
}
--- /dev/null Thu Jan 01 00:00:00 1970 +0000
+++ b/src/jdk.incubator.httpclient/share/classes/jdk/incubator/http/HttpClientFacade.java Sun Nov 05 17:32:13 2017 +0000
@@ -0,0 +1,143 @@
+/*
+ * Copyright (c) 2017, Oracle and/or its affiliates. All rights reserved.
+ * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER.
+ *
+ * This code is free software; you can redistribute it and/or modify it
+ * under the terms of the GNU General Public License version 2 only, as
+ * published by the Free Software Foundation. Oracle designates this
+ * particular file as subject to the "Classpath" exception as provided
+ * by Oracle in the LICENSE file that accompanied this code.
+ *
+ * This code is distributed in the hope that it will be useful, but WITHOUT
+ * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or
+ * FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License
+ * version 2 for more details (a copy is included in the LICENSE file that
+ * accompanied this code).
+ *
+ * You should have received a copy of the GNU General Public License version
+ * 2 along with this work; if not, write to the Free Software Foundation,
+ * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA.
+ *
+ * Please contact Oracle, 500 Oracle Parkway, Redwood Shores, CA 94065 USA
+ * or visit www.oracle.com if you need additional information or have any
+ * questions.
+ */
+
+package jdk.incubator.http;
+
+import java.io.IOException;
+import java.lang.ref.Reference;
+import java.net.Authenticator;
+import java.net.CookieManager;
+import java.net.ProxySelector;
+import java.net.URI;
+import java.util.Optional;
+import java.util.concurrent.CompletableFuture;
+import java.util.concurrent.Executor;
+import javax.net.ssl.SSLContext;
+import javax.net.ssl.SSLParameters;
+
+/**
+ * An HttpClientFacade is a simple class that wraps an HttpClient implementation
+ * and delegates everything to its implementation delegate.
+ */
+final class HttpClientFacade extends HttpClient {
+
+ final HttpClientImpl impl;
+
+ /**
+ * Creates an HttpClientFacade.
+ */
+ HttpClientFacade(HttpClientImpl impl) {
+ this.impl = impl;
+ }
+
+ @Override
+ public Optional<CookieManager> cookieManager() {
+ return impl.cookieManager();
+ }
+
+ @Override
+ public Redirect followRedirects() {
+ return impl.followRedirects();
+ }
+
+ @Override
+ public Optional<ProxySelector> proxy() {
+ return impl.proxy();
+ }
+
+ @Override
+ public SSLContext sslContext() {
+ return impl.sslContext();
+ }
+
+ @Override
+ public SSLParameters sslParameters() {
+ return impl.sslParameters();
+ }
+
+ @Override
+ public Optional<Authenticator> authenticator() {
+ return impl.authenticator();
+ }
+
+ @Override
+ public HttpClient.Version version() {
+ return impl.version();
+ }
+
+ @Override
+ public Optional<Executor> executor() {
+ return impl.executor();
+ }
+
+ @Override
+ public <T> HttpResponse<T>
+ send(HttpRequest req, HttpResponse.BodyHandler<T> responseBodyHandler)
+ throws IOException, InterruptedException
+ {
+ try {
+ return impl.send(req, responseBodyHandler);
+ } finally {
+ Reference.reachabilityFence(this);
+ }
+ }
+
+ @Override
+ public <T> CompletableFuture<HttpResponse<T>>
+ sendAsync(HttpRequest req, HttpResponse.BodyHandler<T> responseBodyHandler) {
+ try {
+ return impl.sendAsync(req, responseBodyHandler);
+ } finally {
+ Reference.reachabilityFence(this);
+ }
+ }
+
+ @Override
+ public <U, T> CompletableFuture<U>
+ sendAsync(HttpRequest req, HttpResponse.MultiSubscriber<U, T> multiSubscriber) {
+ try {
+ return impl.sendAsync(req, multiSubscriber);
+ } finally {
+ Reference.reachabilityFence(this);
+ }
+ }
+
+ @Override
+ public WebSocket.Builder newWebSocketBuilder(URI uri,
+ WebSocket.Listener listener)
+ {
+ try {
+ return impl.newWebSocketBuilder(uri, listener);
+ } finally {
+ Reference.reachabilityFence(this);
+ }
+ }
+
+ @Override
+ public String toString() {
+ // Used by tests to get the client's id.
+ return impl.toString();
+ }
+}
--- a/src/jdk.incubator.httpclient/share/classes/jdk/incubator/http/HttpClientImpl.java Sun Nov 05 17:05:57 2017 +0000
+++ b/src/jdk.incubator.httpclient/share/classes/jdk/incubator/http/HttpClientImpl.java Sun Nov 05 17:32:13 2017 +0000
@@ -28,33 +28,47 @@
import javax.net.ssl.SSLContext;
import javax.net.ssl.SSLParameters;
import java.io.IOException;
+import java.lang.System.Logger.Level;
import java.lang.ref.WeakReference;
import java.net.Authenticator;
import java.net.CookieManager;
+import java.net.NetPermission;
import java.net.ProxySelector;
import java.net.URI;
+import java.nio.channels.CancelledKeyException;
import java.nio.channels.ClosedChannelException;
import java.nio.channels.SelectableChannel;
import java.nio.channels.SelectionKey;
import java.nio.channels.Selector;
import java.nio.channels.SocketChannel;
+import java.security.AccessControlContext;
+import java.security.AccessController;
import java.security.NoSuchAlgorithmException;
+import java.security.PrivilegedAction;
import java.time.Instant;
import java.time.temporal.ChronoUnit;
import java.util.ArrayList;
+import java.util.HashSet;
import java.util.Iterator;
import java.util.List;
import java.util.Optional;
import java.util.Set;
import java.util.TreeSet;
import java.util.concurrent.CompletableFuture;
+import java.util.concurrent.CompletionException;
import java.util.concurrent.Executor;
import java.util.concurrent.Executors;
import java.util.concurrent.ThreadFactory;
+import java.util.concurrent.atomic.AtomicInteger;
+import java.util.concurrent.atomic.AtomicLong;
import java.util.stream.Stream;
+import jdk.incubator.http.HttpResponse.BodyHandler;
+import jdk.incubator.http.HttpResponse.MultiSubscriber;
import jdk.incubator.http.internal.common.Log;
+import jdk.incubator.http.internal.common.Pair;
import jdk.incubator.http.internal.common.Utils;
import jdk.incubator.http.internal.websocket.BuilderImpl;
+import jdk.internal.misc.InnocuousThread;
/**
* Client implementation. Contains all configuration information and also
@@ -63,20 +77,39 @@
*/
class HttpClientImpl extends HttpClient {
+ static final boolean DEBUG = Utils.DEBUG; // Revisit: temporary dev flag.
+ static final boolean DEBUGELAPSED = Utils.ASSERTIONSENABLED || DEBUG; // Revisit: temporary dev flag.
+ static final boolean DEBUGTIMEOUT = false; // Revisit: temporary dev flag.
+ final System.Logger debug = Utils.getDebugLogger(this::dbgString, DEBUG);
+ final System.Logger debugelapsed = Utils.getDebugLogger(this::dbgString, DEBUGELAPSED);
+ final System.Logger debugtimeout = Utils.getDebugLogger(this::dbgString, DEBUGTIMEOUT);
+ static final AtomicLong CLIENT_IDS = new AtomicLong();
+
// Define the default factory as a static inner class
// that embeds all the necessary logic to avoid
// the risk of using a lambda that might keep a reference on the
// HttpClient instance from which it was created (helps with
// heapdump analysis).
private static final class DefaultThreadFactory implements ThreadFactory {
- private DefaultThreadFactory() {}
+ private final String namePrefix;
+ private final AtomicInteger nextId = new AtomicInteger();
+
+ DefaultThreadFactory(long clientID) {
+ namePrefix = "HttpClient-" + clientID + "-Worker-";
+ }
+
@Override
public Thread newThread(Runnable r) {
- Thread t = new Thread(null, r, "HttpClient_worker", 0, true);
+ String name = namePrefix + nextId.getAndIncrement();
+ Thread t;
+ if (System.getSecurityManager() == null) {
+ t = new Thread(null, r, name, 0, false);
+ } else {
+ t = InnocuousThread.newThread(name, r);
+ }
t.setDaemon(true);
return t;
}
- static final ThreadFactory INSTANCE = new DefaultThreadFactory();
}
private final CookieManager cookieManager;
@@ -86,23 +119,95 @@
private final Version version;
private final ConnectionPool connections;
private final Executor executor;
+ private final boolean isDefaultExecutor;
// Security parameters
private final SSLContext sslContext;
private final SSLParameters sslParams;
private final SelectorManager selmgr;
private final FilterFactory filters;
private final Http2ClientImpl client2;
+ private final long id;
+ private final String dbgTag;
+
+ // This reference is used to keep track of the facade HttpClient
+ // that was returned to the application code.
+ // It makes it possible to know when the application no longer
+ // holds any reference to the HttpClient.
+ // Unfortunately, this information is not enough to know when
+ // to exit the SelectorManager thread. Because of the asynchronous
+ // nature of the API, we also need to wait until all pending operations
+ // have completed.
+ private final WeakReference<HttpClientFacade> facadeRef;
+
+ // This counter keeps track of the number of operations pending
+ // on the HttpClient. The SelectorManager thread will wait
+ // until there are no longer any pending operations and the
+ // facadeRef is cleared before exiting.
+ //
+ // The pendingOperationCount is incremented every time a send/sendAsync
+ // operation is invoked on the HttpClient, and is decremented when
+ // the HttpResponse<T> object is returned to the user.
+ // However, at this point, the body may not have been fully read yet.
+ // This is the case when the response T is implemented as a streaming
+ // subscriber (such as an InputStream).
+ //
+ // To take care of this issue the pendingOperationCount will additionally
+ // be incremented/decremented in the following cases:
+ //
+ // 1. For HTTP/2 it is incremented when a stream is added to the
+ // Http2Connection streams map, and decreased when the stream is removed
+ // from the map. This should also take care of push promises.
+ // 2. For WebSocket the count is increased when creating a
+ // DetachedConnectionChannel for the socket, and decreased
+ // when the the channel is closed.
+ // In addition, the HttpClient facade is passed to the WebSocket builder,
+ // (instead of the client implementation delegate).
+ // 3. For HTTP/1.1 the count is incremented before starting to parse the body
+ // response, and decremented when the parser has reached the end of the
+ // response body flow.
+ //
+ // This should ensure that the selector manager thread remains alive until
+ // the response has been fully received or the web socket is closed.
+ private final AtomicLong pendingOperationCount = new AtomicLong();
+ private final AtomicLong pendingWebSocketCount = new AtomicLong();
+ private final AtomicLong pendingHttpRequestCount = new AtomicLong();
/** A Set of, deadline first, ordered timeout events. */
private final TreeSet<TimeoutEvent> timeouts;
- public static HttpClientImpl create(HttpClientBuilderImpl builder) {
- HttpClientImpl impl = new HttpClientImpl(builder);
- impl.start();
- return impl;
+ /**
+ * This is a bit tricky:
+ * 1. an HttpClientFacade has a final HttpClientImpl field.
+ * 2. an HttpClientImpl has a final WeakReference<HttpClientFacade> field,
+ * where the referent is the facade created for that instance.
+ * 3. We cannot just create the HttpClientFacade in the HttpClientImpl
+ * constructor, because it would be only weakly referenced and could
+ * be GC'ed before we can return it.
+ * The solution is to use an instance of SingleFacadeFactory which will
+ * allow the caller of new HttpClientImpl(...) to retrieve the facade
+ * after the HttpClientImpl has been created.
+ */
+ private static final class SingleFacadeFactory {
+ HttpClientFacade facade;
+ HttpClientFacade createFacade(HttpClientImpl impl) {
+ assert facade == null;
+ return (facade = new HttpClientFacade(impl));
+ }
}
- private HttpClientImpl(HttpClientBuilderImpl builder) {
+ static HttpClientFacade create(HttpClientBuilderImpl builder) {
+ SingleFacadeFactory facadeFactory = new SingleFacadeFactory();
+ HttpClientImpl impl = new HttpClientImpl(builder, facadeFactory);
+ impl.start();
+ assert facadeFactory.facade != null;
+ assert impl.facadeRef.get() == facadeFactory.facade;
+ return facadeFactory.facade;
+ }
+
+ private HttpClientImpl(HttpClientBuilderImpl builder,
+ SingleFacadeFactory facadeFactory) {
+ id = CLIENT_IDS.incrementAndGet();
+ dbgTag = "HttpClientImpl(" + id +")";
if (builder.sslContext == null) {
try {
sslContext = SSLContext.getDefault();
@@ -114,10 +219,13 @@
}
Executor ex = builder.executor;
if (ex == null) {
- ex = Executors.newCachedThreadPool(DefaultThreadFactory.INSTANCE);
+ ex = Executors.newCachedThreadPool(new DefaultThreadFactory(id));
+ isDefaultExecutor = true;
} else {
ex = builder.executor;
+ isDefaultExecutor = false;
}
+ facadeRef = new WeakReference<>(facadeFactory.createFacade(this));
client2 = new Http2ClientImpl(this);
executor = ex;
cookieManager = builder.cookieManager;
@@ -135,7 +243,7 @@
} else {
sslParams = builder.sslParams;
}
- connections = new ConnectionPool();
+ connections = new ConnectionPool(id);
connections.start();
timeouts = new TreeSet<>();
try {
@@ -147,30 +255,97 @@
selmgr.setDaemon(true);
filters = new FilterFactory();
initFilters();
+ assert facadeRef.get() != null;
}
private void start() {
selmgr.start();
}
+ // Called from the SelectorManager thread, just before exiting.
+ // Clears the HTTP/1.1 and HTTP/2 cache, ensuring that the connections
+ // that may be still lingering there are properly closed (and their
+ // possibly still opened SocketChannel released).
+ private void stop() {
+ // Clears HTTP/1.1 cache and close its connections
+ connections.stop();
+ // Clears HTTP/2 cache and close its connections.
+ client2.stop();
+ }
+
private static SSLParameters getDefaultParams(SSLContext ctx) {
SSLParameters params = ctx.getSupportedSSLParameters();
params.setProtocols(new String[]{"TLSv1.2"});
return params;
}
+ // Returns the facade that was returned to the application code.
+ // May be null if that facade is no longer referenced.
+ final HttpClientFacade facade() {
+ return facadeRef.get();
+ }
+
+ // Increments the pendingOperationCount.
+ final long reference() {
+ pendingHttpRequestCount.incrementAndGet();
+ return pendingOperationCount.incrementAndGet();
+ }
+
+ // Decrements the pendingOperationCount.
+ final long unreference() {
+ final long count = pendingOperationCount.decrementAndGet();
+ final long httpCount = pendingHttpRequestCount.decrementAndGet();
+ final long webSocketCount = pendingWebSocketCount.get();
+ if (count == 0 && facade() == null) {
+ selmgr.wakeupSelector();
+ }
+ assert httpCount >= 0 : "count of HTTP operations < 0";
+ assert webSocketCount >= 0 : "count of WS operations < 0";
+ assert count >= 0 : "count of pending operations < 0";
+ return count;
+ }
+
+ // Increments the pendingOperationCount.
+ final long webSocketOpen() {
+ pendingWebSocketCount.incrementAndGet();
+ return pendingOperationCount.incrementAndGet();
+ }
+
+ // Decrements the pendingOperationCount.
+ final long webSocketClose() {
+ final long count = pendingOperationCount.decrementAndGet();
+ final long webSocketCount = pendingWebSocketCount.decrementAndGet();
+ final long httpCount = pendingHttpRequestCount.get();
+ if (count == 0 && facade() == null) {
+ selmgr.wakeupSelector();
+ }
+ assert httpCount >= 0 : "count of HTTP operations < 0";
+ assert webSocketCount >= 0 : "count of WS operations < 0";
+ assert count >= 0 : "count of pending operations < 0";
+ return count;
+ }
+
+ // Returns the pendingOperationCount.
+ final long referenceCount() {
+ return pendingOperationCount.get();
+ }
+
+ // Called by the SelectorManager thread to figure out whether it's time
+ // to terminate.
+ final boolean isReferenced() {
+ HttpClient facade = facade();
+ return facade != null || referenceCount() > 0;
+ }
+
/**
- * Wait for activity on given exchange (assuming blocking = false).
- * It's a no-op if blocking = true. In particular, the following occurs
- * in the SelectorManager thread.
+ * Wait for activity on given exchange.
+ * The following occurs in the SelectorManager thread.
*
- * 1) mark the connection non-blocking
- * 2) add to selector
- * 3) If selector fires for this exchange then
- * 4) - mark connection as blocking
- * 5) - call AsyncEvent.handle()
+ * 1) add to selector
+ * 2) If selector fires for this exchange then
+ * call AsyncEvent.handle()
*
- * If exchange needs to block again, then call registerEvent() again
+ * If exchange needs to change interest ops, then call registerEvent() again.
*/
void registerEvent(AsyncEvent exchange) throws IOException {
selmgr.register(exchange);
@@ -184,131 +359,178 @@
selmgr.cancel(s);
}
+ /**
+ * Allows an AsyncEvent to modify its interestOps.
+ * @param event The modified event.
+ */
+ void eventUpdated(AsyncEvent event) throws ClosedChannelException {
+ assert !(event instanceof AsyncTriggerEvent);
+ selmgr.eventUpdated(event);
+ }
+
+ boolean isSelectorThread() {
+ return Thread.currentThread() == selmgr;
+ }
Http2ClientImpl client2() {
return client2;
}
- /*
- @Override
- public ByteBuffer getBuffer() {
- return pool.getBuffer();
- }
-
- // SSL buffers are larger. Manage separately
-
- int size = 16 * 1024;
-
- ByteBuffer getSSLBuffer() {
- return ByteBuffer.allocate(size);
- }
-
- /**
- * Return a new buffer that's a bit bigger than the given one
- *
- * @param buf
- * @return
- *
- ByteBuffer reallocSSLBuffer(ByteBuffer buf) {
- size = buf.capacity() * 12 / 10; // 20% bigger
- return ByteBuffer.allocate(size);
- }
-
- synchronized void returnSSLBuffer(ByteBuffer buf) {
- if (buf.capacity() >= size)
- sslBuffers.add(0, buf);
+ private void debugCompleted(String tag, long startNanos, HttpRequest req) {
+ if (debugelapsed.isLoggable(Level.DEBUG)) {
+ debugelapsed.log(Level.DEBUG, () -> tag + " elapsed "
+ + (System.nanoTime() - startNanos)/1000_000L
+ + " millis for " + req.method()
+ + " to " + req.uri());
+ }
}
@Override
- public void returnBuffer(ByteBuffer buffer) {
- pool.returnBuffer(buffer);
- }
- */
-
- @Override
public <T> HttpResponse<T>
- send(HttpRequest req, HttpResponse.BodyHandler<T> responseHandler)
+ send(HttpRequest req, BodyHandler<T> responseHandler)
throws IOException, InterruptedException
{
- MultiExchange<Void,T> mex = new MultiExchange<>(req, this, responseHandler);
- return mex.response();
+ try {
+ return sendAsync(req, responseHandler).join();
+ } catch (CompletionException ce) {
+ Throwable t = ce.getCause();
+ if (t instanceof Error)
+ throw (Error)t;
+ if (t instanceof RuntimeException)
+ throw (RuntimeException)t;
+ else if (t instanceof IOException)
+ throw Utils.getIOException(t);
+ else
+ throw new InternalError("Unexpected exception", t);
+ }
}
@Override
public <T> CompletableFuture<HttpResponse<T>>
- sendAsync(HttpRequest req, HttpResponse.BodyHandler<T> responseHandler)
+ sendAsync(HttpRequest req, BodyHandler<T> responseHandler)
{
- MultiExchange<Void,T> mex = new MultiExchange<>(req, this, responseHandler);
- return mex.responseAsync()
- .thenApply((HttpResponseImpl<T> b) -> (HttpResponse<T>) b);
+ AccessControlContext acc = null;
+ if (System.getSecurityManager() != null)
+ acc = AccessController.getContext();
+
+ long start = DEBUGELAPSED ? System.nanoTime() : 0;
+ reference();
+ try {
+ debug.log(Level.DEBUG, "ClientImpl (async) send %s", req);
+
+ MultiExchange<Void,T> mex = new MultiExchange<>(req, this, responseHandler, acc);
+ CompletableFuture<HttpResponse<T>> res =
+ mex.responseAsync().whenComplete((b,t) -> unreference());
+ if (DEBUGELAPSED) {
+ res = res.whenComplete(
+ (b,t) -> debugCompleted("ClientImpl (async)", start, req));
+ }
+ // makes sure that any dependent actions happen in the executor
+ if (acc != null) {
+ res.whenCompleteAsync((r, t) -> { /* do nothing */},
+ new PrivilegedExecutor(executor, acc));
+ }
+
+ return res;
+ } catch(Throwable t) {
+ unreference();
+ debugCompleted("ClientImpl (async)", start, req);
+ throw t;
+ }
}
@Override
public <U, T> CompletableFuture<U>
- sendAsync(HttpRequest req, HttpResponse.MultiProcessor<U, T> responseHandler) {
- MultiExchange<U,T> mex = new MultiExchange<>(req, this, responseHandler);
- return mex.multiResponseAsync();
- }
+ sendAsync(HttpRequest req, MultiSubscriber<U, T> responseHandler) {
+ AccessControlContext acc = null;
+ if (System.getSecurityManager() != null)
+ acc = AccessController.getContext();
- // new impl. Should get rid of above
- /*
- static class BufferPool implements BufferHandler {
-
- final LinkedList<ByteBuffer> freelist = new LinkedList<>();
+ long start = DEBUGELAPSED ? System.nanoTime() : 0;
+ reference();
+ try {
+ debug.log(Level.DEBUG, "ClientImpl (async) send multi %s", req);
- @Override
- public synchronized ByteBuffer getBuffer() {
- ByteBuffer buf;
+ MultiExchange<U,T> mex = new MultiExchange<>(req, this, responseHandler, acc);
+ CompletableFuture<U> res = mex.multiResponseAsync()
+ .whenComplete((b,t) -> unreference());
+ if (DEBUGELAPSED) {
+ res = res.whenComplete(
+ (b,t) -> debugCompleted("ClientImpl (async)", start, req));
+ }
+ // makes sure that any dependent actions happen in the executor
+ if (acc != null) {
+ res.whenCompleteAsync((r, t) -> { /* do nothing */},
+ new PrivilegedExecutor(executor, acc));
+ }
- while (!freelist.isEmpty()) {
- buf = freelist.removeFirst();
- buf.clear();
- return buf;
- }
- return ByteBuffer.allocate(BUFSIZE);
- }
-
- @Override
- public synchronized void returnBuffer(ByteBuffer buffer) {
- assert buffer.capacity() > 0;
- freelist.add(buffer);
+ return res;
+ } catch(Throwable t) {
+ unreference();
+ debugCompleted("ClientImpl (async)", start, req);
+ throw t;
}
}
- static BufferPool pool = new BufferPool();
-
- static BufferHandler pool() {
- return pool;
- }
-*/
// Main loop for this client's selector
private final static class SelectorManager extends Thread {
- private static final long NODEADLINE = 3000L;
+ // For testing purposes we have an internal System property that
+ // can control the frequency at which the selector manager will wake
+ // up when there are no pending operations.
+ // Increasing the frequency (shorter delays) might allow the selector
+ // to observe that the facade is no longer referenced and might allow
+ // the selector thread to terminate more timely - for when nothing is
+ // ongoing it will only check for that condition every NODEADLINE ms.
+ // To avoid misuse of the property, the delay that can be specified
+ // is comprised between [MIN_NODEADLINE, MAX_NODEADLINE], and its default
+ // value if unspecified (or <= 0) is DEF_NODEADLINE = 3000ms
+ // The property is -Djdk.httpclient.internal.selector.timeout=<millis>
+ private static final int MIN_NODEADLINE = 1000; // ms
+ private static final int MAX_NODEADLINE = 1000 * 1200; // ms
+ private static final int DEF_NODEADLINE = 3000; // ms
+ private static final long NODEADLINE; // default is DEF_NODEADLINE ms
+ static {
+ // ensure NODEADLINE is inialized with some valid value.
+ long deadline = Utils.getIntegerNetProperty(
+ "jdk.httpclient.internal.selector.timeout",
+ DEF_NODEADLINE); // millis
+ if (deadline <= 0) deadline = DEF_NODEADLINE;
+ deadline = Math.max(deadline, MIN_NODEADLINE);
+ NODEADLINE = Math.min(deadline, MAX_NODEADLINE);
+ }
+
private final Selector selector;
private volatile boolean closed;
- private final List<AsyncEvent> readyList;
private final List<AsyncEvent> registrations;
-
- // Uses a weak reference to the HttpClient owning this
- // selector: a strong reference prevents its garbage
- // collection while the thread is running.
- // We want the thread to exit gracefully when the
- // HttpClient that owns it gets GC'ed.
- WeakReference<HttpClientImpl> ownerRef;
+ private final System.Logger debug;
+ private final System.Logger debugtimeout;
+ HttpClientImpl owner;
+ ConnectionPool pool;
SelectorManager(HttpClientImpl ref) throws IOException {
- super(null, null, "SelectorManager", 0, false);
- ownerRef = new WeakReference<>(ref);
- readyList = new ArrayList<>();
+ super(null, null, "HttpClient-" + ref.id + "-SelectorManager", 0, false);
+ owner = ref;
+ debug = ref.debug;
+ debugtimeout = ref.debugtimeout;
+ pool = ref.connectionPool();
registrations = new ArrayList<>();
selector = Selector.open();
}
+ void eventUpdated(AsyncEvent e) throws ClosedChannelException {
+ if (Thread.currentThread() == this) {
+ SelectionKey key = e.channel().keyFor(selector);
+ SelectorAttachment sa = (SelectorAttachment) key.attachment();
+ if (sa != null) sa.register(e);
+ } else {
+ register(e);
+ }
+ }
+
// This returns immediately. So caller not allowed to send/receive
// on connection.
-
- synchronized void register(AsyncEvent e) throws IOException {
+ synchronized void register(AsyncEvent e) {
registrations.add(e);
selector.wakeup();
}
@@ -326,23 +548,34 @@
}
synchronized void shutdown() {
+ debug.log(Level.DEBUG, "SelectorManager shutting down");
closed = true;
try {
selector.close();
- } catch (IOException ignored) { }
+ } catch (IOException ignored) {
+ } finally {
+ owner.stop();
+ }
}
@Override
public void run() {
+ List<Pair<AsyncEvent,IOException>> errorList = new ArrayList<>();
+ List<AsyncEvent> readyList = new ArrayList<>();
try {
while (!Thread.currentThread().isInterrupted()) {
- HttpClientImpl client;
synchronized (this) {
- for (AsyncEvent exchange : registrations) {
- SelectableChannel c = exchange.channel();
+ assert errorList.isEmpty();
+ assert readyList.isEmpty();
+ for (AsyncEvent event : registrations) {
+ if (event instanceof AsyncTriggerEvent) {
+ readyList.add(event);
+ continue;
+ }
+ SelectableChannel chan = event.channel();
+ SelectionKey key = null;
try {
- c.configureBlocking(false);
- SelectionKey key = c.keyFor(selector);
+ key = chan.keyFor(selector);
SelectorAttachment sa;
if (key == null || !key.isValid()) {
if (key != null) {
@@ -351,70 +584,141 @@
// before registering the new event.
selector.selectNow();
}
- sa = new SelectorAttachment(c, selector);
+ sa = new SelectorAttachment(chan, selector);
} else {
sa = (SelectorAttachment) key.attachment();
}
- sa.register(exchange);
+ // may throw IOE if channel closed: that's OK
+ sa.register(event);
+ if (!chan.isOpen()) {
+ throw new IOException("Channel closed");
+ }
} catch (IOException e) {
- Log.logError("HttpClientImpl: " + e);
- c.close();
- // let the exchange deal with it
- handleEvent(exchange);
+ Log.logTrace("HttpClientImpl: " + e);
+ debug.log(Level.DEBUG, () ->
+ "Got " + e.getClass().getName()
+ + " while handling"
+ + " registration events");
+ chan.close();
+ // let the event abort deal with it
+ errorList.add(new Pair<>(event, e));
+ if (key != null) {
+ key.cancel();
+ selector.selectNow();
+ }
}
}
registrations.clear();
+ selector.selectedKeys().clear();
}
+ for (AsyncEvent event : readyList) {
+ assert event instanceof AsyncTriggerEvent;
+ event.handle();
+ }
+ readyList.clear();
+
+ for (Pair<AsyncEvent,IOException> error : errorList) {
+ // an IOException was raised and the channel closed.
+ handleEvent(error.first, error.second);
+ }
+ errorList.clear();
+
// Check whether client is still alive, and if not,
// gracefully stop this thread
- if ((client = ownerRef.get()) == null) {
+ if (!owner.isReferenced()) {
Log.logTrace("HttpClient no longer referenced. Exiting...");
return;
}
- long millis = client.purgeTimeoutsAndReturnNextDeadline();
- client = null; // don't hold onto the client ref
+
+ // Timeouts will have milliseconds granularity. It is important
+ // to handle them in a timely fashion.
+ long nextTimeout = owner.purgeTimeoutsAndReturnNextDeadline();
+ debugtimeout.log(Level.DEBUG, "next timeout: %d", nextTimeout);
- //debugPrint(selector);
+ // Keep-alive have seconds granularity. It's not really an
+ // issue if we keep connections linger a bit more in the keep
+ // alive cache.
+ long nextExpiry = pool.purgeExpiredConnectionsAndReturnNextDeadline();
+ debugtimeout.log(Level.DEBUG, "next expired: %d", nextExpiry);
+
+ assert nextTimeout >= 0;
+ assert nextExpiry >= 0;
+
// Don't wait for ever as it might prevent the thread to
// stop gracefully. millis will be 0 if no deadline was found.
+ if (nextTimeout <= 0) nextTimeout = NODEADLINE;
+
+ // Clip nextExpiry at NODEADLINE limit. The default
+ // keep alive is 1200 seconds (half an hour) - we don't
+ // want to wait that long.
+ if (nextExpiry <= 0) nextExpiry = NODEADLINE;
+ else nextExpiry = Math.min(NODEADLINE, nextExpiry);
+
+ // takes the least of the two.
+ long millis = Math.min(nextExpiry, nextTimeout);
+
+ debugtimeout.log(Level.DEBUG, "Next deadline is %d",
+ (millis == 0 ? NODEADLINE : millis));
+ //debugPrint(selector);
int n = selector.select(millis == 0 ? NODEADLINE : millis);
if (n == 0) {
// Check whether client is still alive, and if not,
// gracefully stop this thread
- if ((client = ownerRef.get()) == null) {
+ if (!owner.isReferenced()) {
Log.logTrace("HttpClient no longer referenced. Exiting...");
return;
}
- client.purgeTimeoutsAndReturnNextDeadline();
- client = null; // don't hold onto the client ref
+ owner.purgeTimeoutsAndReturnNextDeadline();
continue;
}
Set<SelectionKey> keys = selector.selectedKeys();
+ assert errorList.isEmpty();
for (SelectionKey key : keys) {
SelectorAttachment sa = (SelectorAttachment) key.attachment();
- int eventsOccurred = key.readyOps();
+ if (!key.isValid()) {
+ IOException ex = sa.chan.isOpen()
+ ? new IOException("Invalid key")
+ : new ClosedChannelException();
+ sa.pending.forEach(e -> errorList.add(new Pair<>(e,ex)));
+ sa.pending.clear();
+ continue;
+ }
+
+ int eventsOccurred;
+ try {
+ eventsOccurred = key.readyOps();
+ } catch (CancelledKeyException ex) {
+ IOException io = Utils.getIOException(ex);
+ sa.pending.forEach(e -> errorList.add(new Pair<>(e,io)));
+ sa.pending.clear();
+ continue;
+ }
sa.events(eventsOccurred).forEach(readyList::add);
sa.resetInterestOps(eventsOccurred);
}
selector.selectNow(); // complete cancellation
selector.selectedKeys().clear();
- for (AsyncEvent exchange : readyList) {
- if (exchange.blocking()) {
- exchange.channel().configureBlocking(true);
- }
- handleEvent(exchange); // will be delegated to executor
+ for (AsyncEvent event : readyList) {
+ handleEvent(event, null); // will be delegated to executor
}
readyList.clear();
+ errorList.forEach((p) -> handleEvent(p.first, p.second));
+ errorList.clear();
}
} catch (Throwable e) {
+ //e.printStackTrace();
if (!closed) {
// This terminates thread. So, better just print stack trace
String err = Utils.stackTrace(e);
Log.logError("HttpClientImpl: fatal error: " + err);
}
+ debug.log(Level.DEBUG, "shutting down", e);
+ if (Utils.ASSERTIONSENABLED && !debug.isLoggable(Level.DEBUG)) {
+ e.printStackTrace(System.err); // always print the stack
+ }
} finally {
shutdown();
}
@@ -431,11 +735,12 @@
System.err.println("Selector: debugprint end");
}
- void handleEvent(AsyncEvent e) {
- if (closed) {
- e.abort();
+ /** Handles the given event. The given ioe may be null. */
+ void handleEvent(AsyncEvent event, IOException ioe) {
+ if (closed || ioe != null) {
+ event.abort(ioe);
} else {
- e.handle();
+ event.handle();
}
}
}
@@ -453,11 +758,13 @@
private static class SelectorAttachment {
private final SelectableChannel chan;
private final Selector selector;
- private final ArrayList<AsyncEvent> pending;
+ private final Set<AsyncEvent> pending;
+ private final static System.Logger debug =
+ Utils.getDebugLogger("SelectorAttachment"::toString, DEBUG);
private int interestOps;
SelectorAttachment(SelectableChannel chan, Selector selector) {
- this.pending = new ArrayList<>();
+ this.pending = new HashSet<>();
this.chan = chan;
this.selector = selector;
}
@@ -506,23 +813,48 @@
this.interestOps = newOps;
SelectionKey key = chan.keyFor(selector);
- if (newOps == 0) {
+ if (newOps == 0 && pending.isEmpty()) {
key.cancel();
} else {
- key.interestOps(newOps);
+ try {
+ key.interestOps(newOps);
+ } catch (CancelledKeyException x) {
+ // channel may have been closed
+ debug.log(Level.DEBUG, "key cancelled for " + chan);
+ abortPending(x);
+ }
+ }
+ }
+
+ void abortPending(Throwable x) {
+ if (!pending.isEmpty()) {
+ AsyncEvent[] evts = pending.toArray(new AsyncEvent[0]);
+ pending.clear();
+ IOException io = Utils.getIOException(x);
+ for (AsyncEvent event : evts) {
+ event.abort(io);
+ }
}
}
}
- @Override
- public SSLContext sslContext() {
- Utils.checkNetPermission("getSSLContext");
+ /*package-private*/ SSLContext theSSLContext() {
return sslContext;
}
@Override
- public Optional<SSLParameters> sslParameters() {
- return Optional.ofNullable(sslParams);
+ public SSLContext sslContext() {
+ SecurityManager sm = System.getSecurityManager();
+ if (sm != null) {
+ NetPermission np = new NetPermission("getSSLContext");
+ sm.checkPermission(np);
+ }
+ return sslContext;
+ }
+
+ @Override
+ public SSLParameters sslParameters() {
+ return Utils.copySSLParameters(sslParams);
}
@Override
@@ -530,9 +862,13 @@
return Optional.ofNullable(authenticator);
}
+ /*package-private*/ final Executor theExecutor() {
+ return executor;
+ }
+
@Override
- public Executor executor() {
- return executor;
+ public final Optional<Executor> executor() {
+ return isDefaultExecutor ? Optional.empty() : Optional.of(executor);
}
ConnectionPool connectionPool() {
@@ -558,7 +894,12 @@
@Override
public WebSocket.Builder newWebSocketBuilder(URI uri,
WebSocket.Listener listener) {
- return new BuilderImpl(this, uri, listener);
+ // Make sure to pass the HttpClientFacade to the web socket builder.
+ // This will ensure that the facade is not released before the
+ // WebSocket has been created, at which point the pendingOperationCount
+ // will have been incremented by the DetachedConnectionChannel
+ // (see PlainHttpConnection.detachChannel())
+ return new BuilderImpl(this.facade(), uri, listener);
}
@Override
@@ -566,6 +907,17 @@
return version;
}
+ String dbgString() {
+ return dbgTag;
+ }
+
+ @Override
+ public String toString() {
+ // Used by tests to get the client's id and compute the
+ // name of the SelectorManager thread.
+ return super.toString() + ("(" + id + ")");
+ }
+
//private final HashMap<String, Boolean> http2NotSupported = new HashMap<>();
boolean getHttp2Allowed() {
--- a/src/jdk.incubator.httpclient/share/classes/jdk/incubator/http/HttpConnection.java Sun Nov 05 17:05:57 2017 +0000
+++ b/src/jdk.incubator.httpclient/share/classes/jdk/incubator/http/HttpConnection.java Sun Nov 05 17:32:13 2017 +0000
@@ -28,12 +28,26 @@
import javax.net.ssl.SSLParameters;
import java.io.Closeable;
import java.io.IOException;
+import java.lang.System.Logger.Level;
import java.net.InetSocketAddress;
import java.nio.ByteBuffer;
import java.nio.channels.SocketChannel;
+import java.util.Arrays;
+import java.util.IdentityHashMap;
+import java.util.List;
+import java.util.Map;
import java.util.concurrent.CompletableFuture;
-
+import java.util.concurrent.ConcurrentLinkedDeque;
+import java.util.concurrent.Flow;
+import jdk.incubator.http.HttpClient.Version;
import jdk.incubator.http.internal.common.ByteBufferReference;
+import jdk.incubator.http.internal.common.Demand;
+import jdk.incubator.http.internal.common.FlowTube;
+import jdk.incubator.http.internal.common.SequentialScheduler;
+import jdk.incubator.http.internal.common.SequentialScheduler.DeferredCompleter;
+import jdk.incubator.http.internal.common.Log;
+import jdk.incubator.http.internal.common.Utils;
+import static jdk.incubator.http.HttpClient.Version.HTTP_2;
/**
* Wraps socket channel layer and takes care of SSL also.
@@ -42,75 +56,149 @@
* PlainHttpConnection: regular direct TCP connection to server
* PlainProxyConnection: plain text proxy connection
* PlainTunnelingConnection: opens plain text (CONNECT) tunnel to server
- * SSLConnection: TLS channel direct to server
- * SSLTunnelConnection: TLS channel via (CONNECT) proxy tunnel
+ * AsyncSSLConnection: TLS channel direct to server
+ * AsyncSSLTunnelConnection: TLS channel via (CONNECT) proxy tunnel
*/
-abstract class HttpConnection implements Closeable {
+abstract class HttpConnection implements Closeable, AsyncConnection {
- enum Mode {
- BLOCKING,
- NON_BLOCKING,
- ASYNC
- }
+ static final boolean DEBUG = Utils.DEBUG; // Revisit: temporary dev flag.
+ final System.Logger debug = Utils.getDebugLogger(this::dbgString, DEBUG);
+ final static System.Logger DEBUG_LOGGER = Utils.getDebugLogger(
+ () -> "HttpConnection(SocketTube(?))", DEBUG);
- protected Mode mode;
-
- // address we are connected to. Could be a server or a proxy
+ /** The address this connection is connected to. Could be a server or a proxy. */
final InetSocketAddress address;
- final HttpClientImpl client;
+ private final HttpClientImpl client;
+ private final TrailingOperations trailingOperations;
HttpConnection(InetSocketAddress address, HttpClientImpl client) {
this.address = address;
this.client = client;
+ trailingOperations = new TrailingOperations();
}
- /**
- * Public API to this class. addr is the ultimate destination. Any proxies
- * etc are figured out from the request. Returns an instance of one of the
- * following
- * PlainHttpConnection
- * PlainTunnelingConnection
- * SSLConnection
- * SSLTunnelConnection
- *
- * When object returned, connect() or connectAsync() must be called, which
- * when it returns/completes, the connection is usable for requests.
- */
- public static HttpConnection getConnection(
- InetSocketAddress addr, HttpClientImpl client, HttpRequestImpl request)
- {
- return getConnectionImpl(addr, client, request, false);
+ private static final class TrailingOperations {
+ private final Map<CompletableFuture<?>, Boolean> operations =
+ new IdentityHashMap<>();
+ void add(CompletableFuture<?> cf) {
+ synchronized(operations) {
+ cf.whenComplete((r,t)-> remove(cf));
+ operations.put(cf, Boolean.TRUE);
+ }
+ }
+ boolean remove(CompletableFuture<?> cf) {
+ synchronized(operations) {
+ return operations.remove(cf);
+ }
+ }
}
- /**
- * Called specifically to get an async connection for HTTP/2 over SSL.
- */
- public static HttpConnection getConnection(InetSocketAddress addr,
- HttpClientImpl client, HttpRequestImpl request, boolean isHttp2) {
-
- return getConnectionImpl(addr, client, request, isHttp2);
+ final void addTrailingOperation(CompletableFuture<?> cf) {
+ trailingOperations.add(cf);
}
- public abstract void connect() throws IOException, InterruptedException;
+ final void removeTrailingOperation(CompletableFuture<?> cf) {
+ trailingOperations.remove(cf);
+ }
+
+ final HttpClientImpl client() {
+ return client;
+ }
+
+ //public abstract void connect() throws IOException, InterruptedException;
public abstract CompletableFuture<Void> connectAsync();
- /**
- * Returns whether this connection is connected to its destination
- */
+ /** Tells whether, or not, this connection is connected to its destination. */
abstract boolean connected();
+ /** Tells whether, or not, this connection is secure ( over SSL ) */
abstract boolean isSecure();
+ /** Tells whether, or not, this connection is proxied. */
abstract boolean isProxied();
+ /** Tells whether, or not, this connection is open. */
+ final boolean isOpen() {
+ return channel().isOpen() &&
+ (connected() ? !getConnectionFlow().isFinished() : true);
+ }
+
+ interface HttpPublisher extends FlowTube.TubePublisher { }
+
+ /**
+ * Returns the HTTP publisher associated with this connection. May be null
+ * if invoked before connecting.
+ */
+ abstract HttpPublisher publisher();
+
/**
- * Completes when the first byte of the response is available to be read.
+ * Factory for retrieving HttpConnections. A connection can be retrieved
+ * from the connection pool, or a new one created if none available.
+ *
+ * The given {@code addr} is the ultimate destination. Any proxies,
+ * etc, are determined from the request. Returns a concrete instance which
+ * is one of the following:
+ * {@link PlainHttpConnection}
+ * {@link PlainTunnelingConnection}
+ * {@link SSLConnection}
+ * {@link SSLTunnelConnection}
+ *
+ * The returned connection, if not from the connection pool, must have its,
+ * connect() or connectAsync() method invoked, which ( when it completes
+ * successfully ) renders the connection usable for requests.
*/
- abstract CompletableFuture<Void> whenReceivingResponse();
+ public static HttpConnection getConnection(InetSocketAddress addr,
+ HttpClientImpl client,
+ HttpRequestImpl request,
+ Version version) {
+ HttpConnection c = null;
+ InetSocketAddress proxy = request.proxy(client);
+ if (proxy != null && proxy.isUnresolved()) {
+ // The default proxy selector may select a proxy whose address is
+ // unresolved. We must resolve the address before connecting to it.
+ proxy = new InetSocketAddress(proxy.getHostString(), proxy.getPort());
+ }
+ boolean secure = request.secure();
+ ConnectionPool pool = client.connectionPool();
- final boolean isOpen() {
- return channel().isOpen();
+ if (!secure) {
+ c = pool.getConnection(false, addr, proxy);
+ if (c != null && c.isOpen() /* may have been eof/closed when in the pool */) {
+ final HttpConnection conn = c;
+ DEBUG_LOGGER.log(Level.DEBUG, () -> conn.getConnectionFlow()
+ + ": plain connection retrieved from HTTP/1.1 pool");
+ return c;
+ } else {
+ return getPlainConnection(addr, proxy, request, client);
+ }
+ } else { // secure
+ if (version != HTTP_2) { // only HTTP/1.1 connections are in the pool
+ c = pool.getConnection(true, addr, proxy);
+ }
+ if (c != null && c.isOpen()) {
+ final HttpConnection conn = c;
+ DEBUG_LOGGER.log(Level.DEBUG, () -> conn.getConnectionFlow()
+ + ": SSL connection retrieved from HTTP/1.1 pool");
+ return c;
+ } else {
+ String[] alpn = null;
+ if (version == HTTP_2) {
+ alpn = new String[] { "h2", "http/1.1" };
+ }
+ return getSSLConnection(addr, proxy, alpn, client);
+ }
+ }
+ }
+
+ private static HttpConnection getSSLConnection(InetSocketAddress addr,
+ InetSocketAddress proxy,
+ String[] alpn,
+ HttpClientImpl client) {
+ if (proxy != null)
+ return new AsyncSSLTunnelConnection(addr, client, alpn, proxy);
+ else
+ return new AsyncSSLConnection(addr, client, alpn);
}
/* Returns either a plain HTTP connection or a plain tunnelling connection
@@ -119,143 +207,48 @@
InetSocketAddress proxy,
HttpRequestImpl request,
HttpClientImpl client) {
- if (request.isWebSocket() && proxy != null) {
+ if (request.isWebSocket() && proxy != null)
return new PlainTunnelingConnection(addr, proxy, client);
- } else {
- if (proxy == null) {
- return new PlainHttpConnection(addr, client);
- } else {
- return new PlainProxyConnection(proxy, client);
- }
- }
- }
- private static HttpConnection getSSLConnection(InetSocketAddress addr,
- InetSocketAddress proxy, HttpRequestImpl request,
- String[] alpn, boolean isHttp2, HttpClientImpl client)
- {
- if (proxy != null) {
- if (!isHttp2) {
- return new SSLTunnelConnection(addr, client, proxy);
- } else {
- return new AsyncSSLTunnelConnection(addr, client, alpn, proxy);
- }
- } else if (!isHttp2) {
- return new SSLConnection(addr, client, alpn);
- } else {
- return new AsyncSSLConnection(addr, client, alpn);
- }
+ if (proxy == null)
+ return new PlainHttpConnection(addr, client);
+ else
+ return new PlainProxyConnection(proxy, client);
}
- /**
- * Main factory method. Gets a HttpConnection, either cached or new if
- * none available.
- */
- private static HttpConnection getConnectionImpl(InetSocketAddress addr,
- HttpClientImpl client,
- HttpRequestImpl request, boolean isHttp2)
- {
- HttpConnection c = null;
- InetSocketAddress proxy = request.proxy(client);
- if (proxy != null && proxy.isUnresolved()) {
- // The default proxy selector may select a proxy whose
- // address is unresolved. We must resolve the address
- // before using it to connect.
- proxy = new InetSocketAddress(proxy.getHostString(), proxy.getPort());
- }
- boolean secure = request.secure();
- ConnectionPool pool = client.connectionPool();
- String[] alpn = null;
-
- if (secure && isHttp2) {
- alpn = new String[2];
- alpn[0] = "h2";
- alpn[1] = "http/1.1";
- }
-
- if (!secure) {
- c = pool.getConnection(false, addr, proxy);
- if (c != null) {
- return c;
- } else {
- return getPlainConnection(addr, proxy, request, client);
- }
- } else {
- if (!isHttp2) { // if http2 we don't cache connections
- c = pool.getConnection(true, addr, proxy);
- }
- if (c != null) {
- return c;
- } else {
- return getSSLConnection(addr, proxy, request, alpn, isHttp2, client);
- }
- }
- }
-
- void returnToCache(HttpHeaders hdrs) {
+ void closeOrReturnToCache(HttpHeaders hdrs) {
if (hdrs == null) {
- // the connection was closed by server
+ // the connection was closed by server, eof
close();
return;
}
if (!isOpen()) {
return;
}
+ HttpClientImpl client = client();
+ if (client == null) {
+ close();
+ return;
+ }
ConnectionPool pool = client.connectionPool();
boolean keepAlive = hdrs.firstValue("Connection")
.map((s) -> !s.equalsIgnoreCase("close"))
.orElse(true);
if (keepAlive) {
+ Log.logTrace("Returning connection to the pool: {0}", this);
pool.returnToPool(this);
} else {
close();
}
}
- /**
- * Also check that the number of bytes written is what was expected. This
- * could be different if the buffer is user-supplied and its internal
- * pointers were manipulated in a race condition.
- */
- final void checkWrite(long expected, ByteBuffer buffer) throws IOException {
- long written = write(buffer);
- if (written != expected) {
- throw new IOException("incorrect number of bytes written");
- }
- }
-
- final void checkWrite(long expected,
- ByteBuffer[] buffers,
- int start,
- int length)
- throws IOException
- {
- long written = write(buffers, start, length);
- if (written != expected) {
- throw new IOException("incorrect number of bytes written");
- }
- }
-
abstract SocketChannel channel();
final InetSocketAddress address() {
return address;
}
- synchronized void configureMode(Mode mode) throws IOException {
- this.mode = mode;
- if (mode == Mode.BLOCKING) {
- channel().configureBlocking(true);
- } else {
- channel().configureBlocking(false);
- }
- }
-
- synchronized Mode getMode() {
- return mode;
- }
-
abstract ConnectionPool.CacheKey cacheKey();
// overridden in SSL only
@@ -263,49 +256,6 @@
return null;
}
- // Methods to be implemented for Plain TCP and SSL
-
- abstract long write(ByteBuffer[] buffers, int start, int number)
- throws IOException;
-
- abstract long write(ByteBuffer buffer) throws IOException;
-
- // Methods to be implemented for Plain TCP (async mode) and AsyncSSL
-
- /**
- * In {@linkplain Mode#ASYNC async mode}, this method puts buffers at the
- * end of the send queue; Otherwise, it is equivalent to {@link
- * #write(ByteBuffer[], int, int) write(buffers, 0, buffers.length)}.
- * When in async mode, calling this method should later be followed by
- * subsequent flushAsync invocation.
- * That allows multiple threads to put buffers into the queue while some other
- * thread is writing.
- */
- abstract void writeAsync(ByteBufferReference[] buffers) throws IOException;
-
- /**
- * In {@linkplain Mode#ASYNC async mode}, this method may put
- * buffers at the beginning of send queue, breaking frames sequence and
- * allowing to write these buffers before other buffers in the queue;
- * Otherwise, it is equivalent to {@link
- * #write(ByteBuffer[], int, int) write(buffers, 0, buffers.length)}.
- * When in async mode, calling this method should later be followed by
- * subsequent flushAsync invocation.
- * That allows multiple threads to put buffers into the queue while some other
- * thread is writing.
- */
- abstract void writeAsyncUnordered(ByteBufferReference[] buffers) throws IOException;
-
- /**
- * This method should be called after any writeAsync/writeAsyncUnordered
- * invocation.
- * If there is a race to flushAsync from several threads one thread
- * (race winner) capture flush operation and write the whole queue content.
- * Other threads (race losers) exits from the method (not blocking)
- * and continue execution.
- */
- abstract void flushAsync() throws IOException;
-
/**
* Closes this connection, by returning the socket to its connection pool.
*/
@@ -316,32 +266,142 @@
abstract void shutdownOutput() throws IOException;
- /**
- * Puts position to limit and limit to capacity so we can resume reading
- * into this buffer, but if required > 0 then limit may be reduced so that
- * no more than required bytes are read next time.
- */
- static void resumeChannelRead(ByteBuffer buf, int required) {
- int limit = buf.limit();
- buf.position(limit);
- int capacity = buf.capacity() - limit;
- if (required > 0 && required < capacity) {
- buf.limit(limit + required);
- } else {
- buf.limit(buf.capacity());
+ // Support for WebSocket/RawChannelImpl which unfortunately
+ // still depends on synchronous read/writes.
+ // It should be removed when RawChannelImpl moves to using asynchronous APIs.
+ abstract static class DetachedConnectionChannel implements Closeable {
+ DetachedConnectionChannel() {}
+ abstract SocketChannel channel();
+ abstract long write(ByteBuffer[] buffers, int start, int number)
+ throws IOException;
+ abstract void shutdownInput() throws IOException;
+ abstract void shutdownOutput() throws IOException;
+ abstract ByteBuffer read() throws IOException;
+ @Override
+ public abstract void close();
+ @Override
+ public String toString() {
+ return this.getClass().getSimpleName() + ": " + channel().toString();
}
}
- final ByteBuffer read() throws IOException {
- ByteBuffer b = readImpl();
- return b;
+ // Support for WebSocket/RawChannelImpl which unfortunately
+ // still depends on synchronous read/writes.
+ // It should be removed when RawChannelImpl moves to using asynchronous APIs.
+ abstract DetachedConnectionChannel detachChannel();
+
+ abstract FlowTube getConnectionFlow();
+
+ // This queue and publisher are temporary, and only needed because
+ // the calling code still uses writeAsync/flushAsync
+ final class PlainHttpPublisher implements HttpPublisher {
+ final Object reading;
+ PlainHttpPublisher() {
+ this(new Object());
+ }
+ PlainHttpPublisher(Object readingLock) {
+ this.reading = readingLock;
+ }
+ final ConcurrentLinkedDeque<List<ByteBuffer>> queue = new ConcurrentLinkedDeque<>();
+ volatile Flow.Subscriber<? super List<ByteBuffer>> subscriber;
+ volatile HttpWriteSubscription subscription;
+ final SequentialScheduler writeScheduler =
+ new SequentialScheduler(this::flushTask);
+ @Override
+ public void subscribe(Flow.Subscriber<? super List<ByteBuffer>> subscriber) {
+ synchronized (reading) {
+ //assert this.subscription == null;
+ //assert this.subscriber == null;
+ if (subscription == null) {
+ subscription = new HttpWriteSubscription();
+ }
+ this.subscriber = subscriber;
+ }
+ subscriber.onSubscribe(subscription);
+ signal();
+ }
+
+ void flushTask(DeferredCompleter completer) {
+ try {
+ HttpWriteSubscription sub = subscription;
+ if (sub != null) sub.flush();
+ } finally {
+ completer.complete();
+ }
+ }
+
+ void signal() {
+ writeScheduler.runOrSchedule();
+ }
+
+ final class HttpWriteSubscription implements Flow.Subscription {
+ volatile boolean cancelled;
+ final Demand demand = new Demand();
+
+ @Override
+ public void request(long n) {
+ if (n <= 0) throw new IllegalArgumentException("non-positive request");
+ demand.increase(n);
+ debug.log(Level.DEBUG, () -> "HttpPublisher: got request of "
+ + n + " from "
+ + getConnectionFlow());
+ writeScheduler.runOrSchedule();
+ }
+
+ @Override
+ public void cancel() {
+ debug.log(Level.DEBUG, () -> "HttpPublisher: cancelled by "
+ + getConnectionFlow());
+ cancelled = true;
+ }
+
+ void flush() {
+ while (!queue.isEmpty() && demand.tryDecrement()) {
+ List<ByteBuffer> elem = queue.poll();
+ debug.log(Level.DEBUG, () -> "HttpPublisher: sending "
+ + Utils.remaining(elem) + " bytes ("
+ + elem.size() + " buffers) to "
+ + getConnectionFlow());
+ subscriber.onNext(elem);
+ }
+ }
+ }
+
+ public void writeAsync(ByteBufferReference[] buffers) throws IOException {
+ List<ByteBuffer> l = Arrays.asList(ByteBufferReference.toBuffers(buffers));
+ queue.add(l);
+ int bytes = l.stream().mapToInt(ByteBuffer::remaining).sum();
+ debug.log(Level.DEBUG, "added %d bytes to the write queue", bytes);
+ }
+
+ public void writeAsyncUnordered(ByteBufferReference[] buffers) throws IOException {
+ // Unordered frames are sent before existing frames.
+ List<ByteBuffer> l = Arrays.asList(ByteBufferReference.toBuffers(buffers));
+ int bytes = l.stream().mapToInt(ByteBuffer::remaining).sum();
+ queue.addFirst(l);
+ debug.log(Level.DEBUG, "inserted %d bytes in the write queue", bytes);
+ }
+
+ public void flushAsync() throws IOException {
+ // ### Remove flushAsync
+ // no-op. Should not be needed now with Tube.
+ // Tube.write will initiate the low-level write
+ debug.log(Level.DEBUG, "signalling the publisher of the write queue");
+ signal();
+ }
}
- /*
- * Returns a ByteBuffer with the data available at the moment, or null if
- * reached EOF.
- */
- protected abstract ByteBuffer readImpl() throws IOException;
+ String dbgTag = null;
+ final String dbgString() {
+ FlowTube flow = getConnectionFlow();
+ String tag = dbgTag;
+ if (tag == null && flow != null) {
+ dbgTag = tag = this.getClass().getSimpleName() + "(" + flow + ")";
+ } else if (tag == null) {
+ tag = this.getClass().getSimpleName() + "(?)";
+ }
+ return tag;
+ }
@Override
public String toString() {
--- a/src/jdk.incubator.httpclient/share/classes/jdk/incubator/http/HttpHeaders.java Sun Nov 05 17:05:57 2017 +0000
+++ b/src/jdk.incubator.httpclient/share/classes/jdk/incubator/http/HttpHeaders.java Sun Nov 05 17:32:13 2017 +0000
@@ -1,5 +1,5 @@
/*
- * Copyright (c) 2015, 2016, Oracle and/or its affiliates. All rights reserved.
+ * Copyright (c) 2015, 2017, Oracle and/or its affiliates. All rights reserved.
* DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER.
*
* This code is free software; you can redistribute it and/or modify it
@@ -29,55 +29,123 @@
import java.util.Map;
import java.util.Optional;
import java.util.OptionalLong;
+import static java.util.Collections.emptyList;
+import static java.util.Collections.unmodifiableList;
+import static java.util.Objects.requireNonNull;
/**
* A read-only view of a set of HTTP headers.
+ *
+ * <p> The methods of this class ( that accept a String header name ), and the
+ * Map returned by the {@linkplain #map() map} method, operate without regard to
+ * case when retrieving the header value.
+ *
+ * <p> HttpHeaders instances are immutable.
+ *
* {@Incubating}
*
* @since 9
*/
-public interface HttpHeaders {
+public abstract class HttpHeaders {
+
+ /**
+ * Creates an HttpHeaders.
+ */
+ protected HttpHeaders() {}
/**
- * Returns an {@link java.util.Optional} containing the first value of the
- * given named (and possibly multi-valued) header. If the header is not
- * present, then the returned {@code Optional} is empty.
+ * Returns an {@link Optional} containing the first value of the given named
+ * (and possibly multi-valued) header. If the header is not present, then
+ * the returned {@code Optional} is empty.
+ *
+ * @implSpec
+ * The default implementation invokes
+ * {@code allValues(name).stream().findFirst()}
*
* @param name the header name
* @return an {@code Optional<String>} for the first named value
*/
- public Optional<String> firstValue(String name);
+ public Optional<String> firstValue(String name) {
+ return allValues(name).stream().findFirst();
+ }
/**
- * Returns an {@link java.util.OptionalLong} containing the first value of the
- * named header field. If the header is not
- * present, then the Optional is empty. If the header is present but
- * contains a value that does not parse as a {@code Long} value, then an
- * exception is thrown.
+ * Returns an {@link OptionalLong} containing the first value of the
+ * named header field. If the header is not present, then the Optional is
+ * empty. If the header is present but contains a value that does not parse
+ * as a {@code Long} value, then an exception is thrown.
+ *
+ * @implSpec
+ * The default implementation invokes
+ * {@code allValues(name).stream().mapToLong(Long::valueOf).findFirst()}
*
* @param name the header name
* @return an {@code OptionalLong}
* @throws NumberFormatException if a value is found, but does not parse as
* a Long
*/
- public OptionalLong firstValueAsLong(String name);
+ public OptionalLong firstValueAsLong(String name) {
+ return allValues(name).stream().mapToLong(Long::valueOf).findFirst();
+ }
/**
* Returns an unmodifiable List of all of the values of the given named
* header. Always returns a List, which may be empty if the header is not
* present.
*
+ * @implSpec
+ * The default implementation invokes, among other things, the
+ * {@code map().get(name)} to retrieve the list of header values.
+ *
* @param name the header name
* @return a List of String values
*/
- public List<String> allValues(String name);
+ public List<String> allValues(String name) {
+ requireNonNull(name);
+ List<String> values = map().get(name);
+ // Making unmodifiable list out of empty in order to make a list which
+ // throws UOE unconditionally
+ return values != null ? values : unmodifiableList(emptyList());
+ }
/**
- * Returns an unmodifiable multi Map view of this HttpHeaders. This
- * interface should only be used when it is required to iterate over the
- * entire set of headers.
+ * Returns an unmodifiable multi Map view of this HttpHeaders.
*
* @return the Map
*/
- public Map<String,List<String>> map();
+ public abstract Map<String, List<String>> map();
+
+ /**
+ * Tests this HTTP headers instance for equality with the given object.
+ *
+ * <p> If the given object is not an {@code HttpHeaders} then this
+ * method returns {@code false}. Two HTTP headers are equal if each
+ * of their corresponding {@linkplain #map() maps} are equal.
+ *
+ * <p> This method satisfies the general contract of the {@link
+ * Object#equals(Object) Object.equals} method.
+ *
+ * @param obj the object to which this object is to be compared
+ * @return {@code true} if, and only if, the given object is an {@code
+ * HttpHeaders} that is equal to this HTTP headers
+ */
+ public final boolean equals(Object obj) {
+ if (!(obj instanceof HttpHeaders))
+ return false;
+ HttpHeaders that = (HttpHeaders)obj;
+ return this.map().equals(that.map());
+ }
+
+ /**
+ * Computes a hash code for this HTTP headers instance.
+ *
+ * <p> The hash code is based upon the components of the HTTP headers
+ * {@linkplain #map() map}, and satisfies the general contract of the
+ * {@link Object#hashCode Object.hashCode} method.
+ *
+ * @return the hash-code value for this HTTP headers
+ */
+ public final int hashCode() {
+ return map().hashCode();
+ }
}
--- a/src/jdk.incubator.httpclient/share/classes/jdk/incubator/http/HttpRequest.java Sun Nov 05 17:05:57 2017 +0000
+++ b/src/jdk.incubator.httpclient/share/classes/jdk/incubator/http/HttpRequest.java Sun Nov 05 17:32:13 2017 +0000
@@ -28,34 +28,41 @@
import java.io.FileNotFoundException;
import java.io.InputStream;
import java.net.URI;
+import java.net.URLPermission;
import java.nio.ByteBuffer;
-import java.nio.charset.*;
+import java.nio.charset.Charset;
+import java.nio.charset.StandardCharsets;
+import java.nio.file.Files;
import java.nio.file.Path;
+import java.security.AccessController;
+import java.security.PrivilegedAction;
import java.time.Duration;
import java.util.Iterator;
+import java.util.Objects;
import java.util.Optional;
import java.util.concurrent.CompletableFuture;
+import java.util.concurrent.Executor;
import java.util.concurrent.Flow;
import java.util.function.Supplier;
+import static java.nio.charset.StandardCharsets.UTF_8;
/**
* Represents one HTTP request which can be sent to a server.
* {@Incubating }
*
- * <p> {@code HttpRequest}s are built from {@code HttpRequest}
- * {@link HttpRequest.Builder builder}s. {@code HttpRequest} builders are
- * obtained by calling {@link HttpRequest#newBuilder(java.net.URI)
- * HttpRequest.newBuilder}.
- * A request's {@link java.net.URI}, headers and body can be set. Request bodies
- * are provided through a {@link BodyProcessor} object supplied to the
- * {@link Builder#DELETE(jdk.incubator.http.HttpRequest.BodyProcessor) DELETE},
- * {@link Builder#POST(jdk.incubator.http.HttpRequest.BodyProcessor) POST} or
- * {@link Builder#PUT(jdk.incubator.http.HttpRequest.BodyProcessor) PUT} methods.
+ * <p> {@code HttpRequest}s are built from {@code HttpRequest} {@link
+ * HttpRequest.Builder builder}s. {@code HttpRequest} builders are obtained by
+ * calling {@link HttpRequest#newBuilder(URI) HttpRequest.newBuilder}. A
+ * request's {@linkplain URI}, headers and body can be set. Request bodies are
+ * provided through a {@link BodyPublisher} object supplied to the
+ * {@link Builder#DELETE(BodyPublisher) DELETE},
+ * {@link Builder#POST(BodyPublisher) POST} or
+ * {@link Builder#PUT(BodyPublisher) PUT} methods.
* {@link Builder#GET() GET} does not take a body. Once all required
* parameters have been set in the builder, {@link Builder#build() } is called
- * to return the {@code HttpRequest}. Builders can also be copied
- * and modified multiple times in order to build multiple related requests that
- * differ in some parameters.
+ * to return the {@code HttpRequest}. Builders can also be copied and modified
+ * multiple times in order to build multiple related requests that differ in
+ * some parameters.
*
* <p> Two simple, example HTTP interactions are shown below:
* <pre>
@@ -79,7 +86,7 @@
* HttpRequest
* .newBuilder(new URI("http://www.foo.com/"))
* .headers("Foo", "foovalue", "Bar", "barvalue")
- * .POST(BodyProcessor.fromString("Hello world"))
+ * .POST(BodyPublisher.fromString("Hello world"))
* .build(),
* BodyHandler.asFile(Paths.get("/path"))
* );
@@ -87,6 +94,7 @@
* Path body = response.body(); // should be "/path"
* }
* </pre>
+ *
* <p> The request is sent and the response obtained by calling one of the
* following methods in {@link HttpClient}.
* <ul><li>{@link HttpClient#send(HttpRequest, HttpResponse.BodyHandler)} blocks
@@ -95,7 +103,7 @@
* request and receives the response asynchronously. Returns immediately with a
* {@link java.util.concurrent.CompletableFuture CompletableFuture}<{@link
* HttpResponse}>.</li>
- * <li>{@link HttpClient#sendAsync(HttpRequest,HttpResponse.MultiProcessor) }
+ * <li>{@link HttpClient#sendAsync(HttpRequest, HttpResponse.MultiSubscriber) }
* sends the request asynchronously, expecting multiple responses. This
* capability is of most relevance to HTTP/2 server push, but can be used for
* single responses (HTTP/1.1 or HTTP/2) also.</li>
@@ -109,34 +117,36 @@
*
* <p> <b>Request bodies</b>
*
- * <p> Request bodies are sent using one of the request processor implementations
- * below provided in {@link HttpRequest.BodyProcessor}, or else a custom implementation can be
- * used.
+ * <p> Request bodies can be sent using one of the convenience request publisher
+ * implementations below, provided in {@link BodyPublisher}. Alternatively, a
+ * custom Publisher implementation can be used.
* <ul>
- * <li>{@link BodyProcessor#fromByteArray(byte[]) fromByteArray(byte[])} from byte array</li>
- * <li>{@link BodyProcessor#fromByteArrays(Iterable) fromByteArrays(Iterable)}
+ * <li>{@link BodyPublisher#fromByteArray(byte[]) fromByteArray(byte[])} from byte array</li>
+ * <li>{@link BodyPublisher#fromByteArrays(Iterable) fromByteArrays(Iterable)}
* from an Iterable of byte arrays</li>
- * <li>{@link BodyProcessor#fromFile(java.nio.file.Path) fromFile(Path)} from the file located
+ * <li>{@link BodyPublisher#fromFile(java.nio.file.Path) fromFile(Path)} from the file located
* at the given Path</li>
- * <li>{@link BodyProcessor#fromString(java.lang.String) fromString(String)} from a String </li>
- * <li>{@link BodyProcessor#fromInputStream(Supplier) fromInputStream}({@link Supplier}<
+ * <li>{@link BodyPublisher#fromString(java.lang.String) fromString(String)} from a String </li>
+ * <li>{@link BodyPublisher#fromInputStream(Supplier) fromInputStream}({@link Supplier}<
* {@link InputStream}>) from an InputStream obtained from a Supplier</li>
- * <li>{@link BodyProcessor#noBody() } no request body is sent</li>
+ * <li>{@link BodyPublisher#noBody() } no request body is sent</li>
* </ul>
*
* <p> <b>Response bodies</b>
*
- * <p>Responses bodies are handled at two levels. When sending the request,
- * a response body handler is specified. This is a function ({@link HttpResponse.BodyHandler})
- * which will be called with the response status code and headers, once these are received. This
- * function is then expected to return a {@link HttpResponse.BodyProcessor}
- * {@code <T>} which is then used to read the response body converting it
- * into an instance of T. After this occurs, the response becomes
- * available in a {@link HttpResponse} and {@link HttpResponse#body()} can then
- * be called to obtain the body. Some implementations and examples of usage of both {@link
- * HttpResponse.BodyProcessor} and {@link HttpResponse.BodyHandler}
- * are provided in {@link HttpResponse}:
- * <p><b>Some of the pre-defined body handlers</b><br>
+ * <p> Responses bodies are handled at two levels. When sending the request,
+ * a response body handler is specified. This is a function ({@linkplain
+ * HttpResponse.BodyHandler}) which will be called with the response status code
+ * and headers, once they are received. This function is then expected to return
+ * a {@link HttpResponse.BodySubscriber}{@code <T>} which is then used to read
+ * the response body, converting it into an instance of T. After this occurs,
+ * the response becomes available in a {@link HttpResponse}, and {@link
+ * HttpResponse#body()} can then be called to obtain the actual body. Some
+ * implementations and examples of usage of both {@link
+ * HttpResponse.BodySubscriber} and {@link HttpResponse.BodyHandler} are
+ * provided in {@link HttpResponse}:
+ *
+ * <p> <b>Some of the pre-defined body handlers</b><br>
* <ul>
* <li>{@link HttpResponse.BodyHandler#asByteArray() BodyHandler.asByteArray()}
* stores the body in a byte array</li>
@@ -152,8 +162,8 @@
*
* <p> With HTTP/2 it is possible for a server to return a main response and zero
* or more additional responses (known as server pushes) to a client-initiated
- * request. These are handled using a special response processor called {@link
- * HttpResponse.MultiProcessor}.
+ * request. These are handled using a special response subscriber called {@link
+ * HttpResponse.MultiSubscriber}.
*
* <p> <b>Blocking/asynchronous behavior and thread usage</b>
*
@@ -161,29 +171,38 @@
* <i>asynchronous</i>. {@link HttpClient#send(HttpRequest, HttpResponse.BodyHandler) }
* blocks the calling thread until the request has been sent and the response received.
*
- * <p> {@link HttpClient#sendAsync(HttpRequest, HttpResponse.BodyHandler)} is asynchronous and returns
- * immediately with a {@link java.util.concurrent.CompletableFuture}<{@link
- * HttpResponse}> and when this object completes (in a background thread) the
- * response has been received.
+ * <p> {@link HttpClient#sendAsync(HttpRequest, HttpResponse.BodyHandler)} is
+ * asynchronous and returns immediately with a {@link CompletableFuture}<{@link
+ * HttpResponse}> and when this object completes (possibly in a different
+ * thread) the response has been received.
*
- * <p> {@link HttpClient#sendAsync(HttpRequest,HttpResponse.MultiProcessor)}
+ * <p> {@link HttpClient#sendAsync(HttpRequest, HttpResponse.MultiSubscriber)}
* is the variant for multi responses and is also asynchronous.
*
- * <p> {@code CompletableFuture}s can be combined in different ways to declare the
- * dependencies among several asynchronous tasks, while allowing for the maximum
- * level of parallelism to be utilized.
+ * <p> {@code CompletableFuture}s can be combined in different ways to declare
+ * the dependencies among several asynchronous tasks, while allowing for the
+ * maximum level of parallelism to be utilized.
*
- * <p> <b>Security checks</b>
+ * <p> <a id="securitychecks"></a><b>Security checks</b></a>
*
* <p> If a security manager is present then security checks are performed by
- * the sending methods. A {@link java.net.URLPermission} or {@link java.net.SocketPermission} is required to
- * access any destination origin server and proxy server utilised. {@code URLPermission}s
- * should be preferred in policy files over {@code SocketPermission}s given the more
- * limited scope of {@code URLPermission}. Permission is always implicitly granted to a
- * system's default proxies. The {@code URLPermission} form used to access proxies uses
- * a method parameter of {@code "CONNECT"} (for all kinds of proxying) and a url string
- * of the form {@code "socket://host:port"} where host and port specify the proxy's
- * address.
+ * the HTTP Client's sending methods. An appropriate {@link URLPermission} is
+ * required to access the destination origin server, and proxy server if one has
+ * been configured. The {@code URLPermission} form used to access proxies uses a
+ * method parameter of {@code "CONNECT"} (for all kinds of proxying) and a URL
+ * string of the form {@code "socket://host:port"} where host and port specify
+ * the proxy's address.
+ *
+ * <p> In this implementation, if an explicit {@linkplain
+ * HttpClient.Builder#executor(Executor) executor} has not been set for an
+ * {@code HttpClient}, and a security manager has been installed, then the
+ * default executor will execute asynchronous and dependent tasks in a context
+ * that is granted no permissions. Custom {@linkplain HttpRequest.BodyPublisher
+ * request body publishers}, {@linkplain HttpResponse.BodyHandler response body
+ * handlers}, and {@linkplain HttpResponse.BodySubscriber response body
+ * subscribers}, if executing operations that require privileges, should do so
+ * within an appropriate {@linkplain AccessController#doPrivileged(PrivilegedAction)
+ * privileged context}.
*
* <p> <b>Examples</b>
* <pre>{@code
@@ -193,7 +212,7 @@
*
* HttpRequest request = HttpRequest
* .newBuilder(new URI("http://www.foo.com/"))
- * .POST(BodyProcessor.fromString("Hello world"))
+ * .POST(BodyPublisher.fromString("Hello world"))
* .build();
*
* HttpResponse<Path> response =
@@ -239,8 +258,8 @@
* // Use File.exists() to check whether file was successfully downloaded
* }
* </pre>
- * <p>
- * Unless otherwise stated, {@code null} parameter values will cause methods
+ *
+ * <p> Unless otherwise stated, {@code null} parameter values will cause methods
* of this class to throw {@code NullPointerException}.
*
* @since 9
@@ -263,7 +282,8 @@
* builder and returns <i>this</i> (ie. the same instance). The methods are
* not synchronized and should not be called from multiple threads without
* external synchronization.
- * <p>Note, that not all request headers may be set by user code. Some are
+ *
+ * <p> Note, that not all request headers may be set by user code. Some are
* restricted for security reasons and others such as the headers relating
* to authentication, redirection and cookie management are managed by
* specific APIs rather than through directly user set headers.
@@ -286,16 +306,17 @@
* @param uri the request URI
* @return this request builder
* @throws IllegalArgumentException if the {@code URI} scheme is not
- * supported.
+ * supported
*/
public abstract Builder uri(URI uri);
/**
- * Request server to acknowledge request before sending request
- * body. This is disabled by default. If enabled, the server is requested
- * to send an error response or a {@code 100 Continue} response before the client
- * sends the request body. This means the request processor for the
- * request will not be invoked until this interim response is received.
+ * Requests the server to acknowledge the request before sending the
+ * body. This is disabled by default. If enabled, the server is
+ * requested to send an error response or a {@code 100 Continue}
+ * response before the client sends the request body. This means the
+ * request publisher for the request will not be invoked until this
+ * interim response is received.
*
* @param enable {@code true} if Expect continue to be sent
* @return this request builder
@@ -303,11 +324,12 @@
public abstract Builder expectContinue(boolean enable);
/**
- * Sets the preferred {@link HttpClient.Version} for this
- * request. The corresponding {@link HttpResponse} should be checked
- * for the version that was used. If the version is not set
- * in a request, then the version requested will be that of the
- * sending {@link HttpClient}.
+ * Sets the preferred {@link HttpClient.Version} for this request.
+ *
+ * <p> The corresponding {@link HttpResponse} should be checked for the
+ * version that was actually used. If the version is not set in a
+ * request, then the version requested will be that of the sending
+ * {@link HttpClient}.
*
* @param version the HTTP protocol version requested
* @return this request builder
@@ -320,29 +342,24 @@
* @param name the header name
* @param value the header value
* @return this request builder
+ * @throws IllegalArgumentException if the header name or value is not
+ * valid, see <a href="https://tools.ietf.org/html/rfc7230#section-3.2">
+ * RFC 7230 section-3.2</a>
*/
public abstract Builder header(String name, String value);
-// /**
-// * Overrides the {@code ProxySelector} set on the request's client for this
-// * request.
-// *
-// * @param proxy the ProxySelector to use
-// * @return this request builder
-// */
-// public abstract Builder proxy(ProxySelector proxy);
-
/**
* Adds the given name value pairs to the set of headers for this
- * request. The supplied {@code String}s must alternate as names and values.
+ * request. The supplied {@code String}s must alternate as header names
+ * and values.
*
- * @param headers the list of String name value pairs
+ * @param headers the list of name value pairs
* @return this request builder
- * @throws IllegalArgumentException if there is an odd number of
- * parameters
+ * @throws IllegalArgumentException if there are an odd number of
+ * parameters, or if a header name or value is not valid, see
+ * <a href="https://tools.ietf.org/html/rfc7230#section-3.2">
+ * RFC 7230 section-3.2</a>
*/
- // TODO (spec): consider signature change
- // public abstract Builder headers(java.util.Map.Entry<String,String>... headers);
public abstract Builder headers(String... headers);
/**
@@ -358,6 +375,7 @@
*
* @param duration the timeout duration
* @return this request builder
+ * @throws IllegalArgumentException if the duration is non-positive
*/
public abstract Builder timeout(Duration duration);
@@ -368,6 +386,9 @@
* @param name the header name
* @param value the header value
* @return this request builder
+ * @throws IllegalArgumentException if the header name or value is not valid,
+ * see <a href="https://tools.ietf.org/html/rfc7230#section-3.2">
+ * RFC 7230 section-3.2</a>
*/
public abstract Builder setHeader(String name, String value);
@@ -380,57 +401,61 @@
/**
* Sets the request method of this builder to POST and sets its
- * request body processor to the given value.
+ * request body publisher to the given value.
*
- * @param body the body processor
+ * @param bodyPublisher the body publisher
*
* @return a {@code HttpRequest}
*/
- public abstract Builder POST(BodyProcessor body);
+ public abstract Builder POST(BodyPublisher bodyPublisher);
/**
* Sets the request method of this builder to PUT and sets its
- * request body processor to the given value.
+ * request body publisher to the given value.
*
- * @param body the body processor
+ * @param bodyPublisher the body publisher
*
* @return a {@code HttpRequest}
*/
- public abstract Builder PUT(BodyProcessor body);
+ public abstract Builder PUT(BodyPublisher bodyPublisher);
/**
* Sets the request method of this builder to DELETE and sets its
- * request body processor to the given value.
+ * request body publisher to the given value.
*
- * @param body the body processor
+ * @param bodyPublisher the body publisher
*
* @return a {@code HttpRequest}
*/
- public abstract Builder DELETE(BodyProcessor body);
+ public abstract Builder DELETE(BodyPublisher bodyPublisher);
/**
* Sets the request method and request body of this builder to the
* given values.
*
- * @param body the body processor
+ * @apiNote The {@linkplain #noBody() noBody} request body publisher can
+ * be used where no request body is required or appropriate.
+ *
+ * @param bodyPublisher the body publisher
* @param method the method to use
* @return a {@code HttpRequest}
- * @throws IllegalArgumentException if an unrecognized method is used
+ * @throws IllegalArgumentException if the method is unrecognised
*/
- public abstract Builder method(String method, BodyProcessor body);
+ public abstract Builder method(String method, BodyPublisher bodyPublisher);
/**
* Builds and returns a {@link HttpRequest}.
*
* @return the request
+ * @throws IllegalStateException if a URI has not been set
*/
public abstract HttpRequest build();
/**
- * Returns an exact duplicate copy of this {@code Builder} based on current
- * state. The new builder can then be modified independently of this
- * builder.
+ * Returns an exact duplicate copy of this {@code Builder} based on
+ * current state. The new builder can then be modified independently of
+ * this builder.
*
* @return an exact copy of this Builder
*/
@@ -458,14 +483,13 @@
}
/**
- * Returns an {@code Optional} containing the {@link BodyProcessor}
- * set on this request. If no {@code BodyProcessor} was set in the
- * requests's builder, then the {@code Optional} is empty.
+ * Returns an {@code Optional} containing the {@link BodyPublisher} set on
+ * this request. If no {@code BodyPublisher} was set in the requests's
+ * builder, then the {@code Optional} is empty.
*
- * @return an {@code Optional} containing this request's
- * {@code BodyProcessor}
+ * @return an {@code Optional} containing this request's {@code BodyPublisher}
*/
- public abstract Optional<BodyProcessor> bodyProcessor();
+ public abstract Optional<BodyPublisher> bodyPublisher();
/**
* Returns the request method for this request. If not set explicitly,
@@ -476,11 +500,13 @@
public abstract String method();
/**
- * Returns the duration for this request.
+ * Returns an {@code Optional} containing this request's timeout duration.
+ * If the timeout duration was not set in the request's builder, then the
+ * {@code Optional} is empty.
*
- * @return this requests duration
+ * @return an {@code Optional} containing this request's timeout duration
*/
- public abstract Duration duration();
+ public abstract Optional<Duration> timeout();
/**
* Returns this request's {@link HttpRequest.Builder#expectContinue(boolean)
@@ -519,138 +545,187 @@
/**
- * A request body handler which sends no request body.
+ * Two {@code HttpRequest} objects are equal if their URI, method and headers
+ * fields are all equal.
*
- * @return a BodyProcessor
+ * @param other
+ * @return true if this is equal to other
*/
- public static BodyProcessor noBody() {
- return new RequestProcessors.EmptyProcessor();
+ @Override
+ public final boolean equals(Object other) {
+ if (! (other instanceof HttpRequest))
+ return false;
+ HttpRequest that = (HttpRequest)other;
+ if (!that.method().equals(this.method()))
+ return false;
+ if (!that.uri().equals(this.uri()))
+ return false;
+ if (!that.headers().equals(this.headers()))
+ return false;
+ return true;
+ }
+
+ /**
+ * Returns this object's hash code. The hash code is calculated as the sum of the
+ * hash codes of its method, uri and headers fields.
+ *
+ * @return
+ */
+ public final int hashCode() {
+ return method().hashCode()
+ + uri().hashCode()
+ + headers().hashCode();
}
/**
- * A processor which converts high level Java objects into flows of
- * {@link java.nio.ByteBuffer}s suitable for sending as request bodies.
+ * A request body handler which sends no request body.
+ *
+ * @return a BodyPublisher
+ */
+ public static BodyPublisher noBody() {
+ return new RequestPublishers.EmptyPublisher();
+ }
+
+ /**
+ * A Publisher which converts high level Java objects into flows of
+ * {@linkplain ByteBuffer}s suitable for sending as request bodies.
* {@Incubating}
- * <p>
- * {@code BodyProcessor}s implement {@link Flow.Publisher} which means they
- * act as a publisher of byte buffers.
- * <p>
- * The HTTP client implementation subscribes to the processor in
- * order to receive the flow of outgoing data buffers. The normal semantics
- * of {@link Flow.Subscriber} and {@link Flow.Publisher} are implemented
- * by the library and expected from processor implementations.
- * Each outgoing request results in one {@code Subscriber} subscribing to the
- * {@code Publisher} in order to provide the sequence of {@code ByteBuffer}s containing
- * the request body. {@code ByteBuffer}s must be allocated by the processor,
- * and must not be accessed after being handed over to the library.
- * These subscriptions complete normally when the request is fully
- * sent, and can be canceled or terminated early through error. If a request
+ *
+ * <p> {@code BodyPublisher}s implement {@link Flow.Publisher} which means
+ * they act as a publisher of byte buffers.
+ *
+ * <p> The HTTP client implementation subscribes to the publisher in order
+ * to receive the flow of outgoing data buffers. The normal semantics of
+ * {@link Flow.Subscriber} and {@link Flow.Publisher} are implemented by the
+ * library and are expected from publisher implementations. Each outgoing
+ * request results in one {@code Subscriber} subscribing to the {@code
+ * BodyPublisher} in order to provide the sequence of {@code ByteBuffer}s
+ * containing the request body. {@code ByteBuffer}s must be allocated by the
+ * publisher, and must not be accessed after being handed over to the library.
+ * These subscriptions complete normally when the request is fully sent,
+ * and can be canceled or terminated early through error. If a request
* needs to be resent for any reason, then a new subscription is created
* which is expected to generate the same data as before.
+ *
+ * <p> A publisher that reports a {@linkplain #contentLength() content
+ * length} of {@code 0} may not be subscribed to by the HTTP client
+ * implementation, as it has effectively no data to publish.
*/
- public interface BodyProcessor extends Flow.Publisher<ByteBuffer> {
+ public interface BodyPublisher extends Flow.Publisher<ByteBuffer> {
/**
- * Returns a request body processor whose body is the given {@code String},
- * converted using the {@link java.nio.charset.StandardCharsets#UTF_8 UTF_8}
+ * Returns a request body publisher whose body is the given {@code
+ * String}, converted using the {@link StandardCharsets#UTF_8 UTF_8}
* character set.
*
* @param body the String containing the body
- * @return a BodyProcessor
+ * @return a BodyPublisher
*/
- static BodyProcessor fromString(String body) {
- return fromString(body, StandardCharsets.UTF_8);
+ static BodyPublisher fromString(String body) {
+ return fromString(body, UTF_8);
}
/**
- * Returns a request body processor whose body is the given {@code String}, converted
- * using the given character set.
+ * Returns a request body publisher whose body is the given {@code
+ * String}, converted using the given character set.
*
* @param s the String containing the body
* @param charset the character set to convert the string to bytes
- * @return a BodyProcessor
+ * @return a BodyPublisher
*/
- static BodyProcessor fromString(String s, Charset charset) {
- return new RequestProcessors.StringProcessor(s, charset);
+ static BodyPublisher fromString(String s, Charset charset) {
+ return new RequestPublishers.StringPublisher(s, charset);
}
/**
- * A request body processor that reads its data from an {@link java.io.InputStream}.
- * A {@link Supplier} of {@code InputStream} is used in case the request needs
- * to be sent again as the content is not buffered. The {@code Supplier} may return
- * {@code null} on subsequent attempts in which case, the request fails.
+ * A request body publisher that reads its data from an {@link
+ * InputStream}. A {@link Supplier} of {@code InputStream} is used in
+ * case the request needs to be repeated, as the content is not buffered.
+ * The {@code Supplier} may return {@code null} on subsequent attempts,
+ * in which case the request fails.
*
* @param streamSupplier a Supplier of open InputStreams
- * @return a BodyProcessor
+ * @return a BodyPublisher
*/
// TODO (spec): specify that the stream will be closed
- static BodyProcessor fromInputStream(Supplier<? extends InputStream> streamSupplier) {
- return new RequestProcessors.InputStreamProcessor(streamSupplier);
+ static BodyPublisher fromInputStream(Supplier<? extends InputStream> streamSupplier) {
+ return new RequestPublishers.InputStreamPublisher(streamSupplier);
}
/**
- * Returns a request body processor whose body is the given byte array.
+ * Returns a request body publisher whose body is the given byte array.
*
* @param buf the byte array containing the body
- * @return a BodyProcessor
+ * @return a BodyPublisher
*/
- static BodyProcessor fromByteArray(byte[] buf) {
- return new RequestProcessors.ByteArrayProcessor(buf);
+ static BodyPublisher fromByteArray(byte[] buf) {
+ return new RequestPublishers.ByteArrayPublisher(buf);
}
/**
- * Returns a request body processor whose body is the content of the given byte
- * array of {@code length} bytes starting from the specified
+ * Returns a request body publisher whose body is the content of the
+ * given byte array of {@code length} bytes starting from the specified
* {@code offset}.
*
* @param buf the byte array containing the body
* @param offset the offset of the first byte
* @param length the number of bytes to use
- * @return a BodyProcessor
+ * @return a BodyPublisher
+ * @throws IndexOutOfBoundsException if the sub-range is defined to be
+ * out-of-bounds
*/
- static BodyProcessor fromByteArray(byte[] buf, int offset, int length) {
- return new RequestProcessors.ByteArrayProcessor(buf, offset, length);
+ static BodyPublisher fromByteArray(byte[] buf, int offset, int length) {
+ Objects.checkFromIndexSize(offset, length, buf.length);
+ return new RequestPublishers.ByteArrayPublisher(buf, offset, length);
}
- /**
- * A request body processor that takes data from the contents of a File.
- *
- * @param path the path to the file containing the body
- * @return a BodyProcessor
- * @throws java.io.FileNotFoundException if path not found
- */
- static BodyProcessor fromFile(Path path) throws FileNotFoundException {
- return new RequestProcessors.FileProcessor(path);
+ private static String pathForSecurityCheck(Path path) {
+ return path.toFile().getPath();
}
/**
- * A request body processor that takes data from an {@code Iterable} of byte arrays.
- * An {@link Iterable} is provided which supplies {@link Iterator} instances.
- * Each attempt to send the request results in one invocation of the
- * {@code Iterable}
+ * A request body publisher that takes data from the contents of a File.
+ *
+ * @param path the path to the file containing the body
+ * @return a BodyPublisher
+ * @throws java.io.FileNotFoundException if the path is not found
+ * @throws SecurityException if a security manager has been installed
+ * and it denies {@link SecurityManager#checkRead(String)
+ * read access} to the given file
+ */
+ static BodyPublisher fromFile(Path path) throws FileNotFoundException {
+ Objects.requireNonNull(path);
+ SecurityManager sm = System.getSecurityManager();
+ if (sm != null)
+ sm.checkRead(pathForSecurityCheck(path));
+ if (Files.notExists(path))
+ throw new FileNotFoundException(path + " not found");
+ return new RequestPublishers.FilePublisher(path);
+ }
+
+ /**
+ * A request body publisher that takes data from an {@code Iterable}
+ * of byte arrays. An {@link Iterable} is provided which supplies
+ * {@link Iterator} instances. Each attempt to send the request results
+ * in one invocation of the {@code Iterable}.
*
* @param iter an Iterable of byte arrays
- * @return a BodyProcessor
+ * @return a BodyPublisher
*/
- static BodyProcessor fromByteArrays(Iterable<byte[]> iter) {
- return new RequestProcessors.IterableProcessor(iter);
+ static BodyPublisher fromByteArrays(Iterable<byte[]> iter) {
+ return new RequestPublishers.IterablePublisher(iter);
}
/**
* Returns the content length for this request body. May be zero
- * if no request content being sent, greater than zero for a fixed
- * length content, and less than zero for an unknown content length.
+ * if no request body being sent, greater than zero for a fixed
+ * length content, or less than zero for an unknown content length.
*
- * @return the content length for this request body if known
+ * This method may be invoked before the publisher is subscribed to.
+ * This method may be invoked more than once by the HTTP client
+ * implementation, and MUST return the same constant value each time.
+ *
+ * @return the content length for this request body, if known
*/
long contentLength();
-
-// /**
-// * Returns a used {@code ByteBuffer} to this request processor. When the
-// * HTTP implementation has finished sending the contents of a buffer,
-// * this method is called to return it to the processor for re-use.
-// *
-// * @param buffer a used ByteBuffer
-// */
- //void returnBuffer(ByteBuffer buffer);
}
}
--- a/src/jdk.incubator.httpclient/share/classes/jdk/incubator/http/HttpRequestBuilderImpl.java Sun Nov 05 17:05:57 2017 +0000
+++ b/src/jdk.incubator.httpclient/share/classes/jdk/incubator/http/HttpRequestBuilderImpl.java Sun Nov 05 17:32:13 2017 +0000
@@ -1,5 +1,5 @@
/*
- * Copyright (c) 2015, 2016, Oracle and/or its affiliates. All rights reserved.
+ * Copyright (c) 2015, 2017, Oracle and/or its affiliates. All rights reserved.
* DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER.
*
* This code is free software; you can redistribute it and/or modify it
@@ -26,11 +26,12 @@
package jdk.incubator.http;
import java.net.URI;
-import jdk.incubator.http.HttpRequest.BodyProcessor;
import java.time.Duration;
import java.util.Optional;
+import jdk.incubator.http.HttpRequest.BodyPublisher;
+import jdk.incubator.http.internal.common.HttpHeadersImpl;
+import static java.lang.String.format;
import static java.util.Objects.requireNonNull;
-import jdk.incubator.http.internal.common.HttpHeadersImpl;
import static jdk.incubator.http.internal.common.Utils.isValidName;
import static jdk.incubator.http.internal.common.Utils.isValidValue;
@@ -39,16 +40,13 @@
private HttpHeadersImpl userHeaders;
private URI uri;
private String method;
- //private HttpClient.Redirect followRedirects;
private boolean expectContinue;
- private HttpRequest.BodyProcessor body;
+ private BodyPublisher bodyPublisher;
private volatile Optional<HttpClient.Version> version;
- //private final HttpClientImpl client;
- //private ProxySelector proxy;
private Duration duration;
public HttpRequestBuilderImpl(URI uri) {
- //this.client = client;
+ requireNonNull(uri, "uri must be non-null");
checkURI(uri);
this.uri = uri;
this.userHeaders = new HttpHeadersImpl();
@@ -58,31 +56,66 @@
public HttpRequestBuilderImpl() {
this.userHeaders = new HttpHeadersImpl();
+ this.method = "GET"; // default, as per spec
this.version = Optional.empty();
}
@Override
public HttpRequestBuilderImpl uri(URI uri) {
- requireNonNull(uri);
+ requireNonNull(uri, "uri must be non-null");
checkURI(uri);
this.uri = uri;
return this;
}
+ private static IllegalArgumentException newIAE(String message, Object... args) {
+ return new IllegalArgumentException(format(message, args));
+ }
+
private static void checkURI(URI uri) {
- String scheme = uri.getScheme().toLowerCase();
- if (!scheme.equals("https") && !scheme.equals("http")) {
- throw new IllegalArgumentException("invalid URI scheme");
+ String scheme = uri.getScheme();
+ if (scheme == null)
+ throw newIAE("URI with undefined scheme");
+ scheme = scheme.toLowerCase();
+ if (!(scheme.equals("https") || scheme.equals("http"))) {
+ throw newIAE("invalid URI scheme %s", scheme);
+ }
+ if (uri.getHost() == null) {
+ throw newIAE("unsupported URI %s", uri);
}
}
-/*
+
@Override
- public HttpRequestBuilderImpl followRedirects(HttpClient.Redirect follow) {
- requireNonNull(follow);
- this.followRedirects = follow;
+ public HttpRequestBuilderImpl copy() {
+ HttpRequestBuilderImpl b = new HttpRequestBuilderImpl(this.uri);
+ b.userHeaders = this.userHeaders.deepCopy();
+ b.method = this.method;
+ b.expectContinue = this.expectContinue;
+ b.bodyPublisher = bodyPublisher;
+ b.uri = uri;
+ b.duration = duration;
+ b.version = version;
+ return b;
+ }
+
+ private void checkNameAndValue(String name, String value) {
+ requireNonNull(name, "name");
+ requireNonNull(value, "value");
+ if (!isValidName(name)) {
+ throw newIAE("invalid header name:", name);
+ }
+ if (!isValidValue(value)) {
+ throw newIAE("invalid header value:%s", value);
+ }
+ }
+
+ @Override
+ public HttpRequestBuilderImpl setHeader(String name, String value) {
+ checkNameAndValue(name, value);
+ userHeaders.setHeader(name, value);
return this;
}
-*/
+
@Override
public HttpRequestBuilderImpl header(String name, String value) {
checkNameAndValue(name, value);
@@ -93,8 +126,8 @@
@Override
public HttpRequestBuilderImpl headers(String... params) {
requireNonNull(params);
- if (params.length % 2 != 0) {
- throw new IllegalArgumentException("wrong number of parameters");
+ if (params.length == 0 || params.length % 2 != 0) {
+ throw newIAE("wrong number, %d, of parameters", params.length);
}
for (int i = 0; i < params.length; i += 2) {
String name = params[i];
@@ -104,45 +137,6 @@
return this;
}
- /*
- @Override
- public HttpRequestBuilderImpl proxy(ProxySelector proxy) {
- requireNonNull(proxy);
- this.proxy = proxy;
- return this;
- }
-*/
- @Override
- public HttpRequestBuilderImpl copy() {
- HttpRequestBuilderImpl b = new HttpRequestBuilderImpl(this.uri);
- b.userHeaders = this.userHeaders.deepCopy();
- b.method = this.method;
- //b.followRedirects = this.followRedirects;
- b.expectContinue = this.expectContinue;
- b.body = body;
- b.uri = uri;
- //b.proxy = proxy;
- return b;
- }
-
- @Override
- public HttpRequestBuilderImpl setHeader(String name, String value) {
- checkNameAndValue(name, value);
- userHeaders.setHeader(name, value);
- return this;
- }
-
- private void checkNameAndValue(String name, String value) {
- requireNonNull(name, "name");
- requireNonNull(value, "value");
- if (!isValidName(name)) {
- throw new IllegalArgumentException("invalid header name");
- }
- if (!isValidValue(value)) {
- throw new IllegalArgumentException("invalid header value");
- }
- }
-
@Override
public HttpRequestBuilderImpl expectContinue(boolean enable) {
expectContinue = enable;
@@ -158,49 +152,58 @@
HttpHeadersImpl headers() { return userHeaders; }
- //HttpClientImpl client() {return client;}
-
URI uri() { return uri; }
String method() { return method; }
- //HttpClient.Redirect followRedirects() { return followRedirects; }
-
- //ProxySelector proxy() { return proxy; }
-
boolean expectContinue() { return expectContinue; }
- public HttpRequest.BodyProcessor body() { return body; }
+ BodyPublisher bodyPublisher() { return bodyPublisher; }
Optional<HttpClient.Version> version() { return version; }
@Override
- public HttpRequest.Builder GET() { return method("GET", null); }
+ public HttpRequest.Builder GET() {
+ return method0("GET", null);
+ }
@Override
- public HttpRequest.Builder POST(BodyProcessor body) {
- return method("POST", body);
+ public HttpRequest.Builder POST(BodyPublisher body) {
+ return method0("POST", requireNonNull(body));
+ }
+
+ @Override
+ public HttpRequest.Builder DELETE(BodyPublisher body) {
+ return method0("DELETE", requireNonNull(body));
}
@Override
- public HttpRequest.Builder DELETE(BodyProcessor body) {
- return method("DELETE", body);
+ public HttpRequest.Builder PUT(BodyPublisher body) {
+ return method0("PUT", requireNonNull(body));
}
@Override
- public HttpRequest.Builder PUT(BodyProcessor body) {
- return method("PUT", body);
+ public HttpRequest.Builder method(String method, BodyPublisher body) {
+ requireNonNull(method);
+ if (method.equals(""))
+ throw newIAE("illegal method <empty string>");
+ return method0(method, requireNonNull(body));
}
- @Override
- public HttpRequest.Builder method(String method, BodyProcessor body) {
- this.method = requireNonNull(method);
- this.body = body;
+ private HttpRequest.Builder method0(String method, BodyPublisher body) {
+ assert method != null;
+ assert !method.equals("GET") ? body != null : true;
+ assert !method.equals("");
+ this.method = method;
+ this.bodyPublisher = body;
return this;
}
@Override
public HttpRequest build() {
+ if (uri == null)
+ throw new IllegalStateException("uri is null");
+ assert method != null;
return new HttpRequestImpl(this);
}
@@ -213,6 +216,6 @@
return this;
}
- Duration duration() { return duration; }
+ Duration timeout() { return duration; }
}
--- a/src/jdk.incubator.httpclient/share/classes/jdk/incubator/http/HttpRequestImpl.java Sun Nov 05 17:05:57 2017 +0000
+++ b/src/jdk.incubator.httpclient/share/classes/jdk/incubator/http/HttpRequestImpl.java Sun Nov 05 17:32:13 2017 +0000
@@ -33,6 +33,8 @@
import java.net.ProxySelector;
import java.net.URI;
import java.security.AccessControlContext;
+import java.security.AccessController;
+import java.security.PrivilegedAction;
import java.time.Duration;
import java.util.Locale;
import java.util.Optional;
@@ -46,12 +48,12 @@
private final URI uri;
private InetSocketAddress authority; // only used when URI not specified
private final String method;
- final BodyProcessor requestProcessor;
+ final BodyPublisher requestPublisher;
final boolean secure;
final boolean expectContinue;
private boolean isWebSocket;
private AccessControlContext acc;
- private final Duration duration;
+ private final Duration timeout; // may be null
private final Optional<HttpClient.Version> version;
/**
@@ -59,27 +61,24 @@
*/
public HttpRequestImpl(HttpRequestBuilderImpl builder) {
String method = builder.method();
- this.method = method == null? "GET" : method;
+ this.method = method == null ? "GET" : method;
this.userHeaders = ImmutableHeaders.of(builder.headers().map(), ALLOWED_HEADERS);
this.systemHeaders = new HttpHeadersImpl();
this.uri = builder.uri();
+ assert uri != null;
this.expectContinue = builder.expectContinue();
this.secure = uri.getScheme().toLowerCase(Locale.US).equals("https");
- if (builder.body() == null) {
- this.requestProcessor = HttpRequest.noBody();
- } else {
- this.requestProcessor = builder.body();
- }
- this.duration = builder.duration();
+ this.requestPublisher = builder.bodyPublisher(); // may be null
+ this.timeout = builder.timeout();
this.version = builder.version();
}
/**
* Creates an HttpRequestImpl from the given request.
*/
- public HttpRequestImpl(HttpRequest request) {
+ public HttpRequestImpl(HttpRequest request, AccessControlContext acc) {
String method = request.method();
- this.method = method == null? "GET" : method;
+ this.method = method == null ? "GET" : method;
this.userHeaders = request.headers();
if (request instanceof HttpRequestImpl) {
this.systemHeaders = ((HttpRequestImpl) request).systemHeaders;
@@ -90,12 +89,12 @@
this.uri = request.uri();
this.expectContinue = request.expectContinue();
this.secure = uri.getScheme().toLowerCase(Locale.US).equals("https");
- if (!request.bodyProcessor().isPresent()) {
- this.requestProcessor = HttpRequest.noBody();
- } else {
- this.requestProcessor = request.bodyProcessor().get();
+ this.requestPublisher = request.bodyPublisher().orElse(null);
+ if (acc != null && requestPublisher instanceof RequestPublishers.FilePublisher) {
+ // Restricts the file publisher with the senders ACC, if any
+ ((RequestPublishers.FilePublisher)requestPublisher).setAccessControlContext(acc);
}
- this.duration = request.duration();
+ this.timeout = request.timeout().orElse(null);
this.version = request.version();
}
@@ -110,28 +109,34 @@
this.uri = uri;
this.expectContinue = other.expectContinue;
this.secure = uri.getScheme().toLowerCase(Locale.US).equals("https");
- this.requestProcessor = other.requestProcessor;
+ this.requestPublisher = other.requestPublisher; // may be null
this.acc = other.acc;
- this.duration = other.duration;
+ this.timeout = other.timeout;
this.version = other.version();
}
/* used for creating CONNECT requests */
- HttpRequestImpl(String method, HttpClientImpl client,
- InetSocketAddress authority) {
+ HttpRequestImpl(String method, InetSocketAddress authority) {
// TODO: isWebSocket flag is not specified, but the assumption is that
// such a request will never be made on a connection that will be returned
// to the connection pool (we might need to revisit this constructor later)
this.method = method;
this.systemHeaders = new HttpHeadersImpl();
this.userHeaders = ImmutableHeaders.empty();
- this.uri = URI.create("socket://" + authority.getHostString() + ":" + Integer.toString(authority.getPort()) + "/");
- this.requestProcessor = HttpRequest.noBody();
+ this.uri = URI.create("socket://" + authority.getHostString() + ":"
+ + Integer.toString(authority.getPort()) + "/");
+ this.requestPublisher = null;
this.authority = authority;
this.secure = false;
this.expectContinue = false;
- this.duration = null;
- this.version = Optional.of(client.version());
+ this.timeout = null;
+ // The CONNECT request sent for tunneling is only used in two cases:
+ // 1. websocket, which only supports HTTP/1.1
+ // 2. SSL tunneling through a HTTP/1.1 proxy
+ // In either case we do not want to upgrade the connection to the proxy.
+ // What we want to possibly upgrade is the tunneled connection to the
+ // target server (so not the CONNECT request itself)
+ this.version = Optional.of(HttpClient.Version.HTTP_1_1);
}
/**
@@ -166,9 +171,9 @@
this.systemHeaders = parent.systemHeaders;
this.expectContinue = parent.expectContinue;
this.secure = parent.secure;
- this.requestProcessor = parent.requestProcessor;
+ this.requestPublisher = parent.requestPublisher;
this.acc = parent.acc;
- this.duration = parent.duration;
+ this.timeout = parent.timeout;
this.version = parent.version;
}
@@ -195,9 +200,6 @@
InetSocketAddress proxy(HttpClientImpl client) {
ProxySelector ps = client.proxy().orElse(null);
- if (ps == null) {
- ps = client.proxy().orElse(null);
- }
if (ps == null || method.equalsIgnoreCase("CONNECT")) {
return null;
}
@@ -215,15 +217,10 @@
return isWebSocket;
}
-// /** Returns the follow-redirects setting for this request. */
-// @Override
-// public jdk.incubator.http.HttpClient.Redirect followRedirects() {
-// return followRedirects;
-// }
-
@Override
- public Optional<BodyProcessor> bodyProcessor() {
- return Optional.of(requestProcessor);
+ public Optional<BodyPublisher> bodyPublisher() {
+ return requestPublisher == null ? Optional.empty()
+ : Optional.of(requestPublisher);
}
/**
@@ -237,14 +234,10 @@
public URI uri() { return uri; }
@Override
- public Duration duration() {
- return duration;
+ public Optional<Duration> timeout() {
+ return timeout == null ? Optional.empty() : Optional.of(timeout);
}
-// HttpClientImpl client() {
-// return client;
-// }
-
HttpHeaders getUserHeaders() { return userHeaders; }
HttpHeadersImpl getSystemHeaders() { return systemHeaders; }
@@ -261,57 +254,24 @@
systemHeaders.setHeader(name, value);
}
-// @Override
-// public <T> HttpResponse<T>
-// response(HttpResponse.BodyHandler<T> responseHandler)
-// throws IOException, InterruptedException
-// {
-// if (!sent.compareAndSet(false, true)) {
-// throw new IllegalStateException("request already sent");
-// }
-// MultiExchange<Void,T> mex = new MultiExchange<>(this, responseHandler);
-// return mex.response();
-// }
-//
-// @Override
-// public <T> CompletableFuture<HttpResponse<T>>
-// responseAsync(HttpResponse.BodyHandler<T> responseHandler)
-// {
-// if (!sent.compareAndSet(false, true)) {
-// throw new IllegalStateException("request already sent");
-// }
-// MultiExchange<Void,T> mex = new MultiExchange<>(this, responseHandler);
-// return mex.responseAsync(null)
-// .thenApply((HttpResponseImpl<T> b) -> (HttpResponse<T>) b);
-// }
-//
-// @Override
-// public <U, T> CompletableFuture<U>
-// multiResponseAsync(HttpResponse.MultiProcessor<U, T> responseHandler)
-// {
-// if (!sent.compareAndSet(false, true)) {
-// throw new IllegalStateException("request already sent");
-// }
-// MultiExchange<U,T> mex = new MultiExchange<>(this, responseHandler);
-// return mex.multiResponseAsync();
-// }
-
- public InetSocketAddress getAddress(HttpClientImpl client) {
+ InetSocketAddress getAddress(HttpClientImpl client) {
URI uri = uri();
if (uri == null) {
return authority();
}
- int port = uri.getPort();
- if (port == -1) {
+ int p = uri.getPort();
+ if (p == -1) {
if (uri.getScheme().equalsIgnoreCase("https")) {
- port = 443;
+ p = 443;
} else {
- port = 80;
+ p = 80;
}
}
- String host = uri.getHost();
+ final String host = uri.getHost();
+ final int port = p;
if (proxy(client) == null) {
- return new InetSocketAddress(host, port);
+ PrivilegedAction<InetSocketAddress> pa = () -> new InetSocketAddress(host, port);
+ return AccessController.doPrivileged(pa);
} else {
return InetSocketAddress.createUnresolved(host, port);
}
--- a/src/jdk.incubator.httpclient/share/classes/jdk/incubator/http/HttpResponse.java Sun Nov 05 17:05:57 2017 +0000
+++ b/src/jdk.incubator.httpclient/share/classes/jdk/incubator/http/HttpResponse.java Sun Nov 05 17:32:13 2017 +0000
@@ -26,20 +26,22 @@
package jdk.incubator.http;
import java.io.IOException;
-import java.io.UncheckedIOException;
+import java.io.InputStream;
import java.net.URI;
-import jdk.incubator.http.ResponseProcessors.MultiFile;
-import jdk.incubator.http.ResponseProcessors.MultiProcessorImpl;
+import jdk.incubator.http.ResponseSubscribers.MultiSubscriberImpl;
import static jdk.incubator.http.internal.common.Utils.unchecked;
import static jdk.incubator.http.internal.common.Utils.charsetFrom;
import java.nio.ByteBuffer;
import java.nio.charset.Charset;
-import java.nio.charset.StandardCharsets;
+import java.nio.channels.FileChannel;
import java.nio.file.OpenOption;
import java.nio.file.Path;
import java.nio.file.Paths;
import java.nio.file.StandardOpenOption;
-import java.util.Map;
+import java.security.AccessControlContext;
+import java.util.Arrays;
+import java.util.List;
+import java.util.Objects;
import java.util.Optional;
import java.util.concurrent.CompletableFuture;
import java.util.concurrent.CompletionStage;
@@ -52,7 +54,7 @@
* Represents a response to a {@link HttpRequest}.
* {@Incubating}
*
- * <p>A {@code HttpResponse} is available when the response status code and
+ * <p> A {@code HttpResponse} is available when the response status code and
* headers have been received, and typically after the response body has also
* been received. This depends on the response body handler provided when
* sending the request. In all cases, the response body handler is invoked
@@ -61,23 +63,24 @@
*
* <p> Methods are provided in this class for accessing the response headers,
* and response body.
- * <p>
- * <b>Response handlers and processors</b>
- * <p>
- * Response bodies are handled at two levels. Application code supplies a response
- * handler ({@link BodyHandler}) which may examine the response status code
- * and headers, and which then returns a {@link BodyProcessor} to actually read
- * (or discard) the body and convert it into some useful Java object type. The handler
- * can return one of the pre-defined processor types, or a custom processor, or
- * if the body is to be discarded, it can call {@link BodyProcessor#discard(Object)
- * BodyProcessor.discard()} and return a processor which discards the response body.
- * Static implementations of both handlers and processors are provided in
- * {@link BodyHandler BodyHandler} and {@link BodyProcessor BodyProcessor} respectively.
- * In all cases, the handler functions provided are convenience implementations
- * which ignore the supplied status code and
- * headers and return the relevant pre-defined {@code BodyProcessor}.
- * <p>
- * See {@link BodyHandler} for example usage.
+ *
+ * <p><b>Response handlers and subscribers</b>
+ *
+ * <p> Response bodies are handled at two levels. Application code supplies a
+ * response handler ({@link BodyHandler}) which may examine the response status
+ * code and headers, and which then returns a {@link BodySubscriber} to actually
+ * read (or discard) the body and convert it into some useful Java object type.
+ * The handler can return one of the pre-defined subscriber types, or a custom
+ * subscriber, or if the body is to be discarded it can call {@link
+ * BodySubscriber#discard(Object) discard} and return a subscriber which
+ * discards the response body. Static implementations of both handlers and
+ * subscribers are provided in {@linkplain BodyHandler BodyHandler} and
+ * {@linkplain BodySubscriber BodySubscriber} respectively. In all cases, the
+ * handler functions provided are convenience implementations which ignore the
+ * supplied status code and headers and return the relevant pre-defined {@code
+ * BodySubscriber}.
+ *
+ * <p> See {@link BodyHandler} for example usage.
*
* @param <T> the response body type
* @since 9
@@ -120,17 +123,17 @@
/**
* Returns the received response trailers, if there are any, when they
- * become available. For many response processor types this will be at the same
- * time as the {@code HttpResponse} itself is available. In such cases, the
- * returned {@code CompletableFuture} will be already completed.
+ * become available. For many response subscriber types this will be at the
+ * same time as the {@code HttpResponse} itself is available. In such cases,
+ * the returned {@code CompletableFuture} will be already completed.
*
* @return a CompletableFuture of the response trailers (may be empty)
*/
public abstract CompletableFuture<HttpHeaders> trailers();
/**
- * Returns the body. Depending on the type of {@code T}, the returned body may
- * represent the body after it was read (such as {@code byte[]}, or
+ * Returns the body. Depending on the type of {@code T}, the returned body
+ * may represent the body after it was read (such as {@code byte[]}, or
* {@code String}, or {@code Path}) or it may represent an object with
* which the body is read, such as an {@link java.io.InputStream}.
*
@@ -161,18 +164,101 @@
*/
public abstract HttpClient.Version version();
+
+ private static String pathForSecurityCheck(Path path) {
+ return path.toFile().getPath();
+ }
+
+ /** A body handler that is further restricted by a given ACC. */
+ interface UntrustedBodyHandler<T> extends BodyHandler<T> {
+ void setAccessControlContext(AccessControlContext acc);
+ }
+
+ /**
+ * A Path body handler.
+ *
+ * Note: Exists mainly too allow setting of the senders ACC post creation of
+ * the handler.
+ */
+ static class PathBodyHandler implements UntrustedBodyHandler<Path> {
+ private final Path file;
+ private final OpenOption[]openOptions;
+ private volatile AccessControlContext acc;
+
+ PathBodyHandler(Path file, OpenOption... openOptions) {
+ this.file = file;
+ this.openOptions = openOptions;
+ }
+
+ @Override
+ public void setAccessControlContext(AccessControlContext acc) {
+ this.acc = acc;
+ }
+
+ @Override
+ public BodySubscriber<Path> apply(int statusCode, HttpHeaders headers) {
+ ResponseSubscribers.PathSubscriber bs = (ResponseSubscribers.PathSubscriber)
+ BodySubscriber.asFileImpl(file, openOptions);
+ bs.setAccessControlContext(acc);
+ return bs;
+ }
+ }
+
+ // Similar to Path body handler, but for file download. Supports setting ACC.
+ static class FileDownloadBodyHandler implements UntrustedBodyHandler<Path> {
+ private final Path directory;
+ private final OpenOption[]openOptions;
+ private volatile AccessControlContext acc;
+
+ FileDownloadBodyHandler(Path directory, OpenOption... openOptions) {
+ this.directory = directory;
+ this.openOptions = openOptions;
+ }
+
+ @Override
+ public void setAccessControlContext(AccessControlContext acc) {
+ this.acc = acc;
+ }
+
+ @Override
+ public BodySubscriber<Path> apply(int statusCode, HttpHeaders headers) {
+ String dispoHeader = headers.firstValue("Content-Disposition")
+ .orElseThrow(() -> unchecked(new IOException("No Content-Disposition")));
+ if (!dispoHeader.startsWith("attachment;")) {
+ throw unchecked(new IOException("Unknown Content-Disposition type"));
+ }
+ int n = dispoHeader.indexOf("filename=");
+ if (n == -1) {
+ throw unchecked(new IOException("Bad Content-Disposition type"));
+ }
+ int lastsemi = dispoHeader.lastIndexOf(';');
+ String disposition;
+ if (lastsemi < n) {
+ disposition = dispoHeader.substring(n + 9);
+ } else {
+ disposition = dispoHeader.substring(n + 9, lastsemi);
+ }
+ Path file = Paths.get(directory.toString(), disposition);
+
+ ResponseSubscribers.PathSubscriber bs = (ResponseSubscribers.PathSubscriber)
+ BodySubscriber.asFileImpl(file, openOptions);
+ bs.setAccessControlContext(acc);
+ return bs;
+ }
+ }
+
/**
* A handler for response bodies.
* {@Incubating}
- * <p>
- * This is a function that takes two parameters: the response status code,
- * and the response headers, and which returns a {@link BodyProcessor}.
+ *
+ * <p> This is a function that takes two parameters: the response status code,
+ * and the response headers, and which returns a {@linkplain BodySubscriber}.
* The function is always called just before the response body is read. Its
* implementation may examine the status code or headers and must decide,
* whether to accept the response body or discard it, and if accepting it,
* exactly how to handle it.
- * <p>
- * Some pre-defined implementations which do not utilize the status code
+ *
+ * <p> Some pre-defined implementations which do not utilize the status code
* or headers (meaning the body is always accepted) are defined:
* <ul><li>{@link #asByteArray() }</li>
* <li>{@link #asByteArrayConsumer(java.util.function.Consumer)
@@ -182,15 +268,15 @@
* <li>{@link #discard(Object) }</li>
* <li>{@link #asString(java.nio.charset.Charset)
* asString(Charset)}</li></ul>
- * <p>
- * These implementations return the equivalent {@link BodyProcessor}.
+ *
+ * <p> These implementations return the equivalent {@link BodySubscriber}.
* Alternatively, the handler can be used to examine the status code
- * or headers and return different body processors as appropriate.
- * <p>
- * <b>Examples of handler usage</b>
- * <p>
- * The first example uses one of the predefined handler functions which
- * ignore the response headers and status, and always process the response
+ * or headers and return different body subscribers as appropriate.
+ *
+ * <p><b>Examples of handler usage</b>
+ *
+ * <p> The first example uses one of the predefined handler functions which
+ * ignores the response headers and status, and always process the response
* body in the same way.
* <pre>
* {@code
@@ -201,11 +287,11 @@
* }
* </pre>
* Note, that even though these pre-defined handlers ignore the status code
- * and headers, this information is still accessible from the {@code HttpResponse}
- * when it is returned.
- * <p>
- * In the second example, the function returns a different processor depending
- * on the status code.
+ * and headers, this information is still accessible from the
+ * {@code HttpResponse} when it is returned.
+ *
+ * <p> In the second example, the function returns a different subscriber
+ * depending on the status code.
* <pre>
* {@code
* HttpResponse<Path> resp1 = HttpRequest
@@ -213,93 +299,134 @@
* .GET()
* .response(
* (status, headers) -> status == 200
- * ? BodyProcessor.asFile(Paths.get("/tmp/f"))
- * : BodyProcessor.discard(Paths.get("/NULL")));
+ * ? BodySubscriber.asFile(Paths.get("/tmp/f"))
+ * : BodySubscriber.discard(Paths.get("/NULL")));
* }
* </pre>
*
- * @param <T> the response body type.
+ * @param <T> the response body type
*/
@FunctionalInterface
public interface BodyHandler<T> {
/**
- * Returns a {@link BodyProcessor BodyProcessor} considering the given response status
- * code and headers. This method is always called before the body is read
- * and its implementation can decide to keep the body and store it somewhere
- * or else discard it, by returning the {@code BodyProcessor} returned
- * from {@link BodyProcessor#discard(java.lang.Object) discard()}.
+ * Returns a {@link BodySubscriber BodySubscriber} considering the given
+ * response status code and headers. This method is always called before
+ * the body is read and its implementation can decide to keep the body
+ * and store it somewhere, or else discard it by returning the {@code
+ * BodySubscriber} returned from {@link BodySubscriber#discard(Object)
+ * discard}.
*
* @param statusCode the HTTP status code received
* @param responseHeaders the response headers received
- * @return a response body handler
+ * @return a body subscriber
*/
- public BodyProcessor<T> apply(int statusCode, HttpHeaders responseHeaders);
+ public BodySubscriber<T> apply(int statusCode, HttpHeaders responseHeaders);
/**
* Returns a response body handler which discards the response body and
* uses the given value as a replacement for it.
*
* @param <U> the response body type
- * @param value the value of U to return as the body
+ * @param value the value of U to return as the body, may be null
* @return a response body handler
*/
public static <U> BodyHandler<U> discard(U value) {
- return (status, headers) -> BodyProcessor.discard(value);
+ return (status, headers) -> BodySubscriber.discard(value);
}
/**
* Returns a {@code BodyHandler<String>} that returns a
- * {@link BodyProcessor BodyProcessor}{@code <String>} obtained from
- * {@link BodyProcessor#asString(java.nio.charset.Charset)
- * BodyProcessor.asString(Charset)}. If a charset is provided, the
- * body is decoded using it. If charset is {@code null} then the processor
- * tries to determine the character set from the {@code Content-encoding}
- * header. If that charset is not supported then
- * {@link java.nio.charset.StandardCharsets#UTF_8 UTF_8} is used.
+ * {@link BodySubscriber BodySubscriber}{@code <String>} obtained from
+ * {@link BodySubscriber#asString(Charset) BodySubscriber.asString(Charset)}.
+ * If a charset is provided, the body is decoded using it. If charset is
+ * {@code null} then the handler tries to determine the character set
+ * from the {@code Content-encoding} header. If that charset is not
+ * supported then {@link java.nio.charset.StandardCharsets#UTF_8 UTF_8}
+ * is used.
*
- * @param charset the name of the charset to interpret the body as. If
- * {@code null} then charset determined from Content-encoding header
+ * @param charset The name of the charset to interpret the body as. If
+ * {@code null} then the charset is determined from the
+ * <i>Content-encoding</i> header.
* @return a response body handler
*/
public static BodyHandler<String> asString(Charset charset) {
return (status, headers) -> {
if (charset != null) {
- return BodyProcessor.asString(charset);
+ return BodySubscriber.asString(charset);
}
- return BodyProcessor.asString(charsetFrom(headers));
+ return BodySubscriber.asString(charsetFrom(headers));
};
}
-
- /**
- * Returns a {@code BodyHandler<Path>} that returns a
- * {@link BodyProcessor BodyProcessor}{@code <Path>} obtained from
- * {@link BodyProcessor#asFile(Path) BodyProcessor.asFile(Path)}.
- * <p>
- * When the {@code HttpResponse} object is returned, the body has been completely
- * written to the file, and {@link #body()} returns a reference to its
- * {@link Path}.
- *
- * @param file the file to store the body in
- * @return a response body handler
- */
- public static BodyHandler<Path> asFile(Path file) {
- return (status, headers) -> BodyProcessor.asFile(file);
- }
-
/**
* Returns a {@code BodyHandler<Path>} that returns a
- * {@link BodyProcessor BodyProcessor}<{@link Path}>
+ * {@link BodySubscriber BodySubscriber}{@code <Path>} obtained from
+ * {@link BodySubscriber#asFile(Path, OpenOption...)
+ * BodySubscriber.asFile(Path,OpenOption...)}.
+ *
+ * <p> When the {@code HttpResponse} object is returned, the body has
+ * been completely written to the file, and {@link #body()} returns a
+ * reference to its {@link Path}.
+ *
+ * @param file the filename to store the body in
+ * @param openOptions any options to use when opening/creating the file
+ * @return a response body handler
+ * @throws SecurityException If a security manager has been installed
+ * and it denies {@link SecurityManager#checkWrite(String)
+ * write access} to the file. The {@link
+ * SecurityManager#checkDelete(String) checkDelete} method is
+ * invoked to check delete access if the file is opened with
+ * the {@code DELETE_ON_CLOSE} option.
+ */
+ public static BodyHandler<Path> asFile(Path file, OpenOption... openOptions) {
+ Objects.requireNonNull(file);
+ SecurityManager sm = System.getSecurityManager();
+ if (sm != null) {
+ String fn = pathForSecurityCheck(file);
+ sm.checkWrite(fn);
+ List<OpenOption> opts = Arrays.asList(openOptions);
+ if (opts.contains(StandardOpenOption.DELETE_ON_CLOSE))
+ sm.checkDelete(fn);
+ if (opts.contains(StandardOpenOption.READ))
+ sm.checkRead(fn);
+ }
+ return new PathBodyHandler(file, openOptions);
+ }
+
+ /**
+ * Returns a {@code BodyHandler<Path>} that returns a
+ * {@link BodySubscriber BodySubscriber}{@code <Path>} obtained from
+ * {@link BodySubscriber#asFile(Path) BodySubscriber.asFile(Path)}.
+ *
+ * <p> When the {@code HttpResponse} object is returned, the body has
+ * been completely written to the file, and {@link #body()} returns a
+ * reference to its {@link Path}.
+ *
+ * @param file the file to store the body in
+ * @return a response body handler
+ * @throws SecurityException if a security manager has been installed
+ * and it denies {@link SecurityManager#checkWrite(String)
+ * write access} to the file
+ */
+ public static BodyHandler<Path> asFile(Path file) {
+ return BodyHandler.asFile(file, StandardOpenOption.CREATE,
+ StandardOpenOption.WRITE);
+ }
+
+ /**
+ * Returns a {@code BodyHandler<Path>} that returns a
+ * {@link BodySubscriber BodySubscriber}<{@link Path}>
* where the download directory is specified, but the filename is
* obtained from the {@code Content-Disposition} response header. The
- * {@code Content-Disposition} header must specify the <i>attachment</i> type
- * and must also contain a
- * <i>filename</i> parameter. If the filename specifies multiple path
- * components only the final component is used as the filename (with the
- * given directory name). When the {@code HttpResponse} object is
- * returned, the body has been completely written to the file and {@link
- * #body()} returns a {@code Path} object for the file. The returned {@code Path} is the
+ * {@code Content-Disposition} header must specify the <i>attachment</i>
+ * type and must also contain a <i>filename</i> parameter. If the
+ * filename specifies multiple path components only the final component
+ * is used as the filename (with the given directory name).
+ *
+ * <p> When the {@code HttpResponse} object is returned, the body has
+ * been completely written to the file and {@link #body()} returns a
+ * {@code Path} object for the file. The returned {@code Path} is the
* combination of the supplied directory name and the file name supplied
* by the server. If the destination directory does not exist or cannot
* be written to, then the response will fail with an {@link IOException}.
@@ -307,245 +434,333 @@
* @param directory the directory to store the file in
* @param openOptions open options
* @return a response body handler
+ * @throws SecurityException If a security manager has been installed
+ * and it denies {@link SecurityManager#checkWrite(String)
+ * write access} to the file. The {@link
+ * SecurityManager#checkDelete(String) checkDelete} method is
+ * invoked to check delete access if the file is opened with
+ * the {@code DELETE_ON_CLOSE} option.
*/
- public static BodyHandler<Path> asFileDownload(Path directory, OpenOption... openOptions) {
- return (status, headers) -> {
- String dispoHeader = headers.firstValue("Content-Disposition")
- .orElseThrow(() -> unchecked(new IOException("No Content-Disposition")));
- if (!dispoHeader.startsWith("attachment;")) {
- throw unchecked(new IOException("Unknown Content-Disposition type"));
- }
- int n = dispoHeader.indexOf("filename=");
- if (n == -1) {
- throw unchecked(new IOException("Bad Content-Disposition type"));
- }
- int lastsemi = dispoHeader.lastIndexOf(';');
- String disposition;
- if (lastsemi < n) {
- disposition = dispoHeader.substring(n + 9);
- } else {
- disposition = dispoHeader.substring(n + 9, lastsemi);
- }
- Path file = Paths.get(directory.toString(), disposition);
- return BodyProcessor.asFile(file, openOptions);
- };
+ //####: check if the dir exists and is writable??
+ public static BodyHandler<Path> asFileDownload(Path directory,
+ OpenOption... openOptions) {
+ Objects.requireNonNull(directory);
+ SecurityManager sm = System.getSecurityManager();
+ if (sm != null) {
+ String fn = pathForSecurityCheck(directory);
+ sm.checkWrite(fn);
+ List<OpenOption> opts = Arrays.asList(openOptions);
+ if (opts.contains(StandardOpenOption.DELETE_ON_CLOSE))
+ sm.checkDelete(fn);
+ if (opts.contains(StandardOpenOption.READ))
+ sm.checkRead(fn);
+ }
+ return new FileDownloadBodyHandler(directory, openOptions);
}
/**
- * Returns a {@code BodyHandler<Path>} that returns a
- * {@link BodyProcessor BodyProcessor}{@code <Path>} obtained from
- * {@link BodyProcessor#asFile(java.nio.file.Path, java.nio.file.OpenOption...)
- * BodyProcessor.asFile(Path,OpenOption...)}.
- * <p>
- * When the {@code HttpResponse} object is returned, the body has been completely
- * written to the file, and {@link #body()} returns a reference to its
- * {@link Path}.
+ * Returns a {@code BodyHandler<InputStream>} that returns a
+ * {@link BodySubscriber BodySubscriber}{@code <InputStream>} obtained
+ * from {@link BodySubscriber#asInputStream() BodySubscriber.asInputStream}.
*
- * @param file the filename to store the body in
- * @param openOptions any options to use when opening/creating the file
+ * <p> When the {@code HttpResponse} object is returned, the response
+ * headers will have been completely read, but the body may not have
+ * been fully received yet. The {@link #body()} method returns an
+ * {@link InputStream} from which the body can be read as it is received.
+ *
* @return a response body handler
*/
- public static BodyHandler<Path> asFile(Path file, OpenOption... openOptions) {
- return (status, headers) -> BodyProcessor.asFile(file, openOptions);
+ public static BodyHandler<InputStream> asInputStream() {
+ return (status, headers) -> BodySubscriber.asInputStream();
}
/**
* Returns a {@code BodyHandler<Void>} that returns a
- * {@link BodyProcessor BodyProcessor}{@code <Void>} obtained from
- * {@link BodyProcessor#asByteArrayConsumer(java.util.function.Consumer)
- * BodyProcessor.asByteArrayConsumer(Consumer)}.
- * <p>
- * When the {@code HttpResponse} object is returned, the body has been completely
- * written to the consumer.
+ * {@link BodySubscriber BodySubscriber}{@code <Void>} obtained from
+ * {@link BodySubscriber#asByteArrayConsumer(Consumer)
+ * BodySubscriber.asByteArrayConsumer(Consumer)}.
+ *
+ * <p> When the {@code HttpResponse} object is returned, the body has
+ * been completely written to the consumer.
*
* @param consumer a Consumer to accept the response body
* @return a response body handler
*/
public static BodyHandler<Void> asByteArrayConsumer(Consumer<Optional<byte[]>> consumer) {
- return (status, headers) -> BodyProcessor.asByteArrayConsumer(consumer);
+ return (status, headers) -> BodySubscriber.asByteArrayConsumer(consumer);
}
/**
* Returns a {@code BodyHandler<byte[]>} that returns a
- * {@link BodyProcessor BodyProcessor}<{@code byte[]}> obtained
- * from {@link BodyProcessor#asByteArray() BodyProcessor.asByteArray()}.
- * <p>
- * When the {@code HttpResponse} object is returned, the body has been completely
- * written to the byte array.
+ * {@link BodySubscriber BodySubscriber}<{@code byte[]}> obtained
+ * from {@link BodySubscriber#asByteArray() BodySubscriber.asByteArray()}.
+ *
+ * <p> When the {@code HttpResponse} object is returned, the body has
+ * been completely written to the byte array.
*
* @return a response body handler
*/
public static BodyHandler<byte[]> asByteArray() {
- return (status, headers) -> BodyProcessor.asByteArray();
+ return (status, headers) -> BodySubscriber.asByteArray();
}
/**
* Returns a {@code BodyHandler<String>} that returns a
- * {@link BodyProcessor BodyProcessor}{@code <String>} obtained from
- * {@link BodyProcessor#asString(java.nio.charset.Charset)
- * BodyProcessor.asString(Charset)}. The body is
+ * {@link BodySubscriber BodySubscriber}{@code <String>} obtained from
+ * {@link BodySubscriber#asString(java.nio.charset.Charset)
+ * BodySubscriber.asString(Charset)}. The body is
* decoded using the character set specified in
* the {@code Content-encoding} response header. If there is no such
* header, or the character set is not supported, then
* {@link java.nio.charset.StandardCharsets#UTF_8 UTF_8} is used.
- * <p>
- * When the {@code HttpResponse} object is returned, the body has been completely
- * written to the string.
+ *
+ * <p> When the {@code HttpResponse} object is returned, the body has
+ * been completely written to the string.
*
* @return a response body handler
*/
public static BodyHandler<String> asString() {
- return (status, headers) -> BodyProcessor.asString(charsetFrom(headers));
+ return (status, headers) -> BodySubscriber.asString(charsetFrom(headers));
}
+
+ /**
+ * Returns a {@code BodyHandler} which, when invoked, returns a {@linkplain
+ * BodySubscriber#buffering(BodySubscriber,int) buffering BodySubscriber}
+ * that buffers data before delivering it to the downstream subscriber.
+ * These {@code BodySubscriber} instances are created by calling
+ * {@linkplain BodySubscriber#buffering(BodySubscriber,int)
+ * BodySubscriber.buffering} with a subscriber obtained from the given
+ * downstream handler and the {@code bufferSize} parameter.
+ *
+ * @param downstreamHandler the downstream handler
+ * @param bufferSize the buffer size parameter passed to {@linkplain
+ * BodySubscriber#buffering(BodySubscriber,int) BodySubscriber.buffering}
+ * @return a body handler
+ * @throws IllegalArgumentException if {@code bufferSize <= 0}
+ */
+ public static <T> BodyHandler<T> buffering(BodyHandler<T> downstreamHandler,
+ int bufferSize) {
+ if (bufferSize <= 0)
+ throw new IllegalArgumentException("must be greater than 0");
+ return (status, headers) -> BodySubscriber
+ .buffering(downstreamHandler.apply(status, headers),
+ bufferSize);
+ }
}
/**
- * A processor for response bodies.
+ * A subscriber for response bodies.
* {@Incubating}
- * <p>
- * The object acts as a {@link Flow.Subscriber}<{@link ByteBuffer}> to
- * the HTTP client implementation which publishes ByteBuffers containing the
- * response body. The processor converts the incoming buffers of data to
- * some user-defined object type {@code T}.
- * <p>
- * The {@link #getBody()} method returns a {@link CompletionStage}{@code <T>}
- * that provides the response body object. The {@code CompletionStage} must
- * be obtainable at any time. When it completes depends on the nature
- * of type {@code T}. In many cases, when {@code T} represents the entire body after being
- * read then it completes after the body has been read. If {@code T} is a streaming
- * type such as {@link java.io.InputStream} then it completes before the
- * body has been read, because the calling code uses it to consume the data.
+ *
+ * <p> The object acts as a {@link Flow.Subscriber}<{@link List}<{@link
+ * ByteBuffer}>> to the HTTP client implementation, which publishes
+ * unmodifiable lists of ByteBuffers containing the response body. The Flow
+ * of data, as well as the order of ByteBuffers in the Flow lists, is a
+ * strictly ordered representation of the response body. Both the Lists and
+ * the ByteBuffers, once passed to the subscriber, are no longer used by the
+ * HTTP client. The subscriber converts the incoming buffers of data to some
+ * user-defined object type {@code T}.
+ *
+ * <p> The {@link #getBody()} method returns a {@link CompletionStage}{@code
+ * <T>} that provides the response body object. The {@code CompletionStage}
+ * must be obtainable at any time. When it completes depends on the nature
+ * of type {@code T}. In many cases, when {@code T} represents the entire
+ * body after being read then it completes after the body has been read. If
+ * {@code T} is a streaming type such as {@link java.io.InputStream} then it
+ * completes before the body has been read, because the calling code uses it
+ * to consume the data.
*
* @param <T> the response body type
*/
- public interface BodyProcessor<T>
- extends Flow.Subscriber<ByteBuffer> {
+ public interface BodySubscriber<T>
+ extends Flow.Subscriber<List<ByteBuffer>> {
/**
- * Returns a {@code CompletionStage} which when completed will return the
- * response body object.
+ * Returns a {@code CompletionStage} which when completed will return
+ * the response body object.
*
* @return a CompletionStage for the response body
*/
public CompletionStage<T> getBody();
/**
- * Returns a body processor which stores the response body as a {@code
+ * Returns a body subscriber which stores the response body as a {@code
* String} converted using the given {@code Charset}.
- * <p>
- * The {@link HttpResponse} using this processor is available after the
- * entire response has been read.
+ *
+ * <p> The {@link HttpResponse} using this subscriber is available after
+ * the entire response has been read.
*
* @param charset the character set to convert the String with
- * @return a body processor
+ * @return a body subscriber
*/
- public static BodyProcessor<String> asString(Charset charset) {
- return new ResponseProcessors.ByteArrayProcessor<>(
+ public static BodySubscriber<String> asString(Charset charset) {
+ return new ResponseSubscribers.ByteArraySubscriber<>(
bytes -> new String(bytes, charset)
);
}
/**
- * Returns a {@code BodyProcessor} which stores the response body as a
+ * Returns a {@code BodySubscriber} which stores the response body as a
* byte array.
- * <p>
- * The {@link HttpResponse} using this processor is available after the
- * entire response has been read.
+ *
+ * <p> The {@link HttpResponse} using this subscriber is available after
+ * the entire response has been read.
*
- * @return a body processor
+ * @return a body subscriber
*/
- public static BodyProcessor<byte[]> asByteArray() {
- return new ResponseProcessors.ByteArrayProcessor<>(
+ public static BodySubscriber<byte[]> asByteArray() {
+ return new ResponseSubscribers.ByteArraySubscriber<>(
Function.identity() // no conversion
);
}
+ // no security check
+ static BodySubscriber<Path> asFileImpl(Path file, OpenOption... openOptions) {
+ return new ResponseSubscribers.PathSubscriber(file, openOptions);
+ }
+
/**
- * Returns a {@code BodyProcessor} which stores the response body in a
+ * Returns a {@code BodySubscriber} which stores the response body in a
* file opened with the given options and name. The file will be opened
- * with the given options using
- * {@link java.nio.channels.FileChannel#open(java.nio.file.Path,java.nio.file.OpenOption...)
- * FileChannel.open} just before the body is read. Any exception thrown will be returned
- * or thrown from {@link HttpClient#send(jdk.incubator.http.HttpRequest,
- * jdk.incubator.http.HttpResponse.BodyHandler) HttpClient::send}
- * or {@link HttpClient#sendAsync(jdk.incubator.http.HttpRequest,
- * jdk.incubator.http.HttpResponse.BodyHandler) HttpClient::sendAsync}
- * as appropriate.
- * <p>
- * The {@link HttpResponse} using this processor is available after the
- * entire response has been read.
+ * with the given options using {@link FileChannel#open(Path,OpenOption...)
+ * FileChannel.open} just before the body is read. Any exception thrown
+ * will be returned or thrown from {@link HttpClient#send(HttpRequest,
+ * BodyHandler) HttpClient::send} or {@link HttpClient#sendAsync(HttpRequest,
+ * BodyHandler) HttpClient::sendAsync} as appropriate.
+ *
+ * <p> The {@link HttpResponse} using this subscriber is available after
+ * the entire response has been read.
*
* @param file the file to store the body in
* @param openOptions the list of options to open the file with
- * @return a body processor
+ * @return a body subscriber
+ * @throws SecurityException If a security manager has been installed
+ * and it denies {@link SecurityManager#checkWrite(String)
+ * write access} to the file. The {@link
+ * SecurityManager#checkDelete(String) checkDelete} method is
+ * invoked to check delete access if the file is opened with the
+ * {@code DELETE_ON_CLOSE} option.
*/
- public static BodyProcessor<Path> asFile(Path file, OpenOption... openOptions) {
- return new ResponseProcessors.PathProcessor(file, openOptions);
+ public static BodySubscriber<Path> asFile(Path file, OpenOption... openOptions) {
+ Objects.requireNonNull(file);
+ SecurityManager sm = System.getSecurityManager();
+ if (sm != null) {
+ String fn = pathForSecurityCheck(file);
+ sm.checkWrite(fn);
+ List<OpenOption> opts = Arrays.asList(openOptions);
+ if (opts.contains(StandardOpenOption.DELETE_ON_CLOSE))
+ sm.checkDelete(fn);
+ if (opts.contains(StandardOpenOption.READ))
+ sm.checkRead(fn);
+ }
+ return asFileImpl(file, openOptions);
}
/**
- * Returns a {@code BodyProcessor} which provides the incoming body
- * data to the provided Consumer of {@code Optional<byte[]>}. Each
- * call to {@link Consumer#accept(java.lang.Object) Consumer.accept()}
- * will contain a non empty {@code Optional}, except for the final invocation after
- * all body data has been read, when the {@code Optional} will be empty.
- * <p>
- * The {@link HttpResponse} using this processor is available after the
- * entire response has been read.
+ * Returns a {@code BodySubscriber} which stores the response body in a
+ * file opened with the given name. Has the same effect as calling
+ * {@link #asFile(Path, OpenOption...) asFile} with the standard open
+ * options {@code CREATE} and {@code WRITE}
+ *
+ * <p> The {@link HttpResponse} using this subscriber is available after
+ * the entire response has been read.
*
- * @param consumer a Consumer of byte arrays
- * @return a BodyProcessor
+ * @param file the file to store the body in
+ * @return a body subscriber
+ * @throws SecurityException if a security manager has been installed
+ * and it denies {@link SecurityManager#checkWrite(String)
+ * write access} to the file
*/
- public static BodyProcessor<Void> asByteArrayConsumer(Consumer<Optional<byte[]>> consumer) {
- return new ResponseProcessors.ConsumerProcessor(consumer);
+ public static BodySubscriber<Path> asFile(Path file) {
+ return asFile(file, StandardOpenOption.CREATE, StandardOpenOption.WRITE);
}
/**
- * Returns a {@code BodyProcessor} which stores the response body in a
- * file opened with the given name. Has the same effect as calling
- * {@link #asFile(java.nio.file.Path, java.nio.file.OpenOption...) asFile}
- * with the standard open options {@code CREATE} and {@code WRITE}
- * <p>
- * The {@link HttpResponse} using this processor is available after the
- * entire response has been read.
+ * Returns a {@code BodySubscriber} which provides the incoming body
+ * data to the provided Consumer of {@code Optional<byte[]>}. Each
+ * call to {@link Consumer#accept(java.lang.Object) Consumer.accept()}
+ * will contain a non empty {@code Optional}, except for the final
+ * invocation after all body data has been read, when the {@code
+ * Optional} will be empty.
*
- * @param file the file to store the body in
- * @return a body processor
+ * <p> The {@link HttpResponse} using this subscriber is available after
+ * the entire response has been read.
+ *
+ * @param consumer a Consumer of byte arrays
+ * @return a BodySubscriber
*/
- public static BodyProcessor<Path> asFile(Path file) {
- return new ResponseProcessors.PathProcessor(
- file,
- StandardOpenOption.CREATE, StandardOpenOption.WRITE);
+ public static BodySubscriber<Void> asByteArrayConsumer(Consumer<Optional<byte[]>> consumer) {
+ return new ResponseSubscribers.ConsumerSubscriber(consumer);
}
/**
- * Returns a response processor which discards the response body. The
+ * Returns a {@code BodySubscriber} which streams the response body as
+ * an {@link InputStream}.
+ *
+ * <p> The {@link HttpResponse} using this subscriber is available
+ * immediately after the response headers have been read, without
+ * requiring to wait for the entire body to be processed. The response
+ * body can then be read directly from the {@link InputStream}.
+ *
+ * @return a body subscriber that streams the response body as an
+ * {@link InputStream}.
+ */
+ public static BodySubscriber<InputStream> asInputStream() {
+ return new ResponseSubscribers.HttpResponseInputStream();
+ }
+
+ /**
+ * Returns a response subscriber which discards the response body. The
* supplied value is the value that will be returned from
* {@link HttpResponse#body()}.
*
* @param <U> The type of the response body
- * @param value the value to return from HttpResponse.body()
- * @return a {@code BodyProcessor}
+ * @param value the value to return from HttpResponse.body(), may be null
+ * @return a {@code BodySubscriber}
*/
- public static <U> BodyProcessor<U> discard(U value) {
- return new ResponseProcessors.NullProcessor<>(Optional.ofNullable(value));
+ public static <U> BodySubscriber<U> discard(U value) {
+ return new ResponseSubscribers.NullSubscriber<>(Optional.ofNullable(value));
}
+
+ /**
+ * Returns a {@code BodySubscriber} which buffers data before delivering
+ * it to the given downstream subscriber. The subscriber guarantees to
+ * deliver {@code buffersize} bytes of data to each invocation of the
+ * downstream's {@linkplain #onNext(Object) onNext} method, except for
+ * the final invocation, just before {@linkplain #onComplete() onComplete}
+ * is invoked. The final invocation of {@code onNext} may contain fewer
+ * than {@code buffersize} bytes.
+ *
+ * <p> The returned subscriber delegates its {@link #getBody()} method
+ * to the downstream subscriber.
+ *
+ * @param downstream the downstream subscriber
+ * @param bufferSize the buffer size
+ * @return a buffering body subscriber
+ * @throws IllegalArgumentException if {@code bufferSize <= 0}
+ */
+ public static <T> BodySubscriber<T> buffering(BodySubscriber<T> downstream,
+ int bufferSize) {
+ if (bufferSize <= 0)
+ throw new IllegalArgumentException("must be greater than 0");
+ return new BufferingSubscriber<T>(downstream, bufferSize);
+ }
}
/**
- * A response processor for a HTTP/2 multi response.
+ * A response subscriber for a HTTP/2 multi response.
* {@Incubating}
- * <p>
- * A multi response comprises a main response, and zero or more additional
+ *
+ * <p> A multi response comprises a main response, and zero or more additional
* responses. Each additional response is sent by the server in response to
- * requests that the server also generates. Additional responses are
+ * requests (PUSH_PROMISEs) that the server also generates. Additional responses are
* typically resources that the server expects the client will need which
* are related to the initial request.
* <p>
* Note. Instead of implementing this interface, applications should consider
* first using the mechanism (built on this interface) provided by
- * {@link MultiProcessor#asMap(java.util.function.Function, boolean)
- * MultiProcessor.asMap()} which is a slightly simplified, but
+ * {@link MultiSubscriber#asMap(java.util.function.Function, boolean)
+ * MultiSubscriber.asMap()} which is a slightly simplified, but also
* general purpose interface.
* <p>
* The server generated requests are also known as <i>push promises</i>.
@@ -556,7 +771,7 @@
* the server does not wait for any acknowledgment before sending the
* response, this must be done quickly to avoid unnecessary data transmission.
*
- * <p> {@code MultiProcessor}s are parameterized with a type {@code U} which
+ * <p> {@code MultiSubscriber}s are parameterized with a type {@code U} which
* represents some meaningful aggregate of the responses received. This
* would typically be a collection of response or response body objects.
*
@@ -565,29 +780,40 @@
*
* @since 9
*/
- public interface MultiProcessor<U,T> {
+ public interface MultiSubscriber<U,T> {
/**
- * Called for the main request and each push promise that is received.
- * The first call will always be for the main request that was sent
- * by the caller. This {@link HttpRequest} parameter
- * represents the initial request or subsequent PUSH_PROMISE. The
- * implementation must return an {@code Optional} of {@link BodyHandler} for
- * the response body. Different handlers (of the same type) can be returned
- * for different pushes within the same multi send. If no handler
- * (an empty {@code Optional}) is returned, then the push will be canceled. It is
- * an error to not return a valid {@code BodyHandler} for the initial (main) request.
+ * Called for the main request from the user. This {@link HttpRequest} parameter
+ * is the request that was supplied to {@link HttpClient#sendAsync(HttpRequest, MultiSubscriber)}. The
+ * implementation must return an {@link BodyHandler} for
+ * the response body.
*
- * @param request the main request or subsequent push promise
+ * @param request the request
*
* @return an optional body handler
*/
- Optional<BodyHandler<T>> onRequest(HttpRequest request);
+ BodyHandler<T> onRequest(HttpRequest request);
+
+ /**
+ * Called for each push promise that is received. The {@link HttpRequest} parameter
+ * represents the PUSH_PROMISE. The implementation must return an {@code Optional} of
+ * {@link BodyHandler} for the response body. Different handlers (of the same type) can be returned
+ * for different pushes within the same multi send. If no handler
+ * (an empty {@code Optional}) is returned, then the push will be canceled.
+ * If required, the {@code CompletableFuture<Void>} supplied to the {@code onFinalPushPromise} parameter
+ * of {@link #completion(CompletableFuture, CompletableFuture)} can be used to determine
+ * when the final PUSH_PROMISE is received.
+ *
+ * @param request the push promise
+ *
+ * @return an optional body handler
+ */
+ Optional<BodyHandler<T>> onPushPromise(HttpRequest pushPromise);
/**
* Called for each response received. For each request either one of
* onResponse() or onError() is guaranteed to be called, but not both.
*
- * [Note] The reason for switching to this callback interface rather
+ * <p> Note: The reason for switching to this callback interface rather
* than using CompletableFutures supplied to onRequest() is that there
* is a subtle interaction between those CFs and the CF returned from
* completion() (or when onComplete() was called formerly). The completion()
@@ -618,6 +844,8 @@
* on one of the given {@code CompletableFuture<Void}s which themselves complete
* after all individual responses associated with the multi response
* have completed, or after all push promises have been received.
+ * This method is called after {@link #onRequest(HttpRequest)} but
+ * before any other methods.
*
* @implNote Implementations might follow the pattern shown below
* <pre>
@@ -653,47 +881,47 @@
* generated push promise) is returned as a key of the map. The value
* corresponding to each key is a
* {@code CompletableFuture<HttpResponse<V>>}.
- * <p>
- * There are two ways to use these handlers, depending on the value of
- * the <i>completion</I> parameter. If completion is true, then the
+ *
+ * <p> There are two ways to use these handlers, depending on the value
+ * of the <i>completion</I> parameter. If completion is true, then the
* aggregated result will be available after all responses have
* themselves completed. If <i>completion</i> is false, then the
* aggregated result will be available immediately after the last push
* promise was received. In the former case, this implies that all the
* CompletableFutures in the map values will have completed. In the
* latter case, they may or may not have completed yet.
- * <p>
- * The simplest way to use these handlers is to set completion to
+ *
+ * <p> The simplest way to use these handlers is to set completion to
* {@code true}, and then all (results) values in the Map will be
* accessible without blocking.
* <p>
- * See {@link #asMap(java.util.function.Function, boolean)
- * }
+ * See {@link #asMap(java.util.function.Function, boolean)}
* for a code sample of using this interface.
*
+ * <p> See {@link #asMap(Function, boolean)} for a code sample of using
+ * this interface.
+ *
* @param <V> the body type used for all responses
- * @param pushHandler a function invoked for each request or push
- * promise
+ * @param reqHandler a function invoked for the user's request and each push promise
* @param completion {@code true} if the aggregate CompletableFuture
- * completes after all responses have been received, or {@code false}
- * after all push promises received.
+ * completes after all responses have been received,
+ * or {@code false} after all push promises received
*
- * @return a MultiProcessor
+ * @return a MultiSubscriber
*/
- public static <V> MultiProcessor<MultiMapResult<V>,V> asMap(
- Function<HttpRequest, Optional<HttpResponse.BodyHandler<V>>> pushHandler,
+ public static <V> MultiSubscriber<MultiMapResult<V>,V> asMap(
+ Function<HttpRequest, Optional<HttpResponse.BodyHandler<V>>> reqHandler,
boolean completion) {
-
- return new MultiProcessorImpl<V>(pushHandler, completion);
+ return new MultiSubscriberImpl<V>(reqHandler.andThen(optv -> optv.get()), reqHandler, completion);
}
/**
* Returns a general purpose handler for multi responses. This is a
- * convenience method which invokes {@link #asMap(java.util.function.Function,boolean)
+ * convenience method which invokes {@link #asMap(Function,boolean)
* asMap(Function, true)} meaning that the aggregate result
* object completes after all responses have been received.
- * <p>
- * <b>Example usage:</b>
+ *
+ * <p><b>Example usage:</b>
* <br>
* <pre>
* {@code
@@ -705,26 +933,25 @@
* HttpClient client = HttpClient.newHttpClient();
*
* Map<HttpRequest,CompletableFuture<HttpResponse<String>>> results = client
- * .sendAsync(request, MultiProcessor.asMap(
+ * .sendAsync(request, MultiSubscriber.asMap(
* (req) -> Optional.of(HttpResponse.BodyHandler.asString())))
* .join();
* }</pre>
- * <p>
- * The lambda in this example is the simplest possible implementation,
+ *
+ * <p> The lambda in this example is the simplest possible implementation,
* where neither the incoming requests are examined, nor the response
* headers, and every push that the server sends is accepted. When the
* join() call returns, all {@code HttpResponse}s and their associated
* body objects are available.
*
* @param <V> the body type used for all responses
- * @param pushHandler a function invoked for each request or push
- * promise
- * @return a MultiProcessor
+ * @param reqHandler a function invoked for each push promise and the main request
+ * @return a MultiSubscriber
*/
- public static <V> MultiProcessor<MultiMapResult<V>,V> asMap(
- Function<HttpRequest, Optional<HttpResponse.BodyHandler<V>>> pushHandler) {
+ public static <V> MultiSubscriber<MultiMapResult<V>,V> asMap(
+ Function<HttpRequest, Optional<HttpResponse.BodyHandler<V>>> reqHandler) {
- return asMap(pushHandler, true);
+ return asMap(reqHandler, true);
}
}
--- a/src/jdk.incubator.httpclient/share/classes/jdk/incubator/http/HttpResponseImpl.java Sun Nov 05 17:05:57 2017 +0000
+++ b/src/jdk.incubator.httpclient/share/classes/jdk/incubator/http/HttpResponseImpl.java Sun Nov 05 17:32:13 2017 +0000
@@ -29,6 +29,7 @@
import java.net.URI;
import java.nio.ByteBuffer;
import java.util.concurrent.CompletableFuture;
+import java.util.function.Supplier;
import javax.net.ssl.SSLParameters;
import jdk.incubator.http.internal.common.Log;
import jdk.incubator.http.internal.websocket.RawChannel;
@@ -43,11 +44,9 @@
final HttpRequest initialRequest;
final HttpRequestImpl finalRequest;
final HttpHeaders headers;
- //final HttpHeaders trailers;
final SSLParameters sslParameters;
final URI uri;
final HttpClient.Version version;
- //final AccessControlContext acc;
RawChannel rawchan;
final HttpConnection connection;
final Stream<T> stream;
@@ -55,14 +54,15 @@
public HttpResponseImpl(HttpRequest initialRequest,
Response response,
- T body, Exchange<T> exch) {
+ T body,
+ Exchange<T> exch) {
this.responseCode = response.statusCode();
this.exchange = exch;
this.initialRequest = initialRequest;
this.finalRequest = exchange.request();
this.headers = response.headers();
//this.trailers = trailers;
- this.sslParameters = exch.client().sslParameters().orElse(null);
+ this.sslParameters = exch.client().sslParameters();
this.uri = finalRequest.uri();
this.version = response.version();
this.connection = exch.exchImpl.connection();
@@ -162,8 +162,8 @@
}
// Http1Exchange may have some remaining bytes in its
// internal buffer.
- final ByteBuffer remaining =((Http1Exchange<?>)exchImpl).getBuffer();
- rawchan = new RawChannelImpl(exchange.client(), connection, remaining);
+ Supplier<ByteBuffer> initial = ((Http1Exchange<?>)exchImpl)::getBuffer;
+ rawchan = new RawChannelImpl(exchange.client(), connection, initial);
}
return rawchan;
}
@@ -173,20 +173,18 @@
throw new UnsupportedOperationException("Not supported yet.");
}
- static void logResponse(Response r) {
- if (!Log.requests()) {
- return;
- }
+ @Override
+ public String toString() {
StringBuilder sb = new StringBuilder();
- String method = r.request().method();
- URI uri = r.request().uri();
+ String method = request().method();
+ URI uri = request().uri();
String uristring = uri == null ? "" : uri.toString();
sb.append('(')
- .append(method)
- .append(" ")
- .append(uristring)
- .append(") ")
- .append(r.statusCode());
- Log.logResponse(sb.toString());
+ .append(method)
+ .append(" ")
+ .append(uristring)
+ .append(") ")
+ .append(statusCode());
+ return sb.toString();
}
}
--- a/src/jdk.incubator.httpclient/share/classes/jdk/incubator/http/ImmutableHeaders.java Sun Nov 05 17:05:57 2017 +0000
+++ b/src/jdk.incubator.httpclient/share/classes/jdk/incubator/http/ImmutableHeaders.java Sun Nov 05 17:32:13 2017 +0000
@@ -38,7 +38,7 @@
import static java.util.Collections.unmodifiableMap;
import static java.util.Objects.requireNonNull;
-final class ImmutableHeaders implements HttpHeaders {
+final class ImmutableHeaders extends HttpHeaders {
private final Map<String, List<String>> map;
@@ -72,25 +72,6 @@
}
@Override
- public Optional<String> firstValue(String name) {
- return allValues(name).stream().findFirst();
- }
-
- @Override
- public OptionalLong firstValueAsLong(String name) {
- return allValues(name).stream().mapToLong(Long::valueOf).findFirst();
- }
-
- @Override
- public List<String> allValues(String name) {
- requireNonNull(name);
- List<String> values = map.get(name);
- // Making unmodifiable list out of empty in order to make a list which
- // throws UOE unconditionally
- return values != null ? values : unmodifiableList(emptyList());
- }
-
- @Override
public Map<String, List<String>> map() {
return map;
}
--- a/src/jdk.incubator.httpclient/share/classes/jdk/incubator/http/MultiExchange.java Sun Nov 05 17:05:57 2017 +0000
+++ b/src/jdk.incubator.httpclient/share/classes/jdk/incubator/http/MultiExchange.java Sun Nov 05 17:32:13 2017 +0000
@@ -26,22 +26,23 @@
package jdk.incubator.http;
import java.io.IOException;
+import java.lang.System.Logger.Level;
import java.time.Duration;
import java.util.List;
import java.security.AccessControlContext;
-import java.security.AccessController;
import java.util.concurrent.CompletableFuture;
import java.util.concurrent.CompletionException;
import java.util.concurrent.ExecutionException;
-import java.util.function.BiFunction;
import java.util.concurrent.Executor;
+import java.util.concurrent.atomic.AtomicInteger;
import java.util.function.UnaryOperator;
-
+import jdk.incubator.http.HttpResponse.UntrustedBodyHandler;
import jdk.incubator.http.internal.common.Log;
import jdk.incubator.http.internal.common.MinimalFuture;
-import jdk.incubator.http.internal.common.Pair;
+import jdk.incubator.http.internal.common.ConnectionExpiredException;
import jdk.incubator.http.internal.common.Utils;
-import static jdk.incubator.http.internal.common.Pair.pair;
+import static jdk.incubator.http.internal.common.MinimalFuture.completedFuture;
+import static jdk.incubator.http.internal.common.MinimalFuture.failedFuture;
/**
* Encapsulates multiple Exchanges belonging to one HttpRequestImpl.
@@ -53,18 +54,24 @@
*/
class MultiExchange<U,T> {
+ static final boolean DEBUG = Utils.DEBUG; // Revisit: temporary dev flag.
+ static final System.Logger DEBUG_LOGGER =
+ Utils.getDebugLogger("MultiExchange"::toString, DEBUG);
+
private final HttpRequest userRequest; // the user request
private final HttpRequestImpl request; // a copy of the user request
final AccessControlContext acc;
final HttpClientImpl client;
final HttpResponse.BodyHandler<T> responseHandler;
- final ExecutorWrapper execWrapper;
final Executor executor;
- final HttpResponse.MultiProcessor<U,T> multiResponseHandler;
+ final HttpResponse.MultiSubscriber<U,T> multiResponseSubscriber;
+ final AtomicInteger attempts = new AtomicInteger();
HttpRequestImpl currentreq; // used for async only
Exchange<T> exchange; // the current exchange
Exchange<T> previous;
- int attempts;
+ volatile Throwable retryCause;
+ volatile boolean expiredOnce;
+
// Maximum number of times a request will be retried/redirected
// for any reason
@@ -93,24 +100,24 @@
*/
MultiExchange(HttpRequest req,
HttpClientImpl client,
- HttpResponse.BodyHandler<T> responseHandler) {
+ HttpResponse.BodyHandler<T> responseHandler,
+ AccessControlContext acc) {
this.previous = null;
this.userRequest = req;
- this.request = new HttpRequestImpl(req);
+ this.request = new HttpRequestImpl(req, acc);
this.currentreq = request;
- this.attempts = 0;
this.client = client;
this.filters = client.filterChain();
- if (System.getSecurityManager() != null) {
- this.acc = AccessController.getContext();
- } else {
- this.acc = null;
+ this.acc = acc;
+ this.executor = client.theExecutor();
+ this.responseHandler = responseHandler;
+ if (acc != null) {
+ // Restricts the file publisher with the senders ACC, if any
+ if (responseHandler instanceof UntrustedBodyHandler)
+ ((UntrustedBodyHandler)this.responseHandler).setAccessControlContext(acc);
}
- this.execWrapper = new ExecutorWrapper(client.executor(), acc);
- this.executor = execWrapper.executor();
- this.responseHandler = responseHandler;
this.exchange = new Exchange<>(request, this);
- this.multiResponseHandler = null;
+ this.multiResponseSubscriber = null;
this.pushGroup = null;
}
@@ -119,62 +126,22 @@
*/
MultiExchange(HttpRequest req,
HttpClientImpl client,
- HttpResponse.MultiProcessor<U, T> multiResponseHandler) {
+ HttpResponse.MultiSubscriber<U, T> multiResponseSubscriber,
+ AccessControlContext acc) {
this.previous = null;
this.userRequest = req;
- this.request = new HttpRequestImpl(req);
+ this.request = new HttpRequestImpl(req, acc);
this.currentreq = request;
- this.attempts = 0;
this.client = client;
this.filters = client.filterChain();
- if (System.getSecurityManager() != null) {
- this.acc = AccessController.getContext();
- } else {
- this.acc = null;
- }
- this.execWrapper = new ExecutorWrapper(client.executor(), acc);
- this.executor = execWrapper.executor();
- this.multiResponseHandler = multiResponseHandler;
- this.pushGroup = new PushGroup<>(multiResponseHandler, request);
+ this.acc = acc;
+ this.executor = client.theExecutor();
+ this.multiResponseSubscriber = multiResponseSubscriber;
+ this.pushGroup = new PushGroup<>(multiResponseSubscriber, request, acc);
this.exchange = new Exchange<>(request, this);
this.responseHandler = pushGroup.mainResponseHandler();
}
- public HttpResponseImpl<T> response() throws IOException, InterruptedException {
- HttpRequestImpl r = request;
- if (r.duration() != null) {
- timedEvent = new TimedEvent(r.duration());
- client.registerTimer(timedEvent);
- }
- while (attempts < max_attempts) {
- try {
- attempts++;
- Exchange<T> currExchange = getExchange();
- requestFilters(r);
- Response response = currExchange.response();
- HttpRequestImpl newreq = responseFilters(response);
- if (newreq == null) {
- if (attempts > 1) {
- Log.logError("Succeeded on attempt: " + attempts);
- }
- T body = currExchange.readBody(responseHandler);
- cancelTimer();
- return new HttpResponseImpl<>(userRequest, response, body, currExchange);
- }
- //response.body(HttpResponse.ignoreBody());
- setExchange(new Exchange<>(newreq, this, acc));
- r = newreq;
- } catch (IOException e) {
- if (cancelled) {
- throw new HttpTimeoutException("Request timed out");
- }
- throw e;
- }
- }
- cancelTimer();
- throw new IOException("Retry limit exceeded");
- }
-
CompletableFuture<Void> multiCompletionCF() {
return pushGroup.groupResult();
}
@@ -196,6 +163,9 @@
}
private synchronized void setExchange(Exchange<T> exchange) {
+ if (this.exchange != null && exchange != this.exchange) {
+ this.exchange.released();
+ }
this.exchange = exchange;
}
@@ -239,104 +209,103 @@
getExchange().cancel(cause);
}
- public CompletableFuture<HttpResponseImpl<T>> responseAsync() {
+ public CompletableFuture<HttpResponse<T>> responseAsync() {
CompletableFuture<Void> start = new MinimalFuture<>();
- CompletableFuture<HttpResponseImpl<T>> cf = responseAsync0(start);
+ CompletableFuture<HttpResponse<T>> cf = responseAsync0(start);
start.completeAsync( () -> null, executor); // trigger execution
return cf;
}
- private CompletableFuture<HttpResponseImpl<T>> responseAsync0(CompletableFuture<Void> start) {
+ private CompletableFuture<HttpResponse<T>>
+ responseAsync0(CompletableFuture<Void> start) {
return start.thenCompose( v -> responseAsyncImpl())
- .thenCompose((Response r) -> {
- Exchange<T> exch = getExchange();
- return exch.readBodyAsync(responseHandler)
- .thenApply((T body) -> new HttpResponseImpl<>(userRequest, r, body, exch));
- });
+ .thenCompose((Response r) -> {
+ Exchange<T> exch = getExchange();
+ return exch.readBodyAsync(responseHandler)
+ .thenApply((T body) ->
+ new HttpResponseImpl<>(userRequest,
+ r,
+ body,
+ exch));
+ });
}
CompletableFuture<U> multiResponseAsync() {
CompletableFuture<Void> start = new MinimalFuture<>();
- CompletableFuture<HttpResponseImpl<T>> cf = responseAsync0(start);
+ CompletableFuture<HttpResponse<T>> cf = responseAsync0(start);
CompletableFuture<HttpResponse<T>> mainResponse =
- cf.thenApply((HttpResponseImpl<T> b) -> {
- multiResponseHandler.onResponse(b);
- return (HttpResponse<T>)b;
- });
-
+ cf.thenApply(b -> {
+ multiResponseSubscriber.onResponse(b);
+ pushGroup.noMorePushes(true);
+ return b; });
pushGroup.setMainResponse(mainResponse);
- // set up house-keeping related to multi-response
- mainResponse.thenAccept((r) -> {
- // All push promises received by now.
- pushGroup.noMorePushes(true);
- });
- CompletableFuture<U> res = multiResponseHandler.completion(pushGroup.groupResult(), pushGroup.pushesCF());
+ CompletableFuture<U> res = multiResponseSubscriber.completion(pushGroup.groupResult(),
+ pushGroup.pushesCF());
start.completeAsync( () -> null, executor); // trigger execution
return res;
}
private CompletableFuture<Response> responseAsyncImpl() {
CompletableFuture<Response> cf;
- if (++attempts > max_attempts) {
- cf = MinimalFuture.failedFuture(new IOException("Too many retries"));
+ if (attempts.incrementAndGet() > max_attempts) {
+ cf = failedFuture(new IOException("Too many retries", retryCause));
} else {
- if (currentreq.duration() != null) {
- timedEvent = new TimedEvent(currentreq.duration());
+ if (currentreq.timeout().isPresent()) {
+ timedEvent = new TimedEvent(currentreq.timeout().get());
client.registerTimer(timedEvent);
}
try {
- // 1. Apply request filters
+ // 1. apply request filters
requestFilters(currentreq);
} catch (IOException e) {
- return MinimalFuture.failedFuture(e);
+ return failedFuture(e);
}
Exchange<T> exch = getExchange();
// 2. get response
cf = exch.responseAsync()
- .thenCompose((Response response) -> {
- HttpRequestImpl newrequest = null;
- try {
- // 3. Apply response filters
- newrequest = responseFilters(response);
- } catch (IOException e) {
- return MinimalFuture.failedFuture(e);
- }
- // 4. Check filter result and repeat or continue
- if (newrequest == null) {
- if (attempts > 1) {
- Log.logError("Succeeded on attempt: " + attempts);
+ .thenCompose((Response response) -> {
+ HttpRequestImpl newrequest;
+ try {
+ // 3. apply response filters
+ newrequest = responseFilters(response);
+ } catch (IOException e) {
+ return failedFuture(e);
}
- return MinimalFuture.completedFuture(response);
- } else {
- currentreq = newrequest;
- setExchange(new Exchange<>(currentreq, this, acc));
- //reads body off previous, and then waits for next response
- return responseAsyncImpl();
- }
- })
- // 5. Handle errors and cancel any timer set
- .handle((response, ex) -> {
- cancelTimer();
- if (ex == null) {
- assert response != null;
- return MinimalFuture.completedFuture(response);
- }
- // all exceptions thrown are handled here
- CompletableFuture<Response> error = getExceptionalCF(ex);
- if (error == null) {
- return responseAsyncImpl();
- } else {
- return error;
- }
- })
- .thenCompose(UnaryOperator.identity());
+ // 4. check filter result and repeat or continue
+ if (newrequest == null) {
+ if (attempts.get() > 1) {
+ Log.logError("Succeeded on attempt: " + attempts);
+ }
+ return completedFuture(response);
+ } else {
+ currentreq = newrequest;
+ expiredOnce = false;
+ setExchange(new Exchange<>(currentreq, this, acc));
+ //reads body off previous, and then waits for next response
+ return responseAsyncImpl();
+ } })
+ .handle((response, ex) -> {
+ // 5. handle errors and cancel any timer set
+ cancelTimer();
+ if (ex == null) {
+ assert response != null;
+ return completedFuture(response);
+ }
+ // all exceptions thrown are handled here
+ CompletableFuture<Response> errorCF = getExceptionalCF(ex);
+ if (errorCF == null) {
+ return responseAsyncImpl();
+ } else {
+ return errorCF;
+ } })
+ .thenCompose(UnaryOperator.identity());
}
return cf;
}
/**
- * Take a Throwable and return a suitable CompletableFuture that is
- * completed exceptionally.
+ * Takes a Throwable and returns a suitable CompletableFuture that is
+ * completed exceptionally, or null.
*/
private CompletableFuture<Response> getExceptionalCF(Throwable t) {
if ((t instanceof CompletionException) || (t instanceof ExecutionException)) {
@@ -346,8 +315,24 @@
}
if (cancelled && t instanceof IOException) {
t = new HttpTimeoutException("request timed out");
+ } else if (t instanceof ConnectionExpiredException) {
+ // allow the retry mechanism to do its work
+ // ####: method (GET,HEAD, not POST?), no bytes written or read ( differentiate? )
+ if (t.getCause() != null) retryCause = t.getCause();
+ if (!expiredOnce) {
+ DEBUG_LOGGER.log(Level.DEBUG,
+ "MultiExchange: ConnectionExpiredException (async): retrying...",
+ t);
+ expiredOnce = true;
+ return null;
+ } else {
+ DEBUG_LOGGER.log(Level.DEBUG,
+ "MultiExchange: ConnectionExpiredException (async): already retried once.",
+ t);
+ if (t.getCause() != null) t = t.getCause();
+ }
}
- return MinimalFuture.failedFuture(t);
+ return failedFuture(t);
}
class TimedEvent extends TimeoutEvent {
@@ -356,6 +341,9 @@
}
@Override
public void handle() {
+ DEBUG_LOGGER.log(Level.DEBUG,
+ "Cancelling MultiExchange due to timeout for request %s",
+ request);
cancel(new HttpTimeoutException("request timed out"));
}
}
--- a/src/jdk.incubator.httpclient/share/classes/jdk/incubator/http/MultiMapResult.java Sun Nov 05 17:05:57 2017 +0000
+++ b/src/jdk.incubator.httpclient/share/classes/jdk/incubator/http/MultiMapResult.java Sun Nov 05 17:32:13 2017 +0000
@@ -44,8 +44,8 @@
* <p>
* {@link CompletableFuture}<{@code MultiMapResult<V>}>
* {@link HttpClient#sendAsync(HttpRequest,
- * HttpResponse.MultiProcessor) HttpClient.sendAsync(}{@link
- * HttpResponse.MultiProcessor#asMap(java.util.function.Function)
+ * HttpResponse.MultiSubscriber) HttpClient.sendAsync(}{@link
+ * HttpResponse.MultiSubscriber#asMap(java.util.function.Function)
* MultiProcessor.asMap(Function)})
*
* @param <V> the response body type for all responses
@@ -117,4 +117,3 @@
return map.entrySet();
}
}
-
--- a/src/jdk.incubator.httpclient/share/classes/jdk/incubator/http/PlainHttpConnection.java Sun Nov 05 17:05:57 2017 +0000
+++ b/src/jdk.incubator.httpclient/share/classes/jdk/incubator/http/PlainHttpConnection.java Sun Nov 05 17:32:13 2017 +0000
@@ -26,73 +26,45 @@
package jdk.incubator.http;
import java.io.IOException;
+import java.lang.System.Logger.Level;
import java.net.InetSocketAddress;
import java.net.StandardSocketOptions;
import java.nio.ByteBuffer;
import java.nio.channels.SelectableChannel;
import java.nio.channels.SelectionKey;
import java.nio.channels.SocketChannel;
+import java.security.AccessController;
+import java.security.PrivilegedActionException;
+import java.security.PrivilegedExceptionAction;
import java.util.concurrent.CompletableFuture;
-import java.util.function.Consumer;
-import java.util.function.Supplier;
-
-import jdk.incubator.http.internal.common.AsyncWriteQueue;
import jdk.incubator.http.internal.common.ByteBufferReference;
+import jdk.incubator.http.internal.common.FlowTube;
import jdk.incubator.http.internal.common.Log;
import jdk.incubator.http.internal.common.MinimalFuture;
import jdk.incubator.http.internal.common.Utils;
/**
- * Plain raw TCP connection direct to destination. 2 modes
- * 1) Blocking used by http/1. In this case the connect is actually non
- * blocking but the request is sent blocking. The first byte of a response
- * is received non-blocking and the remainder of the response is received
- * blocking
- * 2) Non-blocking. In this case (for http/2) the connection is actually opened
- * blocking but all reads and writes are done non-blocking under the
- * control of a Http2Connection object.
+ * Plain raw TCP connection direct to destination.
+ * The connection operates in asynchronous non-blocking mode.
+ * All reads and writes are done non-blocking.
*/
-class PlainHttpConnection extends HttpConnection implements AsyncConnection {
+class PlainHttpConnection extends HttpConnection {
+ private final Object reading = new Object();
protected final SocketChannel chan;
+ private final FlowTube tube;
+ // The PlainHttpPublisher is a temporary hack needed because we still
+ // use writeAsync/flushAsync
+ private final PlainHttpPublisher writePublisher = new PlainHttpPublisher(reading);
private volatile boolean connected;
private boolean closed;
// should be volatile to provide proper synchronization(visibility) action
- private volatile Consumer<ByteBufferReference> asyncReceiver;
- private volatile Consumer<Throwable> errorReceiver;
- private volatile Supplier<ByteBufferReference> readBufferSupplier;
- private boolean asyncReading;
- private final AsyncWriteQueue asyncOutputQ = new AsyncWriteQueue(this::asyncOutput);
-
- private final Object reading = new Object();
-
- @Override
- public void startReading() {
- try {
- synchronized(reading) {
- asyncReading = true;
- }
- client.registerEvent(new ReadEvent());
- } catch (IOException e) {
- shutdown();
- }
- }
-
- @Override
- public void stopAsyncReading() {
- synchronized(reading) {
- asyncReading = false;
- }
- client.cancelRegistration(chan);
- }
-
- class ConnectEvent extends AsyncEvent {
- CompletableFuture<Void> cf;
+ final class ConnectEvent extends AsyncEvent {
+ private final CompletableFuture<Void> cf;
ConnectEvent(CompletableFuture<Void> cf) {
- super(AsyncEvent.BLOCKING);
this.cf = cf;
}
@@ -108,39 +80,53 @@
@Override
public void handle() {
+ assert !connected : "Already connected";
+ assert !chan.isBlocking() : "Unexpected blocking channel";
try {
- chan.finishConnect();
+ debug.log(Level.DEBUG, "ConnectEvent: finishing connect");
+ boolean finished = chan.finishConnect();
+ assert finished : "Expected channel to be connected";
+ debug.log(Level.DEBUG,
+ "ConnectEvent: connect finished: %s", finished);
+ connected = true;
+ // complete async since the event runs on the SelectorManager thread
+ cf.completeAsync(() -> null, client().theExecutor());
} catch (IOException e) {
- cf.completeExceptionally(e);
- return;
+ client().theExecutor().execute( () -> cf.completeExceptionally(e));
}
- connected = true;
- cf.complete(null);
}
@Override
- public void abort() {
+ public void abort(IOException ioe) {
close();
+ client().theExecutor().execute( () -> cf.completeExceptionally(ioe));
}
}
@Override
public CompletableFuture<Void> connectAsync() {
- CompletableFuture<Void> plainFuture = new MinimalFuture<>();
+ assert !connected : "Already connected";
+ assert !chan.isBlocking() : "Unexpected blocking channel";
+ CompletableFuture<Void> cf = new MinimalFuture<>();
try {
- chan.configureBlocking(false);
- chan.connect(address);
- client.registerEvent(new ConnectEvent(plainFuture));
- } catch (IOException e) {
- plainFuture.completeExceptionally(e);
+ boolean finished = false;
+ PrivilegedExceptionAction<Boolean> pa = () -> chan.connect(address);
+ try {
+ finished = AccessController.doPrivileged(pa);
+ } catch (PrivilegedActionException e) {
+ cf.completeExceptionally(e.getCause());
+ }
+ if (finished) {
+ debug.log(Level.DEBUG, "connect finished without blocking");
+ cf.complete(null);
+ } else {
+ debug.log(Level.DEBUG, "registering connect event");
+ client().registerEvent(new ConnectEvent(cf));
+ }
+ } catch (Throwable throwable) {
+ cf.completeExceptionally(throwable);
}
- return plainFuture;
- }
-
- @Override
- public void connect() throws IOException {
- chan.connect(address);
- connected = true;
+ return cf;
}
@Override
@@ -148,106 +134,44 @@
return chan;
}
+ @Override
+ final FlowTube getConnectionFlow() {
+ return tube;
+ }
+
PlainHttpConnection(InetSocketAddress addr, HttpClientImpl client) {
super(addr, client);
try {
this.chan = SocketChannel.open();
+ chan.configureBlocking(false);
int bufsize = client.getReceiveBufferSize();
chan.setOption(StandardSocketOptions.SO_RCVBUF, bufsize);
chan.setOption(StandardSocketOptions.TCP_NODELAY, true);
+ // wrap the connected channel in a Tube for async reading and writing
+ tube = new SocketTube(client(), chan, Utils::getBuffer);
} catch (IOException e) {
throw new InternalError(e);
}
}
@Override
- long write(ByteBuffer[] buffers, int start, int number) throws IOException {
- if (getMode() != Mode.ASYNC) {
- return chan.write(buffers, start, number);
- }
- // async
- buffers = Utils.reduce(buffers, start, number);
- long n = Utils.remaining(buffers);
- asyncOutputQ.put(ByteBufferReference.toReferences(buffers));
- flushAsync();
- return n;
- }
-
- @Override
- long write(ByteBuffer buffer) throws IOException {
- if (getMode() != Mode.ASYNC) {
- return chan.write(buffer);
- }
- // async
- long n = buffer.remaining();
- asyncOutputQ.put(ByteBufferReference.toReferences(buffer));
- flushAsync();
- return n;
- }
-
- // handle registered WriteEvent; invoked from SelectorManager thread
- void flushRegistered() {
- if (getMode() == Mode.ASYNC) {
- try {
- asyncOutputQ.flushDelayed();
- } catch (IOException e) {
- // Only IOException caused by closed Queue is expected here
- shutdown();
- }
- }
- }
+ HttpPublisher publisher() { return writePublisher; }
@Override
public void writeAsync(ByteBufferReference[] buffers) throws IOException {
- if (getMode() != Mode.ASYNC) {
- chan.write(ByteBufferReference.toBuffers(buffers));
- ByteBufferReference.clear(buffers);
- } else {
- asyncOutputQ.put(buffers);
- }
+ writePublisher.writeAsync(buffers);
}
@Override
public void writeAsyncUnordered(ByteBufferReference[] buffers) throws IOException {
- if (getMode() != Mode.ASYNC) {
- chan.write(ByteBufferReference.toBuffers(buffers));
- ByteBufferReference.clear(buffers);
- } else {
- // Unordered frames are sent before existing frames.
- asyncOutputQ.putFirst(buffers);
- }
+ writePublisher.writeAsyncUnordered(buffers);
}
@Override
public void flushAsync() throws IOException {
- if (getMode() == Mode.ASYNC) {
- asyncOutputQ.flush();
- }
- }
-
- @Override
- public void enableCallback() {
- // not used
- assert false;
+ writePublisher.flushAsync();
}
- boolean asyncOutput(ByteBufferReference[] refs, AsyncWriteQueue delayCallback) {
- try {
- ByteBuffer[] bufs = ByteBufferReference.toBuffers(refs);
- while (Utils.remaining(bufs) > 0) {
- long n = chan.write(bufs);
- if (n == 0) {
- delayCallback.setDelayed(refs);
- client.registerEvent(new WriteEvent());
- return false;
- }
- }
- ByteBufferReference.clear(refs);
- } catch (IOException e) {
- shutdown();
- }
- return true;
- }
@Override
public String toString() {
@@ -255,7 +179,7 @@
}
/**
- * Close this connection
+ * Closes this connection
*/
@Override
public synchronized void close() {
@@ -264,80 +188,23 @@
}
closed = true;
try {
- Log.logError("Closing: " + toString());
+ Log.logTrace("Closing: " + toString());
chan.close();
} catch (IOException e) {}
}
@Override
void shutdownInput() throws IOException {
+ debug.log(Level.DEBUG, "Shutting down input");
chan.shutdownInput();
}
@Override
void shutdownOutput() throws IOException {
+ debug.log(Level.DEBUG, "Shutting down output");
chan.shutdownOutput();
}
- void shutdown() {
- close();
- errorReceiver.accept(new IOException("Connection aborted"));
- }
-
- void asyncRead() {
- synchronized (reading) {
- try {
- while (asyncReading) {
- ByteBufferReference buf = readBufferSupplier.get();
- int n = chan.read(buf.get());
- if (n == -1) {
- throw new IOException();
- }
- if (n == 0) {
- buf.clear();
- return;
- }
- buf.get().flip();
- asyncReceiver.accept(buf);
- }
- } catch (IOException e) {
- shutdown();
- }
- }
- }
-
- @Override
- protected ByteBuffer readImpl() throws IOException {
- ByteBuffer dst = ByteBuffer.allocate(8192);
- int n = readImpl(dst);
- if (n > 0) {
- return dst;
- } else if (n == 0) {
- return Utils.EMPTY_BYTEBUFFER;
- } else {
- return null;
- }
- }
-
- private int readImpl(ByteBuffer buf) throws IOException {
- int mark = buf.position();
- int n;
- // FIXME: this hack works in conjunction with the corresponding change
- // in jdk.incubator.http.RawChannel.registerEvent
- //if ((n = buffer.remaining()) != 0) {
- //buf.put(buffer);
- //} else {
- n = chan.read(buf);
- //}
- if (n == -1) {
- return -1;
- }
- Utils.flipToMark(buf, mark);
- // String s = "Receive (" + n + " bytes) ";
- //debugPrint(s, buf);
- return n;
- }
-
@Override
ConnectionPool.CacheKey cacheKey() {
return new ConnectionPool.CacheKey(address, null);
@@ -348,98 +215,6 @@
return connected;
}
- // used for all output in HTTP/2
- class WriteEvent extends AsyncEvent {
- WriteEvent() {
- super(0);
- }
-
- @Override
- public SelectableChannel channel() {
- return chan;
- }
-
- @Override
- public int interestOps() {
- return SelectionKey.OP_WRITE;
- }
-
- @Override
- public void handle() {
- flushRegistered();
- }
-
- @Override
- public void abort() {
- shutdown();
- }
- }
-
- // used for all input in HTTP/2
- class ReadEvent extends AsyncEvent {
- ReadEvent() {
- super(AsyncEvent.REPEATING); // && !BLOCKING
- }
-
- @Override
- public SelectableChannel channel() {
- return chan;
- }
-
- @Override
- public int interestOps() {
- return SelectionKey.OP_READ;
- }
-
- @Override
- public void handle() {
- asyncRead();
- }
-
- @Override
- public void abort() {
- shutdown();
- }
-
- @Override
- public String toString() {
- return super.toString() + "/" + chan;
- }
- }
-
- // used in blocking channels only
- class ReceiveResponseEvent extends AsyncEvent {
- CompletableFuture<Void> cf;
-
- ReceiveResponseEvent(CompletableFuture<Void> cf) {
- super(AsyncEvent.BLOCKING);
- this.cf = cf;
- }
- @Override
- public SelectableChannel channel() {
- return chan;
- }
-
- @Override
- public void handle() {
- cf.complete(null);
- }
-
- @Override
- public int interestOps() {
- return SelectionKey.OP_READ;
- }
-
- @Override
- public void abort() {
- close();
- }
-
- @Override
- public String toString() {
- return super.toString() + "/" + chan;
- }
- }
@Override
boolean isSecure() {
@@ -451,24 +226,91 @@
return false;
}
- @Override
- public void setAsyncCallbacks(Consumer<ByteBufferReference> asyncReceiver,
- Consumer<Throwable> errorReceiver,
- Supplier<ByteBufferReference> readBufferSupplier) {
- this.asyncReceiver = asyncReceiver;
- this.errorReceiver = errorReceiver;
- this.readBufferSupplier = readBufferSupplier;
+ // Support for WebSocket/RawChannelImpl which unfortunately
+ // still depends on synchronous read/writes.
+ // It should be removed when RawChannelImpl moves to using asynchronous APIs.
+ private static final class PlainDetachedChannel
+ extends DetachedConnectionChannel {
+ final PlainHttpConnection plainConnection;
+ boolean closed;
+ PlainDetachedChannel(PlainHttpConnection conn) {
+ // We're handing the connection channel over to a web socket.
+ // We need the selector manager's thread to stay alive until
+ // the WebSocket is closed.
+ conn.client().webSocketOpen();
+ this.plainConnection = conn;
+ }
+
+ @Override
+ SocketChannel channel() {
+ return plainConnection.channel();
+ }
+
+ @Override
+ ByteBuffer read() throws IOException {
+ ByteBuffer dst = ByteBuffer.allocate(8192);
+ int n = readImpl(dst);
+ if (n > 0) {
+ return dst;
+ } else if (n == 0) {
+ return Utils.EMPTY_BYTEBUFFER;
+ } else {
+ return null;
+ }
+ }
+
+ @Override
+ public void close() {
+ HttpClientImpl client = plainConnection.client();
+ try {
+ plainConnection.close();
+ } finally {
+ // notify the HttpClientImpl that the websocket is no
+ // no longer operating.
+ synchronized(this) {
+ if (closed == true) return;
+ closed = true;
+ }
+ client.webSocketClose();
+ }
+ }
+
+ @Override
+ public long write(ByteBuffer[] buffers, int start, int number)
+ throws IOException
+ {
+ return channel().write(buffers, start, number);
+ }
+
+ @Override
+ public void shutdownInput() throws IOException {
+ plainConnection.shutdownInput();
+ }
+
+ @Override
+ public void shutdownOutput() throws IOException {
+ plainConnection.shutdownOutput();
+ }
+
+ private int readImpl(ByteBuffer buf) throws IOException {
+ int mark = buf.position();
+ int n;
+ n = channel().read(buf);
+ if (n == -1) {
+ return -1;
+ }
+ Utils.flipToMark(buf, mark);
+ return n;
+ }
}
+ // Support for WebSocket/RawChannelImpl which unfortunately
+ // still depends on synchronous read/writes.
+ // It should be removed when RawChannelImpl moves to using asynchronous APIs.
@Override
- CompletableFuture<Void> whenReceivingResponse() {
- CompletableFuture<Void> cf = new MinimalFuture<>();
- try {
- ReceiveResponseEvent evt = new ReceiveResponseEvent(cf);
- client.registerEvent(evt);
- } catch (IOException e) {
- cf.completeExceptionally(e);
- }
- return cf;
+ DetachedConnectionChannel detachChannel() {
+ client().cancelRegistration(channel());
+ return new PlainDetachedChannel(this);
}
+
}
--- a/src/jdk.incubator.httpclient/share/classes/jdk/incubator/http/PlainTunnelingConnection.java Sun Nov 05 17:05:57 2017 +0000
+++ b/src/jdk.incubator.httpclient/share/classes/jdk/incubator/http/PlainTunnelingConnection.java Sun Nov 05 17:32:13 2017 +0000
@@ -25,71 +25,28 @@
package jdk.incubator.http;
-import jdk.incubator.http.internal.common.ByteBufferReference;
-import jdk.incubator.http.internal.common.MinimalFuture;
-import jdk.incubator.http.HttpResponse.BodyHandler;
-
import java.io.IOException;
+import java.lang.System.Logger.Level;
import java.net.InetSocketAddress;
import java.nio.ByteBuffer;
import java.nio.channels.SocketChannel;
import java.util.concurrent.CompletableFuture;
-import java.util.function.Consumer;
-import java.util.function.Supplier;
+import jdk.incubator.http.internal.common.ByteBufferReference;
+import jdk.incubator.http.internal.common.FlowTube;
+import jdk.incubator.http.internal.common.MinimalFuture;
+import static jdk.incubator.http.HttpResponse.BodyHandler.discard;
/**
* A plain text socket tunnel through a proxy. Uses "CONNECT" but does not
* encrypt. Used by WebSocket, as well as HTTP over SSL + Proxy.
* Wrapped in SSLTunnelConnection or AsyncSSLTunnelConnection for encryption.
*/
-class PlainTunnelingConnection extends HttpConnection implements AsyncConnection {
+final class PlainTunnelingConnection extends HttpConnection {
final PlainHttpConnection delegate;
protected final InetSocketAddress proxyAddr;
private volatile boolean connected;
- @Override
- public CompletableFuture<Void> connectAsync() {
- return delegate.connectAsync()
- .thenCompose((Void v) -> {
- HttpRequestImpl req = new HttpRequestImpl("CONNECT", client, address);
- MultiExchange<Void,Void> mconnectExchange = new MultiExchange<>(req, client, this::ignore);
- return mconnectExchange.responseAsync()
- .thenCompose((HttpResponseImpl<Void> resp) -> {
- CompletableFuture<Void> cf = new MinimalFuture<>();
- if (resp.statusCode() != 200) {
- cf.completeExceptionally(new IOException("Tunnel failed"));
- } else {
- connected = true;
- cf.complete(null);
- }
- return cf;
- });
- });
- }
-
- private HttpResponse.BodyProcessor<Void> ignore(int status, HttpHeaders hdrs) {
- return HttpResponse.BodyProcessor.discard((Void)null);
- }
-
- @Override
- public void connect() throws IOException, InterruptedException {
- delegate.connect();
- HttpRequestImpl req = new HttpRequestImpl("CONNECT", client, address);
- MultiExchange<Void,Void> mul = new MultiExchange<>(req, client, BodyHandler.<Void>discard(null));
- Exchange<Void> connectExchange = new Exchange<>(req, mul);
- Response r = connectExchange.responseImpl(delegate);
- if (r.statusCode() != 200) {
- throw new IOException("Tunnel failed");
- }
- connected = true;
- }
-
- @Override
- boolean connected() {
- return connected;
- }
-
protected PlainTunnelingConnection(InetSocketAddress addr,
InetSocketAddress proxy,
HttpClientImpl client) {
@@ -99,26 +56,62 @@
}
@Override
+ public CompletableFuture<Void> connectAsync() {
+ debug.log(Level.DEBUG, "Connecting plain connection");
+ return delegate.connectAsync()
+ .thenCompose((Void v) -> {
+ debug.log(Level.DEBUG, "sending HTTP/1.1 CONNECT");
+ HttpClientImpl client = client();
+ assert client != null;
+ HttpRequestImpl req = new HttpRequestImpl("CONNECT", address);
+ MultiExchange<Void,Void> mulEx = new MultiExchange<>(req, client, discard(null), null);
+ Exchange<Void> connectExchange = new Exchange<>(req, mulEx);
+
+ return connectExchange
+ .responseAsyncImpl(delegate)
+ .thenCompose((Response resp) -> {
+ CompletableFuture<Void> cf = new MinimalFuture<>();
+ debug.log(Level.DEBUG, "got response: %d", resp.statusCode());
+ if (resp.statusCode() != 200) {
+ cf.completeExceptionally(new IOException(
+ "Tunnel failed, got: "+ resp.statusCode()));
+ } else {
+ // get the initial/remaining bytes
+ ByteBuffer b = ((Http1Exchange<?>)connectExchange.exchImpl).getBuffer();
+ int remaining = b.remaining();
+ assert remaining == 0: "Unexpected remaining: " + remaining;
+ connected = true;
+ cf.complete(null);
+ }
+ return cf;
+ });
+ });
+ }
+
+ @Override
+ HttpPublisher publisher() { return delegate.publisher(); }
+
+ @Override
+ boolean connected() {
+ return connected;
+ }
+
+ @Override
SocketChannel channel() {
return delegate.channel();
}
@Override
+ FlowTube getConnectionFlow() {
+ return delegate.getConnectionFlow();
+ }
+
+ @Override
ConnectionPool.CacheKey cacheKey() {
return new ConnectionPool.CacheKey(null, proxyAddr);
}
@Override
- long write(ByteBuffer[] buffers, int start, int number) throws IOException {
- return delegate.write(buffers, start, number);
- }
-
- @Override
- long write(ByteBuffer buffer) throws IOException {
- return delegate.write(buffer);
- }
-
- @Override
public void writeAsync(ByteBufferReference[] buffers) throws IOException {
delegate.writeAsync(buffers);
}
@@ -150,16 +143,6 @@
}
@Override
- CompletableFuture<Void> whenReceivingResponse() {
- return delegate.whenReceivingResponse();
- }
-
- @Override
- protected ByteBuffer readImpl() throws IOException {
- return delegate.readImpl();
- }
-
- @Override
boolean isSecure() {
return false;
}
@@ -169,31 +152,11 @@
return true;
}
- @Override
- public void setAsyncCallbacks(Consumer<ByteBufferReference> asyncReceiver,
- Consumer<Throwable> errorReceiver,
- Supplier<ByteBufferReference> readBufferSupplier) {
- delegate.setAsyncCallbacks(asyncReceiver, errorReceiver, readBufferSupplier);
- }
-
- @Override
- public void startReading() {
- delegate.startReading();
- }
-
+ // Support for WebSocket/RawChannelImpl which unfortunately
+ // still depends on synchronous read/writes.
+ // It should be removed when RawChannelImpl moves to using asynchronous APIs.
@Override
- public void stopAsyncReading() {
- delegate.stopAsyncReading();
- }
-
- @Override
- public void enableCallback() {
- delegate.enableCallback();
- }
-
- @Override
- synchronized void configureMode(Mode mode) throws IOException {
- super.configureMode(mode);
- delegate.configureMode(mode);
+ DetachedConnectionChannel detachChannel() {
+ return delegate.detachChannel();
}
}
--- /dev/null Thu Jan 01 00:00:00 1970 +0000
+++ b/src/jdk.incubator.httpclient/share/classes/jdk/incubator/http/PrivilegedExecutor.java Sun Nov 05 17:32:13 2017 +0000
@@ -0,0 +1,69 @@
+/*
+ * Copyright (c) 2015, 2017, Oracle and/or its affiliates. All rights reserved.
+ * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER.
+ *
+ * This code is free software; you can redistribute it and/or modify it
+ * under the terms of the GNU General Public License version 2 only, as
+ * published by the Free Software Foundation. Oracle designates this
+ * particular file as subject to the "Classpath" exception as provided
+ * by Oracle in the LICENSE file that accompanied this code.
+ *
+ * This code is distributed in the hope that it will be useful, but WITHOUT
+ * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or
+ * FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License
+ * version 2 for more details (a copy is included in the LICENSE file that
+ * accompanied this code).
+ *
+ * You should have received a copy of the GNU General Public License version
+ * 2 along with this work; if not, write to the Free Software Foundation,
+ * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA.
+ *
+ * Please contact Oracle, 500 Oracle Parkway, Redwood Shores, CA 94065 USA
+ * or visit www.oracle.com if you need additional information or have any
+ * questions.
+ */
+
+package jdk.incubator.http;
+
+import java.security.AccessControlContext;
+import java.security.AccessController;
+import java.security.PrivilegedAction;
+import java.util.Objects;
+import java.util.concurrent.Executor;
+
+/**
+ * Executes tasks within a given access control context, and by a given executor.
+ */
+class PrivilegedExecutor implements Executor {
+
+ /** The underlying executor. May be provided by the user. */
+ final Executor executor;
+ /** The ACC to execute the tasks within. */
+ final AccessControlContext acc;
+
+ public PrivilegedExecutor(Executor executor, AccessControlContext acc) {
+ Objects.requireNonNull(executor);
+ Objects.requireNonNull(acc);
+ this.executor = executor;
+ this.acc = acc;
+ }
+
+ private static class PrivilegedRunnable implements Runnable {
+ private final Runnable r;
+ private final AccessControlContext acc;
+ PrivilegedRunnable(Runnable r, AccessControlContext acc) {
+ this.r = r;
+ this.acc = acc;
+ }
+ @Override
+ public void run() {
+ PrivilegedAction<Void> pa = () -> { r.run(); return null; };
+ AccessController.doPrivileged(pa, acc);
+ }
+ }
+
+ @Override
+ public void execute(Runnable r) {
+ executor.execute(new PrivilegedRunnable(r, acc));
+ }
+}
--- a/src/jdk.incubator.httpclient/share/classes/jdk/incubator/http/PullPublisher.java Sun Nov 05 17:05:57 2017 +0000
+++ b/src/jdk.incubator.httpclient/share/classes/jdk/incubator/http/PullPublisher.java Sun Nov 05 17:32:13 2017 +0000
@@ -1,5 +1,5 @@
/*
- * Copyright (c) 2016, Oracle and/or its affiliates. All rights reserved.
+ * Copyright (c) 2016, 2017, Oracle and/or its affiliates. All rights reserved.
* DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER.
*
* This code is free software; you can redistribute it and/or modify it
@@ -27,10 +27,12 @@
import java.util.Iterator;
import java.util.concurrent.Flow;
+import jdk.incubator.http.internal.common.Demand;
+import jdk.incubator.http.internal.common.SequentialScheduler;
/**
- * A Publisher that is expected to run in same thread as subscriber.
- * Items are obtained from Iterable. Each new subscription gets a new Iterator.
+ * A Publisher that publishes items obtained from the given Iterable. Each new
+ * subscription gets a new Iterator.
*/
class PullPublisher<T> implements Flow.Publisher<T> {
@@ -49,41 +51,54 @@
private final Flow.Subscriber<? super T> subscriber;
private final Iterator<T> iter;
- private boolean done = false;
- private long demand = 0;
- private int recursion = 0;
+ private volatile boolean completed;
+ private volatile boolean cancelled;
+ private volatile Throwable error;
+ final SequentialScheduler pullScheduler = new SequentialScheduler(new PullTask());
+ private final Demand demand = new Demand();
Subscription(Flow.Subscriber<? super T> subscriber, Iterator<T> iter) {
this.subscriber = subscriber;
this.iter = iter;
}
- @Override
- public void request(long n) {
- if (done) {
- subscriber.onError(new IllegalArgumentException("request(" + n + ")"));
- }
- demand += n;
- recursion ++;
- if (recursion > 1) {
- return;
- }
- while (demand > 0) {
- done = !iter.hasNext();
- if (done) {
- subscriber.onComplete();
- recursion --;
+ final class PullTask extends SequentialScheduler.CompleteRestartableTask {
+ @Override
+ protected void run() {
+ if (completed) {
+ pullScheduler.stop();
return;
}
- subscriber.onNext(iter.next());
- demand --;
+ Throwable t = error;
+ if (t != null) {
+ completed = true;
+ pullScheduler.stop();
+ subscriber.onError(t);
+ }
+ if (demand.tryDecrement()) {
+ boolean done = completed = !iter.hasNext();
+ if (done)
+ subscriber.onComplete();
+ else
+ subscriber.onNext(iter.next());
+ }
}
}
@Override
- public void cancel() {
- done = true;
+ public void request(long n) {
+ if (cancelled) {
+ error = new IllegalArgumentException("request("
+ + n + "): cancelled");
+ } else {
+ demand.increase(n);
+ }
+ pullScheduler.runOrSchedule();
}
+ @Override
+ public void cancel() {
+ cancelled = true;
+ }
}
}
--- a/src/jdk.incubator.httpclient/share/classes/jdk/incubator/http/PushGroup.java Sun Nov 05 17:05:57 2017 +0000
+++ b/src/jdk.incubator.httpclient/share/classes/jdk/incubator/http/PushGroup.java Sun Nov 05 17:32:13 2017 +0000
@@ -1,5 +1,5 @@
/*
- * Copyright (c) 2016, Oracle and/or its affiliates. All rights reserved.
+ * Copyright (c) 2016, 2017, Oracle and/or its affiliates. All rights reserved.
* DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER.
*
* This code is free software; you can redistribute it and/or modify it
@@ -25,7 +25,11 @@
package jdk.incubator.http;
+import java.security.AccessControlContext;
+import java.util.Optional;
import java.util.concurrent.CompletableFuture;
+import jdk.incubator.http.HttpResponse.BodyHandler;
+import jdk.incubator.http.HttpResponse.UntrustedBodyHandler;
import jdk.incubator.http.internal.common.MinimalFuture;
import jdk.incubator.http.internal.common.Log;
@@ -38,59 +42,82 @@
final CompletableFuture<Void> resultCF;
final CompletableFuture<Void> noMorePushesCF;
- volatile Throwable error; // any exception that occured during pushes
+ volatile Throwable error; // any exception that occurred during pushes
// CF for main response
final CompletableFuture<HttpResponse<T>> mainResponse;
- // user's processor object
- final HttpResponse.MultiProcessor<U, T> multiProcessor;
+ // user's subscriber object
+ final HttpResponse.MultiSubscriber<U, T> multiSubscriber;
final HttpResponse.BodyHandler<T> mainBodyHandler;
+ private final AccessControlContext acc;
+
int numberOfPushes;
int remainingPushes;
boolean noMorePushes = false;
- PushGroup(HttpResponse.MultiProcessor<U, T> multiProcessor, HttpRequestImpl req) {
- this(multiProcessor, req, new MinimalFuture<>());
+ PushGroup(HttpResponse.MultiSubscriber<U, T> multiSubscriber,
+ HttpRequestImpl req,
+ AccessControlContext acc) {
+ this(multiSubscriber, req, new MinimalFuture<>(), acc);
}
// Check mainBodyHandler before calling nested constructor.
- private PushGroup(HttpResponse.MultiProcessor<U, T> multiProcessor,
- HttpRequestImpl req,
- CompletableFuture<HttpResponse<T>> mainResponse) {
- this(multiProcessor, mainResponse,
- multiProcessor.onRequest(req).orElseThrow(
- () -> new IllegalArgumentException(
- "A valid body processor for the main response is required")));
+ private PushGroup(HttpResponse.MultiSubscriber<U, T> multiSubscriber,
+ HttpRequestImpl req,
+ CompletableFuture<HttpResponse<T>> mainResponse,
+ AccessControlContext acc) {
+ this(multiSubscriber,
+ mainResponse,
+ multiSubscriber.onRequest(req),
+ acc);
}
- // This private constructor is called after all parameters have been
- // checked.
- private PushGroup(HttpResponse.MultiProcessor<U, T> multiProcessor,
+ // This private constructor is called after all parameters have been checked.
+ private PushGroup(HttpResponse.MultiSubscriber<U, T> multiSubscriber,
CompletableFuture<HttpResponse<T>> mainResponse,
- HttpResponse.BodyHandler<T> mainBodyHandler) {
+ HttpResponse.BodyHandler<T> mainBodyHandler,
+ AccessControlContext acc) {
assert mainResponse != null; // A new instance is created above
assert mainBodyHandler != null; // should have been checked above
this.resultCF = new MinimalFuture<>();
this.noMorePushesCF = new MinimalFuture<>();
- this.multiProcessor = multiProcessor;
+ this.multiSubscriber = multiSubscriber;
this.mainResponse = mainResponse.thenApply(r -> {
- multiProcessor.onResponse(r);
+ multiSubscriber.onResponse(r);
return r;
});
this.mainBodyHandler = mainBodyHandler;
+ if (acc != null) {
+ // Restricts the file publisher with the senders ACC, if any
+ if (mainBodyHandler instanceof UntrustedBodyHandler)
+ ((UntrustedBodyHandler)this.mainBodyHandler).setAccessControlContext(acc);
+ }
+ this.acc = acc;
}
CompletableFuture<Void> groupResult() {
return resultCF;
}
- HttpResponse.MultiProcessor<U, T> processor() {
- return multiProcessor;
+ HttpResponse.MultiSubscriber<U, T> subscriber() {
+ return multiSubscriber;
+ }
+
+ Optional<BodyHandler<T>> handlerForPushRequest(HttpRequest ppRequest) {
+ Optional<BodyHandler<T>> bh = multiSubscriber.onPushPromise(ppRequest);
+ if (acc != null && bh.isPresent()) {
+ // Restricts the file publisher with the senders ACC, if any
+ BodyHandler<T> x = bh.get();
+ if (x instanceof UntrustedBodyHandler)
+ ((UntrustedBodyHandler)x).setAccessControlContext(acc);
+ bh = Optional.of(x);
+ }
+ return bh;
}
HttpResponse.BodyHandler<T> mainResponseHandler() {
--- a/src/jdk.incubator.httpclient/share/classes/jdk/incubator/http/RawChannelImpl.java Sun Nov 05 17:05:57 2017 +0000
+++ b/src/jdk.incubator.httpclient/share/classes/jdk/incubator/http/RawChannelImpl.java Sun Nov 05 17:32:13 2017 +0000
@@ -32,6 +32,7 @@
import java.nio.ByteBuffer;
import java.nio.channels.SelectableChannel;
import java.nio.channels.SocketChannel;
+import java.util.function.Supplier;
/*
* Each RawChannel corresponds to a TCP connection (SocketChannel) but is
@@ -41,17 +42,19 @@
final class RawChannelImpl implements RawChannel {
private final HttpClientImpl client;
- private final HttpConnection connection;
+ private final HttpConnection.DetachedConnectionChannel detachedChannel;
private final Object initialLock = new Object();
- private ByteBuffer initial;
+ private Supplier<ByteBuffer> initial;
RawChannelImpl(HttpClientImpl client,
HttpConnection connection,
- ByteBuffer initial)
+ Supplier<ByteBuffer> initial)
throws IOException
{
this.client = client;
- this.connection = connection;
+ this.detachedChannel = connection.detachChannel();
+ this.initial = initial;
+
SocketChannel chan = connection.channel();
client.cancelRegistration(chan);
// Constructing a RawChannel is supposed to have a "hand over"
@@ -64,15 +67,11 @@
chan.close();
} catch (IOException e1) {
e.addSuppressed(e1);
+ } finally {
+ detachedChannel.close();
}
throw e;
}
- // empty the initial buffer into our own copy.
- synchronized (initialLock) {
- this.initial = initial.hasRemaining()
- ? Utils.copy(initial)
- : Utils.EMPTY_BYTEBUFFER;
- }
}
private class NonBlockingRawAsyncEvent extends AsyncEvent {
@@ -80,13 +79,13 @@
private final RawEvent re;
NonBlockingRawAsyncEvent(RawEvent re) {
- super(0); // !BLOCKING & !REPEATING
+ // !BLOCKING & !REPEATING
this.re = re;
}
@Override
public SelectableChannel channel() {
- return connection.channel();
+ return detachedChannel.channel();
}
@Override
@@ -100,7 +99,7 @@
}
@Override
- public void abort() { }
+ public void abort(IOException ioe) { }
}
@Override
@@ -110,8 +109,9 @@
@Override
public ByteBuffer read() throws IOException {
- assert !connection.channel().isBlocking();
- return connection.read();
+ assert !detachedChannel.channel().isBlocking();
+ // connection.read() will no longer be available.
+ return detachedChannel.read();
}
@Override
@@ -120,7 +120,9 @@
if (initial == null) {
throw new IllegalStateException();
}
- ByteBuffer ref = initial;
+ ByteBuffer ref = initial.get();
+ ref = ref.hasRemaining() ? Utils.copy(ref)
+ : Utils.EMPTY_BYTEBUFFER;
initial = null;
return ref;
}
@@ -128,21 +130,29 @@
@Override
public long write(ByteBuffer[] src, int offset, int len) throws IOException {
- return connection.write(src, offset, len);
+ // this makes the whitebox driver test fail.
+ return detachedChannel.write(src, offset, len);
}
@Override
public void shutdownInput() throws IOException {
- connection.shutdownInput();
+ detachedChannel.shutdownInput();
}
@Override
public void shutdownOutput() throws IOException {
- connection.shutdownOutput();
+ detachedChannel.shutdownOutput();
}
@Override
public void close() throws IOException {
- connection.close();
+ detachedChannel.close();
}
+
+ @Override
+ public String toString() {
+ return super.toString()+"("+ detachedChannel.toString() + ")";
+ }
+
+
}
--- a/src/jdk.incubator.httpclient/share/classes/jdk/incubator/http/RequestProcessors.java Sun Nov 05 17:05:57 2017 +0000
+++ /dev/null Thu Jan 01 00:00:00 1970 +0000
@@ -1,312 +0,0 @@
-/*
- * Copyright (c) 2016, Oracle and/or its affiliates. All rights reserved.
- * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER.
- *
- * This code is free software; you can redistribute it and/or modify it
- * under the terms of the GNU General Public License version 2 only, as
- * published by the Free Software Foundation. Oracle designates this
- * particular file as subject to the "Classpath" exception as provided
- * by Oracle in the LICENSE file that accompanied this code.
- *
- * This code is distributed in the hope that it will be useful, but WITHOUT
- * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or
- * FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License
- * version 2 for more details (a copy is included in the LICENSE file that
- * accompanied this code).
- *
- * You should have received a copy of the GNU General Public License version
- * 2 along with this work; if not, write to the Free Software Foundation,
- * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA.
- *
- * Please contact Oracle, 500 Oracle Parkway, Redwood Shores, CA 94065 USA
- * or visit www.oracle.com if you need additional information or have any
- * questions.
- */
-
-package jdk.incubator.http;
-
-import java.io.File;
-import java.io.FileInputStream;
-import java.io.FileNotFoundException;
-import java.io.IOException;
-import java.io.InputStream;
-import java.io.UncheckedIOException;
-import java.nio.ByteBuffer;
-import java.nio.charset.Charset;
-import java.nio.file.Path;
-import java.util.ArrayList;
-import java.util.Iterator;
-import java.util.List;
-import java.util.NoSuchElementException;
-import java.util.concurrent.ConcurrentLinkedQueue;
-import java.util.concurrent.Flow;
-import java.util.function.Supplier;
-import jdk.incubator.http.internal.common.Utils;
-
-class RequestProcessors {
-
- static class ByteArrayProcessor implements HttpRequest.BodyProcessor {
- private volatile Flow.Publisher<ByteBuffer> delegate;
- private final int length;
- private final byte[] content;
- private final int offset;
-
- ByteArrayProcessor(byte[] content) {
- this(content, 0, content.length);
- }
-
- ByteArrayProcessor(byte[] content, int offset, int length) {
- this.content = content;
- this.offset = offset;
- this.length = length;
- }
-
- List<ByteBuffer> copy(byte[] content, int offset, int length) {
- List<ByteBuffer> bufs = new ArrayList<>();
- while (length > 0) {
- ByteBuffer b = ByteBuffer.allocate(Math.min(Utils.BUFSIZE, length));
- int max = b.capacity();
- int tocopy = Math.min(max, length);
- b.put(content, offset, tocopy);
- offset += tocopy;
- length -= tocopy;
- b.flip();
- bufs.add(b);
- }
- return bufs;
- }
-
- @Override
- public void subscribe(Flow.Subscriber<? super ByteBuffer> subscriber) {
- List<ByteBuffer> copy = copy(content, offset, length);
- this.delegate = new PullPublisher<>(copy);
- delegate.subscribe(subscriber);
- }
-
- @Override
- public long contentLength() {
- return length;
- }
- }
-
- // This implementation has lots of room for improvement.
- static class IterableProcessor implements HttpRequest.BodyProcessor {
- private volatile Flow.Publisher<ByteBuffer> delegate;
- private final Iterable<byte[]> content;
- private volatile long contentLength;
-
- IterableProcessor(Iterable<byte[]> content) {
- this.content = content;
- }
-
- // The ByteBufferIterator will iterate over the byte[] arrays in
- // the content one at the time.
- //
- class ByteBufferIterator implements Iterator<ByteBuffer> {
- final ConcurrentLinkedQueue<ByteBuffer> buffers = new ConcurrentLinkedQueue<>();
- final Iterator<byte[]> iterator = content.iterator();
- @Override
- public boolean hasNext() {
- return !buffers.isEmpty() || iterator.hasNext();
- }
-
- @Override
- public ByteBuffer next() {
- ByteBuffer buffer = buffers.poll();
- while (buffer == null) {
- copy();
- buffer = buffers.poll();
- }
- return buffer;
- }
-
- ByteBuffer getBuffer() {
- return Utils.getBuffer();
- }
-
- void copy() {
- byte[] bytes = iterator.next();
- int length = bytes.length;
- if (length == 0 && iterator.hasNext()) {
- // avoid inserting empty buffers, except
- // if that's the last.
- return;
- }
- int offset = 0;
- do {
- ByteBuffer b = getBuffer();
- int max = b.capacity();
-
- int tocopy = Math.min(max, length);
- b.put(bytes, offset, tocopy);
- offset += tocopy;
- length -= tocopy;
- b.flip();
- buffers.add(b);
- } while (length > 0);
- }
- }
-
- public Iterator<ByteBuffer> iterator() {
- return new ByteBufferIterator();
- }
-
- @Override
- public void subscribe(Flow.Subscriber<? super ByteBuffer> subscriber) {
- Iterable<ByteBuffer> iterable = this::iterator;
- this.delegate = new PullPublisher<>(iterable);
- delegate.subscribe(subscriber);
- }
-
- static long computeLength(Iterable<byte[]> bytes) {
- long len = 0;
- for (byte[] b : bytes) {
- len = Math.addExact(len, (long)b.length);
- }
- return len;
- }
-
- @Override
- public long contentLength() {
- if (contentLength == 0) {
- synchronized(this) {
- if (contentLength == 0) {
- contentLength = computeLength(content);
- }
- }
- }
- return contentLength;
- }
- }
-
- static class StringProcessor extends ByteArrayProcessor {
- public StringProcessor(String content, Charset charset) {
- super(content.getBytes(charset));
- }
- }
-
- static class EmptyProcessor implements HttpRequest.BodyProcessor {
- PseudoPublisher<ByteBuffer> delegate = new PseudoPublisher<>();
-
- @Override
- public long contentLength() {
- return 0;
- }
-
- @Override
- public void subscribe(Flow.Subscriber<? super ByteBuffer> subscriber) {
- delegate.subscribe(subscriber);
- }
- }
-
- static class FileProcessor extends InputStreamProcessor
- implements HttpRequest.BodyProcessor
- {
- File file;
-
- FileProcessor(Path name) {
- super(() -> create(name));
- file = name.toFile();
- }
-
- static FileInputStream create(Path name) {
- try {
- return new FileInputStream(name.toFile());
- } catch (FileNotFoundException e) {
- throw new UncheckedIOException(e);
- }
- }
- @Override
- public long contentLength() {
- return file.length();
- }
- }
-
- /**
- * Reads one buffer ahead all the time, blocking in hasNext()
- */
- static class StreamIterator implements Iterator<ByteBuffer> {
- final InputStream is;
- ByteBuffer nextBuffer;
- boolean need2Read = true;
- boolean haveNext;
- Throwable error;
-
- StreamIterator(InputStream is) {
- this.is = is;
- }
-
- Throwable error() {
- return error;
- }
-
- private int read() {
- nextBuffer = Utils.getBuffer();
- nextBuffer.clear();
- byte[] buf = nextBuffer.array();
- int offset = nextBuffer.arrayOffset();
- int cap = nextBuffer.capacity();
- try {
- int n = is.read(buf, offset, cap);
- if (n == -1) {
- is.close();
- return -1;
- }
- //flip
- nextBuffer.limit(n);
- nextBuffer.position(0);
- return n;
- } catch (IOException ex) {
- error = ex;
- return -1;
- }
- }
-
- @Override
- public synchronized boolean hasNext() {
- if (need2Read) {
- haveNext = read() != -1;
- if (haveNext) {
- need2Read = false;
- }
- return haveNext;
- }
- return haveNext;
- }
-
- @Override
- public synchronized ByteBuffer next() {
- if (!hasNext()) {
- throw new NoSuchElementException();
- }
- need2Read = true;
- return nextBuffer;
- }
-
- }
-
- static class InputStreamProcessor implements HttpRequest.BodyProcessor {
- private final Supplier<? extends InputStream> streamSupplier;
- private Flow.Publisher<ByteBuffer> delegate;
-
- InputStreamProcessor(Supplier<? extends InputStream> streamSupplier) {
- this.streamSupplier = streamSupplier;
- }
-
- @Override
- public synchronized void subscribe(Flow.Subscriber<? super ByteBuffer> subscriber) {
-
- InputStream is = streamSupplier.get();
- if (is == null) {
- throw new UncheckedIOException(new IOException("no inputstream supplied"));
- }
- this.delegate = new PullPublisher<>(() -> new StreamIterator(is));
- delegate.subscribe(subscriber);
- }
-
- @Override
- public long contentLength() {
- return -1;
- }
- }
-}
--- /dev/null Thu Jan 01 00:00:00 1970 +0000
+++ b/src/jdk.incubator.httpclient/share/classes/jdk/incubator/http/RequestPublishers.java Sun Nov 05 17:32:13 2017 +0000
@@ -0,0 +1,351 @@
+/*
+ * Copyright (c) 2016, 2017, Oracle and/or its affiliates. All rights reserved.
+ * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER.
+ *
+ * This code is free software; you can redistribute it and/or modify it
+ * under the terms of the GNU General Public License version 2 only, as
+ * published by the Free Software Foundation. Oracle designates this
+ * particular file as subject to the "Classpath" exception as provided
+ * by Oracle in the LICENSE file that accompanied this code.
+ *
+ * This code is distributed in the hope that it will be useful, but WITHOUT
+ * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or
+ * FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License
+ * version 2 for more details (a copy is included in the LICENSE file that
+ * accompanied this code).
+ *
+ * You should have received a copy of the GNU General Public License version
+ * 2 along with this work; if not, write to the Free Software Foundation,
+ * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA.
+ *
+ * Please contact Oracle, 500 Oracle Parkway, Redwood Shores, CA 94065 USA
+ * or visit www.oracle.com if you need additional information or have any
+ * questions.
+ */
+
+package jdk.incubator.http;
+
+import java.io.File;
+import java.io.FileInputStream;
+import java.io.IOException;
+import java.io.InputStream;
+import java.io.UncheckedIOException;
+import java.nio.ByteBuffer;
+import java.nio.charset.Charset;
+import java.nio.file.Path;
+import java.security.AccessControlContext;
+import java.security.AccessController;
+import java.security.PrivilegedAction;
+import java.security.PrivilegedActionException;
+import java.security.PrivilegedExceptionAction;
+import java.util.ArrayList;
+import java.util.Iterator;
+import java.util.List;
+import java.util.NoSuchElementException;
+import java.util.Objects;
+import java.util.concurrent.ConcurrentLinkedQueue;
+import java.util.concurrent.Flow;
+import java.util.function.Supplier;
+import jdk.incubator.http.HttpRequest.BodyPublisher;
+import jdk.incubator.http.internal.common.Utils;
+
+class RequestPublishers {
+
+ static class ByteArrayPublisher implements HttpRequest.BodyPublisher {
+ private volatile Flow.Publisher<ByteBuffer> delegate;
+ private final int length;
+ private final byte[] content;
+ private final int offset;
+ private final int bufSize;
+
+ ByteArrayPublisher(byte[] content) {
+ this(content, 0, content.length);
+ }
+
+ ByteArrayPublisher(byte[] content, int offset, int length) {
+ this(content, offset, length, Utils.BUFSIZE);
+ }
+
+ /* bufSize exposed for testing purposes */
+ ByteArrayPublisher(byte[] content, int offset, int length, int bufSize) {
+ this.content = content;
+ this.offset = offset;
+ this.length = length;
+ this.bufSize = bufSize;
+ }
+
+ List<ByteBuffer> copy(byte[] content, int offset, int length) {
+ List<ByteBuffer> bufs = new ArrayList<>();
+ while (length > 0) {
+ ByteBuffer b = ByteBuffer.allocate(Math.min(bufSize, length));
+ int max = b.capacity();
+ int tocopy = Math.min(max, length);
+ b.put(content, offset, tocopy);
+ offset += tocopy;
+ length -= tocopy;
+ b.flip();
+ bufs.add(b);
+ }
+ return bufs;
+ }
+
+ @Override
+ public void subscribe(Flow.Subscriber<? super ByteBuffer> subscriber) {
+ List<ByteBuffer> copy = copy(content, offset, length);
+ this.delegate = new PullPublisher<>(copy);
+ delegate.subscribe(subscriber);
+ }
+
+ @Override
+ public long contentLength() {
+ return length;
+ }
+ }
+
+ // This implementation has lots of room for improvement.
+ static class IterablePublisher implements HttpRequest.BodyPublisher {
+ private volatile Flow.Publisher<ByteBuffer> delegate;
+ private final Iterable<byte[]> content;
+ private volatile long contentLength;
+
+ IterablePublisher(Iterable<byte[]> content) {
+ this.content = content;
+ }
+
+ // The ByteBufferIterator will iterate over the byte[] arrays in
+ // the content one at the time.
+ //
+ class ByteBufferIterator implements Iterator<ByteBuffer> {
+ final ConcurrentLinkedQueue<ByteBuffer> buffers = new ConcurrentLinkedQueue<>();
+ final Iterator<byte[]> iterator = content.iterator();
+ @Override
+ public boolean hasNext() {
+ return !buffers.isEmpty() || iterator.hasNext();
+ }
+
+ @Override
+ public ByteBuffer next() {
+ ByteBuffer buffer = buffers.poll();
+ while (buffer == null) {
+ copy();
+ buffer = buffers.poll();
+ }
+ return buffer;
+ }
+
+ ByteBuffer getBuffer() {
+ return Utils.getBuffer();
+ }
+
+ void copy() {
+ byte[] bytes = iterator.next();
+ int length = bytes.length;
+ if (length == 0 && iterator.hasNext()) {
+ // avoid inserting empty buffers, except
+ // if that's the last.
+ return;
+ }
+ int offset = 0;
+ do {
+ ByteBuffer b = getBuffer();
+ int max = b.capacity();
+
+ int tocopy = Math.min(max, length);
+ b.put(bytes, offset, tocopy);
+ offset += tocopy;
+ length -= tocopy;
+ b.flip();
+ buffers.add(b);
+ } while (length > 0);
+ }
+ }
+
+ public Iterator<ByteBuffer> iterator() {
+ return new ByteBufferIterator();
+ }
+
+ @Override
+ public void subscribe(Flow.Subscriber<? super ByteBuffer> subscriber) {
+ Iterable<ByteBuffer> iterable = this::iterator;
+ this.delegate = new PullPublisher<>(iterable);
+ delegate.subscribe(subscriber);
+ }
+
+ static long computeLength(Iterable<byte[]> bytes) {
+ long len = 0;
+ for (byte[] b : bytes) {
+ len = Math.addExact(len, (long)b.length);
+ }
+ return len;
+ }
+
+ @Override
+ public long contentLength() {
+ if (contentLength == 0) {
+ synchronized(this) {
+ if (contentLength == 0) {
+ contentLength = computeLength(content);
+ }
+ }
+ }
+ return contentLength;
+ }
+ }
+
+ static class StringPublisher extends ByteArrayPublisher {
+ public StringPublisher(String content, Charset charset) {
+ super(content.getBytes(charset));
+ }
+ }
+
+ static class EmptyPublisher implements HttpRequest.BodyPublisher {
+ private final PseudoPublisher<ByteBuffer> delegate = new PseudoPublisher<>();
+
+ @Override
+ public long contentLength() {
+ return 0;
+ }
+
+ @Override
+ public void subscribe(Flow.Subscriber<? super ByteBuffer> subscriber) {
+ delegate.subscribe(subscriber);
+ }
+ }
+
+ static class FilePublisher implements BodyPublisher {
+ private final File file;
+ private volatile AccessControlContext acc;
+
+ FilePublisher(Path name) {
+ file = name.toFile();
+ }
+
+ void setAccessControlContext(AccessControlContext acc) {
+ this.acc = acc;
+ }
+
+ @Override
+ public void subscribe(Flow.Subscriber<? super ByteBuffer> subscriber) {
+ if (System.getSecurityManager() != null && acc == null)
+ throw new InternalError(
+ "Unexpected null acc when security manager has been installed");
+
+ InputStream is;
+ try {
+ PrivilegedExceptionAction<FileInputStream> pa =
+ () -> new FileInputStream(file);
+ is = AccessController.doPrivileged(pa, acc);
+ } catch (PrivilegedActionException pae) {
+ throw new UncheckedIOException((IOException)pae.getCause());
+ }
+ PullPublisher<ByteBuffer> publisher =
+ new PullPublisher<>(() -> new StreamIterator(is));
+ publisher.subscribe(subscriber);
+ }
+
+ @Override
+ public long contentLength() {
+ assert System.getSecurityManager() != null ? acc != null: true;
+ PrivilegedAction<Long> pa = () -> file.length();
+ return AccessController.doPrivileged(pa, acc);
+ }
+ }
+
+ /**
+ * Reads one buffer ahead all the time, blocking in hasNext()
+ */
+ static class StreamIterator implements Iterator<ByteBuffer> {
+ final InputStream is;
+ final Supplier<? extends ByteBuffer> bufSupplier;
+ volatile ByteBuffer nextBuffer;
+ volatile boolean need2Read = true;
+ volatile boolean haveNext;
+ volatile Throwable error;
+
+ StreamIterator(InputStream is) {
+ this(is, Utils::getBuffer);
+ }
+
+ StreamIterator(InputStream is, Supplier<? extends ByteBuffer> bufSupplier) {
+ this.is = is;
+ this.bufSupplier = bufSupplier;
+ }
+
+ Throwable error() {
+ return error;
+ }
+
+ private int read() {
+ nextBuffer = bufSupplier.get();
+ nextBuffer.clear();
+ byte[] buf = nextBuffer.array();
+ int offset = nextBuffer.arrayOffset();
+ int cap = nextBuffer.capacity();
+ try {
+ int n = is.read(buf, offset, cap);
+ if (n == -1) {
+ is.close();
+ return -1;
+ }
+ //flip
+ nextBuffer.limit(n);
+ nextBuffer.position(0);
+ return n;
+ } catch (IOException ex) {
+ error = ex;
+ return -1;
+ }
+ }
+
+ @Override
+ public synchronized boolean hasNext() {
+ if (need2Read) {
+ haveNext = read() != -1;
+ if (haveNext) {
+ need2Read = false;
+ }
+ return haveNext;
+ }
+ return haveNext;
+ }
+
+ @Override
+ public synchronized ByteBuffer next() {
+ if (!hasNext()) {
+ throw new NoSuchElementException();
+ }
+ need2Read = true;
+ return nextBuffer;
+ }
+
+ }
+
+ static class InputStreamPublisher implements BodyPublisher {
+ private final Supplier<? extends InputStream> streamSupplier;
+
+ InputStreamPublisher(Supplier<? extends InputStream> streamSupplier) {
+ this.streamSupplier = streamSupplier;
+ }
+
+ @Override
+ public void subscribe(Flow.Subscriber<? super ByteBuffer> subscriber) {
+
+ InputStream is = streamSupplier.get();
+ if (is == null) {
+ throw new UncheckedIOException(new IOException("no inputstream supplied"));
+ }
+ PullPublisher<ByteBuffer> publisher =
+ new PullPublisher<>(iterableOf(is));
+ publisher.subscribe(subscriber);
+ }
+
+ protected Iterable<ByteBuffer> iterableOf(InputStream is) {
+ return () -> new StreamIterator(is);
+ }
+
+ @Override
+ public long contentLength() {
+ return -1;
+ }
+ }
+}
--- a/src/jdk.incubator.httpclient/share/classes/jdk/incubator/http/Response.java Sun Nov 05 17:05:57 2017 +0000
+++ b/src/jdk.incubator.httpclient/share/classes/jdk/incubator/http/Response.java Sun Nov 05 17:32:13 2017 +0000
@@ -25,6 +25,8 @@
package jdk.incubator.http;
+import java.net.URI;
+
/**
* Response headers and status code.
*/
@@ -66,4 +68,19 @@
int statusCode() {
return statusCode;
}
+
+ @Override
+ public String toString() {
+ StringBuilder sb = new StringBuilder();
+ String method = request().method();
+ URI uri = request().uri();
+ String uristring = uri == null ? "" : uri.toString();
+ sb.append('(')
+ .append(method)
+ .append(" ")
+ .append(uristring)
+ .append(") ")
+ .append(statusCode());
+ return sb.toString();
+ }
}
--- a/src/jdk.incubator.httpclient/share/classes/jdk/incubator/http/ResponseContent.java Sun Nov 05 17:05:57 2017 +0000
+++ b/src/jdk.incubator.httpclient/share/classes/jdk/incubator/http/ResponseContent.java Sun Nov 05 17:32:13 2017 +0000
@@ -26,8 +26,10 @@
package jdk.incubator.http;
import java.io.IOException;
+import java.lang.System.Logger.Level;
import java.nio.ByteBuffer;
-import java.util.Optional;
+import java.util.ArrayList;
+import java.util.List;
import java.util.function.Consumer;
import jdk.incubator.http.internal.common.Utils;
@@ -39,35 +41,27 @@
*/
class ResponseContent {
- final HttpResponse.BodyProcessor<?> pusher;
- final HttpConnection connection;
+ static final boolean DEBUG = Utils.DEBUG; // Revisit: temporary dev flag.
+
+ final HttpResponse.BodySubscriber<?> pusher;
final int contentLength;
- ByteBuffer buffer;
- //ByteBuffer lastBufferUsed;
- final ResponseHeaders headers;
- private final Consumer<Optional<ByteBuffer>> dataConsumer;
- private final Consumer<IOException> errorConsumer;
- private final HttpClientImpl client;
+ final HttpHeaders headers;
// this needs to run before we complete the body
// so that connection can be returned to pool
private final Runnable onFinished;
+ private final String dbgTag;
ResponseContent(HttpConnection connection,
int contentLength,
- ResponseHeaders h,
- HttpResponse.BodyProcessor<?> userProcessor,
- Consumer<Optional<ByteBuffer>> dataConsumer,
- Consumer<IOException> errorConsumer,
+ HttpHeaders h,
+ HttpResponse.BodySubscriber<?> userProcessor,
Runnable onFinished)
{
- this.pusher = (HttpResponse.BodyProcessor)userProcessor;
- this.connection = connection;
+ this.pusher = userProcessor;
this.contentLength = contentLength;
this.headers = h;
- this.dataConsumer = dataConsumer;
- this.errorConsumer = errorConsumer;
- this.client = connection.client;
this.onFinished = onFinished;
+ this.dbgTag = connection.dbgString() + "/ResponseContent";
}
static final int LF = 10;
@@ -77,7 +71,7 @@
boolean chunkedContent, chunkedContentInitialized;
- private boolean contentChunked() throws IOException {
+ boolean contentChunked() throws IOException {
if (chunkedContentInitialized) {
return chunkedContent;
}
@@ -98,182 +92,374 @@
return chunkedContent;
}
- /**
- * Entry point for pusher. b is an initial ByteBuffer that may
- * have some data in it. When this method returns, the body
- * has been fully processed.
- */
- void pushBody(ByteBuffer b) {
- try {
- // TODO: check status
- if (contentChunked()) {
- pushBodyChunked(b);
- } else {
- pushBodyFixed(b);
- }
- } catch (IOException t) {
- errorConsumer.accept(t);
+ interface BodyParser extends Consumer<ByteBuffer> {
+ void onSubscribe(AbstractSubscription sub);
+ }
+
+ // Returns a parser that will take care of parsing the received byte
+ // buffers and forward them to the BodyProcessor.
+ // When the parser is done, it will call onComplete.
+ // If parsing was successful, the throwable parameter will be null.
+ // Otherwise it will be the exception that occurred
+ // Note: revisit: it might be better to use a CompletableFuture than
+ // a completion handler.
+ BodyParser getBodyParser(Consumer<Throwable> onComplete)
+ throws IOException {
+ if (contentChunked()) {
+ return new ChunkedBodyParser(onComplete);
+ } else {
+ return new FixedLengthBodyParser(contentLength, onComplete);
}
}
- // reads and returns chunklen. Position of chunkbuf is first byte
- // of chunk on return. chunklen includes the CR LF at end of chunk
- int readChunkLen() throws IOException {
- chunklen = 0;
- boolean cr = false;
- while (true) {
- getHunk();
- int c = chunkbuf.get();
- if (cr) {
- if (c == LF) {
- return chunklen + 2;
- } else {
- throw new IOException("invalid chunk header");
- }
- }
- if (c == CR) {
- cr = true;
- } else {
- int digit = toDigit(c);
- chunklen = chunklen * 16 + digit;
- }
+
+ static enum ChunkState {READING_LENGTH, READING_DATA, DONE};
+ class ChunkedBodyParser implements BodyParser {
+ final ByteBuffer READMORE = Utils.EMPTY_BYTEBUFFER;
+ final Consumer<Throwable> onComplete;
+ final System.Logger debug = Utils.getDebugLogger(this::dbgString, DEBUG);
+ final String dbgTag = ResponseContent.this.dbgTag + "/ChunkedBodyParser";
+
+ volatile Throwable closedExceptionally;
+ volatile int partialChunklen = 0; // partially read chunk len
+ volatile int chunklen = -1; // number of bytes in chunk
+ volatile int bytesremaining; // number of bytes in chunk left to be read incl CRLF
+ volatile boolean cr = false; // tryReadChunkLength has found CR
+ volatile int bytesToConsume; // number of bytes that still need to be consumed before proceeding
+ volatile ChunkState state = ChunkState.READING_LENGTH; // current state
+ volatile AbstractSubscription sub;
+ ChunkedBodyParser(Consumer<Throwable> onComplete) {
+ this.onComplete = onComplete;
}
- }
+
+ String dbgString() {
+ return dbgTag;
+ }
- int chunklen = -1; // number of bytes in chunk (fixed)
- int bytesremaining; // number of bytes in chunk left to be read incl CRLF
- int bytesread;
- ByteBuffer chunkbuf; // initialise
+ @Override
+ public void onSubscribe(AbstractSubscription sub) {
+ debug.log(Level.DEBUG, () -> "onSubscribe: "
+ + pusher.getClass().getName());
+ pusher.onSubscribe(this.sub = sub);
+ }
- // make sure we have at least 1 byte to look at
- private void getHunk() throws IOException {
- if (chunkbuf == null || !chunkbuf.hasRemaining()) {
- chunkbuf = connection.read();
- }
- }
-
- private void consumeBytes(int n) throws IOException {
- getHunk();
- while (n > 0) {
- int e = Math.min(chunkbuf.remaining(), n);
- chunkbuf.position(chunkbuf.position() + e);
- n -= e;
- if (n > 0) {
- getHunk();
+ @Override
+ public void accept(ByteBuffer b) {
+ if (closedExceptionally != null) {
+ debug.log(Level.DEBUG, () -> "already closed: "
+ + closedExceptionally);
+ return;
}
- }
- }
+ boolean completed = false;
+ try {
+ List<ByteBuffer> out = new ArrayList<>();
+ do {
+ if (tryPushOneHunk(b, out)) {
+ // We're done! (true if the final chunk was parsed).
+ if (!out.isEmpty()) {
+ // push what we have and complete
+ // only reduce demand if we actually push something.
+ // we would not have come here if there was no
+ // demand.
+ boolean hasDemand = sub.demand().tryDecrement();
+ assert hasDemand;
+ pusher.onNext(out);
+ }
+ debug.log(Level.DEBUG, () -> "done!");
+ assert closedExceptionally == null;
+ assert state == ChunkState.DONE;
+ onFinished.run();
+ pusher.onComplete();
+ completed = true;
+ onComplete.accept(closedExceptionally); // should be null
+ break;
+ }
+ // the buffer may contain several hunks, and therefore
+ // we must loop while it's not exhausted.
+ } while (b.hasRemaining());
- /**
- * Returns a ByteBuffer containing a chunk of data or a "hunk" of data
- * (a chunk of a chunk if the chunk size is larger than our ByteBuffers).
- * ByteBuffer returned is obtained from response processor.
- */
- ByteBuffer readChunkedBuffer() throws IOException {
- if (chunklen == -1) {
- // new chunk
- chunklen = readChunkLen() - 2;
- bytesremaining = chunklen;
- if (chunklen == 0) {
- consumeBytes(2);
- return null;
+ if (!completed && !out.isEmpty()) {
+ // push what we have.
+ // only reduce demand if we actually push something.
+ // we would not have come here if there was no
+ // demand.
+ boolean hasDemand = sub.demand().tryDecrement();
+ assert hasDemand;
+ pusher.onNext(out);
+ }
+ assert state == ChunkState.DONE || !b.hasRemaining();
+ } catch(Throwable t) {
+ closedExceptionally = t;
+ if (!completed) onComplete.accept(t);
}
}
- getHunk();
- bytesread = chunkbuf.remaining();
- ByteBuffer returnBuffer = Utils.getBuffer();
- int space = returnBuffer.remaining();
+ // reads and returns chunklen. Position of chunkbuf is first byte
+ // of chunk on return. chunklen includes the CR LF at end of chunk
+ // returns -1 if needs more bytes
+ private int tryReadChunkLen(ByteBuffer chunkbuf) throws IOException {
+ assert state == ChunkState.READING_LENGTH;
+ while (chunkbuf.hasRemaining()) {
+ int c = chunkbuf.get();
+ if (cr) {
+ if (c == LF) {
+ return partialChunklen;
+ } else {
+ throw new IOException("invalid chunk header");
+ }
+ }
+ if (c == CR) {
+ cr = true;
+ } else {
+ int digit = toDigit(c);
+ partialChunklen = partialChunklen * 16 + digit;
+ }
+ }
+ return -1;
+ }
+
+
+ // try to consume as many bytes as specified by bytesToConsume.
+ // returns the number of bytes that still need to be consumed.
+ // In practice this method is only called to consume one CRLF pair
+ // with bytesToConsume set to 2, so it will only return 0 (if completed),
+ // 1, or 2 (if chunkbuf doesn't have the 2 chars).
+ private int tryConsumeBytes(ByteBuffer chunkbuf) throws IOException {
+ int n = bytesToConsume;
+ if (n > 0) {
+ int e = Math.min(chunkbuf.remaining(), n);
+
+ // verifies some assertions
+ // this methods is called only to consume CRLF
+ if (Utils.ASSERTIONSENABLED) {
+ assert n <= 2 && e <= 2;
+ ByteBuffer tmp = chunkbuf.slice();
+ // if n == 2 assert that we will first consume CR
+ assert (n == 2 && e > 0) ? tmp.get() == CR : true;
+ // if n == 1 || n == 2 && e == 2 assert that we then consume LF
+ assert (n == 1 || e == 2) ? tmp.get() == LF : true;
+ }
+
+ chunkbuf.position(chunkbuf.position() + e);
+ n -= e;
+ bytesToConsume = n;
+ }
+ assert n >= 0;
+ return n;
+ }
+
+ /**
+ * Returns a ByteBuffer containing chunk of data or a "hunk" of data
+ * (a chunk of a chunk if the chunk size is larger than our ByteBuffers).
+ * If the given chunk does not have enough data this method return
+ * an empty ByteBuffer (READMORE).
+ * If we encounter the final chunk (an empty chunk) this method
+ * returns null.
+ */
+ ByteBuffer tryReadOneHunk(ByteBuffer chunk) throws IOException {
+ int unfulfilled = bytesremaining;
+ int toconsume = bytesToConsume;
+ ChunkState st = state;
+ if (st == ChunkState.READING_LENGTH && chunklen == -1) {
+ debug.log(Level.DEBUG, () -> "Trying to read chunk len"
+ + " (remaining in buffer:"+chunk.remaining()+")");
+ int clen = chunklen = tryReadChunkLen(chunk);
+ if (clen == -1) return READMORE;
+ debug.log(Level.DEBUG, "Got chunk len %d", clen);
+ cr = false; partialChunklen = 0;
+ unfulfilled = bytesremaining = clen;
+ if (clen == 0) toconsume = bytesToConsume = 2; // that was the last chunk
+ else st = state = ChunkState.READING_DATA; // read the data
+ }
+
+ if (toconsume > 0) {
+ debug.log(Level.DEBUG,
+ "Trying to consume bytes: %d (remaining in buffer: %s)",
+ toconsume, chunk.remaining());
+ if (tryConsumeBytes(chunk) > 0) {
+ return READMORE;
+ }
+ }
+
+ toconsume = bytesToConsume;
+ assert toconsume == 0;
+
- int bytes2Copy = Math.min(bytesread, Math.min(bytesremaining, space));
- Utils.copy(chunkbuf, returnBuffer, bytes2Copy);
- returnBuffer.flip();
- bytesremaining -= bytes2Copy;
- if (bytesremaining == 0) {
- consumeBytes(2);
- chunklen = -1;
+ if (st == ChunkState.READING_LENGTH) {
+ // we will come here only if chunklen was 0, after having
+ // consumed the trailing CRLF
+ int clen = chunklen;
+ assert clen == 0;
+ debug.log(Level.DEBUG, "No more chunks: %d", clen);
+ // the DONE state is not really needed but it helps with
+ // assertions...
+ state = ChunkState.DONE;
+ return null;
+ }
+
+ int clen = chunklen;
+ assert clen > 0;
+ assert st == ChunkState.READING_DATA;
+
+ ByteBuffer returnBuffer = READMORE; // May be a hunk or a chunk
+ if (unfulfilled > 0) {
+ int bytesread = chunk.remaining();
+ debug.log(Level.DEBUG, "Reading chunk: available %d, needed %d",
+ bytesread, unfulfilled);
+
+ int bytes2return = Math.min(bytesread, unfulfilled);
+ debug.log(Level.DEBUG, "Returning chunk bytes: %d", bytes2return);
+ returnBuffer = Utils.slice(chunk, bytes2return);
+ unfulfilled = bytesremaining -= bytes2return;
+ if (unfulfilled == 0) bytesToConsume = 2;
+ }
+
+ assert unfulfilled >= 0;
+
+ if (unfulfilled == 0) {
+ debug.log(Level.DEBUG,
+ "No more bytes to read - %d yet to consume.",
+ unfulfilled);
+ // check whether the trailing CRLF is consumed, try to
+ // consume it if not. If tryConsumeBytes needs more bytes
+ // then we will come back here later - skipping the block
+ // that reads data because remaining==0, and finding
+ // that the two bytes are now consumed.
+ if (tryConsumeBytes(chunk) == 0) {
+ // we're done for this chunk! reset all states and
+ // prepare to read the next chunk.
+ chunklen = -1;
+ partialChunklen = 0;
+ cr = false;
+ state = ChunkState.READING_LENGTH;
+ debug.log(Level.DEBUG, "Ready to read next chunk");
+ }
+ }
+ if (returnBuffer == READMORE) {
+ debug.log(Level.DEBUG, "Need more data");
+ }
+ return returnBuffer;
}
- return returnBuffer;
+
+
+ // Attempt to parse and push one hunk from the buffer.
+ // Returns true if the final chunk was parsed.
+ // Returns false if we need to push more chunks.
+ private boolean tryPushOneHunk(ByteBuffer b, List<ByteBuffer> out)
+ throws IOException {
+ assert state != ChunkState.DONE;
+ ByteBuffer b1 = tryReadOneHunk(b);
+ if (b1 != null) {
+ //assert b1.hasRemaining() || b1 == READMORE;
+ if (b1.hasRemaining()) {
+ debug.log(Level.DEBUG, "Sending chunk to consumer (%d)",
+ b1.remaining());
+ out.add(b1);
+ debug.log(Level.DEBUG, "Chunk sent.");
+ }
+ return false; // we haven't parsed the final chunk yet.
+ } else {
+ return true; // we're done! the final chunk was parsed.
+ }
+ }
+
+ private int toDigit(int b) throws IOException {
+ if (b >= 0x30 && b <= 0x39) {
+ return b - 0x30;
+ }
+ if (b >= 0x41 && b <= 0x46) {
+ return b - 0x41 + 10;
+ }
+ if (b >= 0x61 && b <= 0x66) {
+ return b - 0x61 + 10;
+ }
+ throw new IOException("Invalid chunk header byte " + b);
+ }
+
}
- ByteBuffer initialBuffer;
- int fixedBytesReturned;
+ class FixedLengthBodyParser implements BodyParser {
+ final int contentLength;
+ final Consumer<Throwable> onComplete;
+ final System.Logger debug = Utils.getDebugLogger(this::dbgString, DEBUG);
+ final String dbgTag = ResponseContent.this.dbgTag + "/FixedLengthBodyParser";
+ volatile int remaining;
+ volatile Throwable closedExceptionally;
+ volatile AbstractSubscription sub;
+ FixedLengthBodyParser(int contentLength, Consumer<Throwable> onComplete) {
+ this.contentLength = this.remaining = contentLength;
+ this.onComplete = onComplete;
+ }
+
+ String dbgString() {
+ return dbgTag;
+ }
- //ByteBuffer getResidue() {
- //return lastBufferUsed;
- //}
-
- private void compactBuffer(ByteBuffer buf) {
- buf.compact()
- .flip();
- }
+ @Override
+ public void onSubscribe(AbstractSubscription sub) {
+ debug.log(Level.DEBUG, () -> "length="
+ + contentLength +", onSubscribe: "
+ + pusher.getClass().getName());
+ pusher.onSubscribe(this.sub = sub);
+ try {
+ if (contentLength == 0) {
+ pusher.onComplete();
+ onComplete.accept(null);
+ }
+ } catch (Throwable t) {
+ closedExceptionally = t;
+ try {
+ pusher.onError(t);
+ } finally {
+ onComplete.accept(t);
+ }
+ }
+ }
- /**
- * Copies inbuf (numBytes from its position) to new buffer. The returned
- * buffer's position is zero and limit is at end (numBytes)
- */
- private ByteBuffer copyBuffer(ByteBuffer inbuf, int numBytes) {
- ByteBuffer b1 = Utils.getBuffer();
- assert b1.remaining() >= numBytes;
- byte[] b = b1.array();
- inbuf.get(b, 0, numBytes);
- b1.limit(numBytes);
- return b1;
- }
+ @Override
+ public void accept(ByteBuffer b) {
+ if (closedExceptionally != null) {
+ debug.log(Level.DEBUG, () -> "already closed: "
+ + closedExceptionally);
+ return;
+ }
+ boolean completed = false;
+ try {
+ int unfulfilled = remaining;
+ debug.log(Level.DEBUG, "Parser got %d bytes (%d remaining / %d)",
+ b.remaining(), unfulfilled, contentLength);
+ assert unfulfilled != 0 || contentLength == 0 || b.remaining() == 0;
+
+ if (unfulfilled == 0 && contentLength > 0) return;
- private void pushBodyChunked(ByteBuffer b) throws IOException {
- chunkbuf = b;
- while (true) {
- ByteBuffer b1 = readChunkedBuffer();
- if (b1 != null) {
- if (b1.hasRemaining()) {
- dataConsumer.accept(Optional.of(b1));
+ if (b.hasRemaining() && unfulfilled > 0) {
+ // only reduce demand if we actually push something.
+ // we would not have come here if there was no
+ // demand.
+ boolean hasDemand = sub.demand().tryDecrement();
+ assert hasDemand;
+ int amount = Math.min(b.remaining(), unfulfilled);
+ unfulfilled = remaining -= amount;
+ ByteBuffer buffer = Utils.slice(b, amount);
+ pusher.onNext(List.of(buffer));
}
- } else {
- onFinished.run();
- dataConsumer.accept(Optional.empty());
- return;
+ if (unfulfilled == 0) {
+ // We're done! All data has been received.
+ assert closedExceptionally == null;
+ onFinished.run();
+ pusher.onComplete();
+ completed = true;
+ onComplete.accept(closedExceptionally); // should be null
+ } else {
+ assert b.remaining() == 0;
+ }
+ } catch (Throwable t) {
+ debug.log(Level.DEBUG, "Unexpected exception", t);
+ closedExceptionally = t;
+ if (!completed) {
+ onComplete.accept(t);
+ }
}
}
}
-
- private int toDigit(int b) throws IOException {
- if (b >= 0x30 && b <= 0x39) {
- return b - 0x30;
- }
- if (b >= 0x41 && b <= 0x46) {
- return b - 0x41 + 10;
- }
- if (b >= 0x61 && b <= 0x66) {
- return b - 0x61 + 10;
- }
- throw new IOException("Invalid chunk header byte " + b);
- }
-
- private void pushBodyFixed(ByteBuffer b) throws IOException {
- int remaining = contentLength;
- while (b.hasRemaining() && remaining > 0) {
- ByteBuffer buffer = Utils.getBuffer();
- int amount = Math.min(b.remaining(), remaining);
- Utils.copy(b, buffer, amount);
- remaining -= amount;
- buffer.flip();
- dataConsumer.accept(Optional.of(buffer));
- }
- while (remaining > 0) {
- ByteBuffer buffer = connection.read();
- if (buffer == null)
- throw new IOException("connection closed");
-
- int bytesread = buffer.remaining();
- // assume for now that pipelining not implemented
- if (bytesread > remaining) {
- throw new IOException("too many bytes read");
- }
- remaining -= bytesread;
- dataConsumer.accept(Optional.of(buffer));
- }
- onFinished.run();
- dataConsumer.accept(Optional.empty());
- }
}
--- a/src/jdk.incubator.httpclient/share/classes/jdk/incubator/http/ResponseHeaders.java Sun Nov 05 17:05:57 2017 +0000
+++ b/src/jdk.incubator.httpclient/share/classes/jdk/incubator/http/ResponseHeaders.java Sun Nov 05 17:32:13 2017 +0000
@@ -44,11 +44,14 @@
import static jdk.incubator.http.internal.common.Utils.isValidValue;
import static java.util.Objects.requireNonNull;
+
+// ####: Remove. Replaced with Http1HeaderDecoder
+
/*
* Reads entire header block off channel, in blocking mode.
* This class is not thread-safe.
*/
-final class ResponseHeaders implements HttpHeaders {
+final class ResponseHeaders extends HttpHeaders {
private static final char CR = '\r';
private static final char LF = '\n';
@@ -63,27 +66,33 @@
* leftovers (i.e. data, if any, beyond the header block) are accessible
* from this same buffer from its position to its limit.
*/
- ResponseHeaders(HttpConnection connection, ByteBuffer buffer) throws IOException {
- requireNonNull(connection);
+ ResponseHeaders(BufferReader reader, ByteBuffer buffer) throws IOException {
+ requireNonNull(reader);
requireNonNull(buffer);
- InputStreamWrapper input = new InputStreamWrapper(connection, buffer);
+ InputStreamWrapper input =
+ new InputStreamWrapper(reader, buffer);
delegate = ImmutableHeaders.of(parse(input));
}
+ @FunctionalInterface
+ static interface BufferReader {
+ ByteBuffer read() throws IOException;
+ }
+
static final class InputStreamWrapper extends InputStream {
- final HttpConnection connection;
+ final BufferReader reader;
ByteBuffer buffer;
int lastRead = -1; // last byte read from the buffer
int consumed = 0; // number of bytes consumed.
- InputStreamWrapper(HttpConnection connection, ByteBuffer buffer) {
+ InputStreamWrapper(BufferReader reader, ByteBuffer buffer) {
super();
- this.connection = connection;
+ this.reader = reader;
this.buffer = buffer;
}
@Override
public int read() throws IOException {
if (!buffer.hasRemaining()) {
- buffer = connection.read();
+ buffer = reader.read();
if (buffer == null) {
return lastRead = -1;
}
--- a/src/jdk.incubator.httpclient/share/classes/jdk/incubator/http/ResponseProcessors.java Sun Nov 05 17:05:57 2017 +0000
+++ /dev/null Thu Jan 01 00:00:00 1970 +0000
@@ -1,331 +0,0 @@
-/*
- * Copyright (c) 2016, Oracle and/or its affiliates. All rights reserved.
- * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER.
- *
- * This code is free software; you can redistribute it and/or modify it
- * under the terms of the GNU General Public License version 2 only, as
- * published by the Free Software Foundation. Oracle designates this
- * particular file as subject to the "Classpath" exception as provided
- * by Oracle in the LICENSE file that accompanied this code.
- *
- * This code is distributed in the hope that it will be useful, but WITHOUT
- * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or
- * FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License
- * version 2 for more details (a copy is included in the LICENSE file that
- * accompanied this code).
- *
- * You should have received a copy of the GNU General Public License version
- * 2 along with this work; if not, write to the Free Software Foundation,
- * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA.
- *
- * Please contact Oracle, 500 Oracle Parkway, Redwood Shores, CA 94065 USA
- * or visit www.oracle.com if you need additional information or have any
- * questions.
- */
-
-package jdk.incubator.http;
-
-import java.io.IOException;
-import java.io.UncheckedIOException;
-import java.net.URI;
-import java.nio.ByteBuffer;
-import java.nio.channels.FileChannel;
-import java.nio.file.Files;
-import java.nio.file.OpenOption;
-import java.nio.file.Path;
-import java.util.ArrayList;
-import java.util.Collections;
-import java.util.HashMap;
-import java.util.List;
-import java.util.Map;
-import java.util.Optional;
-import java.util.concurrent.CompletableFuture;
-import java.util.concurrent.CompletionStage;
-import java.util.concurrent.ConcurrentHashMap;
-import java.util.concurrent.Flow;
-import java.util.function.Consumer;
-import java.util.function.Function;
-import jdk.incubator.http.internal.common.MinimalFuture;
-import jdk.incubator.http.internal.common.Utils;
-import jdk.incubator.http.internal.common.Log;
-
-class ResponseProcessors {
-
- static class ConsumerProcessor implements HttpResponse.BodyProcessor<Void> {
- private final Consumer<Optional<byte[]>> consumer;
- private Flow.Subscription subscription;
- private final CompletableFuture<Void> result = new MinimalFuture<>();
-
- ConsumerProcessor(Consumer<Optional<byte[]>> consumer) {
- this.consumer = consumer;
- }
-
- @Override
- public CompletionStage<Void> getBody() {
- return result;
- }
-
- @Override
- public void onSubscribe(Flow.Subscription subscription) {
- this.subscription = subscription;
- subscription.request(1);
- }
-
- @Override
- public void onNext(ByteBuffer item) {
- byte[] buf = new byte[item.remaining()];
- item.get(buf);
- consumer.accept(Optional.of(buf));
- subscription.request(1);
- }
-
- @Override
- public void onError(Throwable throwable) {
- result.completeExceptionally(throwable);
- }
-
- @Override
- public void onComplete() {
- consumer.accept(Optional.empty());
- result.complete(null);
- }
-
- }
-
- static class PathProcessor implements HttpResponse.BodyProcessor<Path> {
-
- private final Path file;
- private final CompletableFuture<Path> result = new MinimalFuture<>();
-
- private Flow.Subscription subscription;
- private FileChannel out;
- private final OpenOption[] options;
-
- PathProcessor(Path file, OpenOption... options) {
- this.file = file;
- this.options = options;
- }
-
- @Override
- public void onSubscribe(Flow.Subscription subscription) {
- this.subscription = subscription;
- try {
- out = FileChannel.open(file, options);
- } catch (IOException e) {
- result.completeExceptionally(e);
- subscription.cancel();
- return;
- }
- subscription.request(1);
- }
-
- @Override
- public void onNext(ByteBuffer item) {
- try {
- out.write(item);
- } catch (IOException ex) {
- Utils.close(out);
- subscription.cancel();
- result.completeExceptionally(ex);
- }
- subscription.request(1);
- }
-
- @Override
- public void onError(Throwable e) {
- result.completeExceptionally(e);
- Utils.close(out);
- }
-
- @Override
- public void onComplete() {
- Utils.close(out);
- result.complete(file);
- }
-
- @Override
- public CompletionStage<Path> getBody() {
- return result;
- }
- }
-
- static class ByteArrayProcessor<T> implements HttpResponse.BodyProcessor<T> {
- private final Function<byte[], T> finisher;
- private final CompletableFuture<T> result = new MinimalFuture<>();
- private final List<ByteBuffer> received = new ArrayList<>();
-
- private Flow.Subscription subscription;
-
- ByteArrayProcessor(Function<byte[],T> finisher) {
- this.finisher = finisher;
- }
-
- @Override
- public void onSubscribe(Flow.Subscription subscription) {
- if (this.subscription != null) {
- subscription.cancel();
- return;
- }
- this.subscription = subscription;
- // We can handle whatever you've got
- subscription.request(Long.MAX_VALUE);
- }
-
- @Override
- public void onNext(ByteBuffer item) {
- // incoming buffers are allocated by http client internally,
- // and won't be used anywhere except this place.
- // So it's free simply to store them for further processing.
- if(item.hasRemaining()) {
- received.add(item);
- }
- }
-
- @Override
- public void onError(Throwable throwable) {
- received.clear();
- result.completeExceptionally(throwable);
- }
-
- static private byte[] join(List<ByteBuffer> bytes) {
- int size = Utils.remaining(bytes);
- byte[] res = new byte[size];
- int from = 0;
- for (ByteBuffer b : bytes) {
- int l = b.remaining();
- b.get(res, from, l);
- from += l;
- }
- return res;
- }
-
- @Override
- public void onComplete() {
- try {
- result.complete(finisher.apply(join(received)));
- received.clear();
- } catch (IllegalArgumentException e) {
- result.completeExceptionally(e);
- }
- }
-
- @Override
- public CompletionStage<T> getBody() {
- return result;
- }
- }
-
- static class MultiProcessorImpl<V> implements HttpResponse.MultiProcessor<MultiMapResult<V>,V> {
- private final MultiMapResult<V> results;
- private final Function<HttpRequest,Optional<HttpResponse.BodyHandler<V>>> pushHandler;
- private final boolean completion; // aggregate completes on last PP received or overall completion
-
- MultiProcessorImpl(Function<HttpRequest,Optional<HttpResponse.BodyHandler<V>>> pushHandler, boolean completion) {
- this.results = new MultiMapResult<V>(new ConcurrentHashMap<>());
- this.pushHandler = pushHandler;
- this.completion = completion;
- }
-
- @Override
- public Optional<HttpResponse.BodyHandler<V>> onRequest(HttpRequest request) {
- return pushHandler.apply(request);
- }
-
- @Override
- public void onResponse(HttpResponse<V> response) {
- results.put(response.request(), CompletableFuture.completedFuture(response));
- }
-
- @Override
- public void onError(HttpRequest request, Throwable t) {
- results.put(request, MinimalFuture.failedFuture(t));
- }
-
- @Override
- public CompletableFuture<MultiMapResult<V>> completion(
- CompletableFuture<Void> onComplete, CompletableFuture<Void> onFinalPushPromise) {
- if (completion)
- return onComplete.thenApply((ignored)-> results);
- else
- return onFinalPushPromise.thenApply((ignored) -> results);
- }
- }
-
- static class MultiFile {
-
- final Path pathRoot;
-
- MultiFile(Path destination) {
- if (!destination.toFile().isDirectory())
- throw new UncheckedIOException(new IOException("destination is not a directory"));
- pathRoot = destination;
- }
-
- Optional<HttpResponse.BodyHandler<Path>> handlePush(HttpRequest request) {
- final URI uri = request.uri();
- String path = uri.getPath();
- while (path.startsWith("/"))
- path = path.substring(1);
- Path p = pathRoot.resolve(path);
- if (Log.trace()) {
- Log.logTrace("Creating file body processor for URI={0}, path={1}",
- uri, p);
- }
- try {
- Files.createDirectories(p.getParent());
- } catch (IOException ex) {
- throw new UncheckedIOException(ex);
- }
-
- final HttpResponse.BodyHandler<Path> proc =
- HttpResponse.BodyHandler.asFile(p);
-
- return Optional.of(proc);
- }
- }
-
- /**
- * Currently this consumes all of the data and ignores it
- */
- static class NullProcessor<T> implements HttpResponse.BodyProcessor<T> {
-
- Flow.Subscription subscription;
- final CompletableFuture<T> cf = new MinimalFuture<>();
- final Optional<T> result;
-
- NullProcessor(Optional<T> result) {
- this.result = result;
- }
-
- @Override
- public void onSubscribe(Flow.Subscription subscription) {
- this.subscription = subscription;
- subscription.request(Long.MAX_VALUE);
- }
-
- @Override
- public void onNext(ByteBuffer item) {
- // TODO: check whether this should consume the buffer, as in:
- item.position(item.limit());
- }
-
- @Override
- public void onError(Throwable throwable) {
- cf.completeExceptionally(throwable);
- }
-
- @Override
- public void onComplete() {
- if (result.isPresent()) {
- cf.complete(result.get());
- } else {
- cf.complete(null);
- }
- }
-
- @Override
- public CompletionStage<T> getBody() {
- return cf;
- }
- }
-}
--- /dev/null Thu Jan 01 00:00:00 1970 +0000
+++ b/src/jdk.incubator.httpclient/share/classes/jdk/incubator/http/ResponseSubscribers.java Sun Nov 05 17:32:13 2017 +0000
@@ -0,0 +1,545 @@
+/*
+ * Copyright (c) 2016, 2017, Oracle and/or its affiliates. All rights reserved.
+ * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER.
+ *
+ * This code is free software; you can redistribute it and/or modify it
+ * under the terms of the GNU General Public License version 2 only, as
+ * published by the Free Software Foundation. Oracle designates this
+ * particular file as subject to the "Classpath" exception as provided
+ * by Oracle in the LICENSE file that accompanied this code.
+ *
+ * This code is distributed in the hope that it will be useful, but WITHOUT
+ * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or
+ * FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License
+ * version 2 for more details (a copy is included in the LICENSE file that
+ * accompanied this code).
+ *
+ * You should have received a copy of the GNU General Public License version
+ * 2 along with this work; if not, write to the Free Software Foundation,
+ * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA.
+ *
+ * Please contact Oracle, 500 Oracle Parkway, Redwood Shores, CA 94065 USA
+ * or visit www.oracle.com if you need additional information or have any
+ * questions.
+ */
+
+package jdk.incubator.http;
+
+import java.io.IOException;
+import java.io.InputStream;
+import java.io.UncheckedIOException;
+import java.lang.System.Logger.Level;
+import java.net.URI;
+import java.nio.ByteBuffer;
+import java.nio.channels.FileChannel;
+import java.nio.file.Files;
+import java.nio.file.OpenOption;
+import java.nio.file.Path;
+import java.security.AccessControlContext;
+import java.security.AccessController;
+import java.security.PrivilegedActionException;
+import java.security.PrivilegedExceptionAction;
+import java.util.ArrayList;
+import java.util.Iterator;
+import java.util.List;
+import java.util.Optional;
+import java.util.concurrent.ArrayBlockingQueue;
+import java.util.concurrent.BlockingQueue;
+import java.util.concurrent.CompletableFuture;
+import java.util.concurrent.CompletionStage;
+import java.util.concurrent.ConcurrentHashMap;
+import java.util.concurrent.Flow;
+import java.util.function.Consumer;
+import java.util.function.Function;
+import jdk.incubator.http.internal.common.MinimalFuture;
+import jdk.incubator.http.internal.common.Utils;
+import jdk.incubator.http.internal.common.Log;
+
+class ResponseSubscribers {
+
+ static class ConsumerSubscriber implements HttpResponse.BodySubscriber<Void> {
+ private final Consumer<Optional<byte[]>> consumer;
+ private Flow.Subscription subscription;
+ private final CompletableFuture<Void> result = new MinimalFuture<>();
+
+ ConsumerSubscriber(Consumer<Optional<byte[]>> consumer) {
+ this.consumer = consumer;
+ }
+
+ @Override
+ public CompletionStage<Void> getBody() {
+ return result;
+ }
+
+ @Override
+ public void onSubscribe(Flow.Subscription subscription) {
+ this.subscription = subscription;
+ subscription.request(1);
+ }
+
+ @Override
+ public void onNext(List<ByteBuffer> items) {
+ for (ByteBuffer item : items) {
+ byte[] buf = new byte[item.remaining()];
+ item.get(buf);
+ consumer.accept(Optional.of(buf));
+ }
+ subscription.request(1);
+ }
+
+ @Override
+ public void onError(Throwable throwable) {
+ result.completeExceptionally(throwable);
+ }
+
+ @Override
+ public void onComplete() {
+ consumer.accept(Optional.empty());
+ result.complete(null);
+ }
+
+ }
+
+ static class PathSubscriber implements HttpResponse.BodySubscriber<Path> {
+
+ private final Path file;
+ private final CompletableFuture<Path> result = new MinimalFuture<>();
+
+ private volatile Flow.Subscription subscription;
+ private volatile FileChannel out;
+ private volatile AccessControlContext acc;
+ private final OpenOption[] options;
+
+ PathSubscriber(Path file, OpenOption... options) {
+ this.file = file;
+ this.options = options;
+ }
+
+ void setAccessControlContext(AccessControlContext acc) {
+ this.acc = acc;
+ }
+
+ @Override
+ public void onSubscribe(Flow.Subscription subscription) {
+ if (System.getSecurityManager() != null && acc == null)
+ throw new InternalError(
+ "Unexpected null acc when security manager has been installed");
+
+ this.subscription = subscription;
+ try {
+ PrivilegedExceptionAction<FileChannel> pa =
+ () -> FileChannel.open(file, options);
+ out = AccessController.doPrivileged(pa, acc);
+ } catch (PrivilegedActionException pae) {
+ Throwable t = pae.getCause() != null ? pae.getCause() : pae;
+ result.completeExceptionally(t);
+ subscription.cancel();
+ return;
+ }
+ subscription.request(1);
+ }
+
+ @Override
+ public void onNext(List<ByteBuffer> items) {
+ try {
+ out.write(items.toArray(new ByteBuffer[0]));
+ } catch (IOException ex) {
+ Utils.close(out);
+ subscription.cancel();
+ result.completeExceptionally(ex);
+ }
+ subscription.request(1);
+ }
+
+ @Override
+ public void onError(Throwable e) {
+ result.completeExceptionally(e);
+ Utils.close(out);
+ }
+
+ @Override
+ public void onComplete() {
+ Utils.close(out);
+ result.complete(file);
+ }
+
+ @Override
+ public CompletionStage<Path> getBody() {
+ return result;
+ }
+ }
+
+ static class ByteArraySubscriber<T> implements HttpResponse.BodySubscriber<T> {
+ private final Function<byte[], T> finisher;
+ private final CompletableFuture<T> result = new MinimalFuture<>();
+ private final List<ByteBuffer> received = new ArrayList<>();
+
+ private volatile Flow.Subscription subscription;
+
+ ByteArraySubscriber(Function<byte[],T> finisher) {
+ this.finisher = finisher;
+ }
+
+ @Override
+ public void onSubscribe(Flow.Subscription subscription) {
+ if (this.subscription != null) {
+ subscription.cancel();
+ return;
+ }
+ this.subscription = subscription;
+ // We can handle whatever you've got
+ subscription.request(Long.MAX_VALUE);
+ }
+
+ @Override
+ public void onNext(List<ByteBuffer> items) {
+ // incoming buffers are allocated by http client internally,
+ // and won't be used anywhere except this place.
+ // So it's free simply to store them for further processing.
+ assert Utils.hasRemaining(items);
+ Utils.accumulateBuffers(received, items);
+ }
+
+ @Override
+ public void onError(Throwable throwable) {
+ received.clear();
+ result.completeExceptionally(throwable);
+ }
+
+ static private byte[] join(List<ByteBuffer> bytes) {
+ int size = Utils.remaining(bytes, Integer.MAX_VALUE);
+ byte[] res = new byte[size];
+ int from = 0;
+ for (ByteBuffer b : bytes) {
+ int l = b.remaining();
+ b.get(res, from, l);
+ from += l;
+ }
+ return res;
+ }
+
+ @Override
+ public void onComplete() {
+ try {
+ result.complete(finisher.apply(join(received)));
+ received.clear();
+ } catch (IllegalArgumentException e) {
+ result.completeExceptionally(e);
+ }
+ }
+
+ @Override
+ public CompletionStage<T> getBody() {
+ return result;
+ }
+ }
+
+ /**
+ * An InputStream built on top of the Flow API.
+ */
+ static class HttpResponseInputStream extends InputStream
+ implements HttpResponse.BodySubscriber<InputStream>
+ {
+ final static boolean DEBUG = Utils.DEBUG;
+ final static int MAX_BUFFERS_IN_QUEUE = 1; // lock-step with the producer
+
+ // An immutable ByteBuffer sentinel to mark that the last byte was received.
+ private static final ByteBuffer LAST_BUFFER = ByteBuffer.wrap(new byte[0]);
+ private static final List<ByteBuffer> LAST_LIST = List.of(LAST_BUFFER);
+ private static final System.Logger DEBUG_LOGGER =
+ Utils.getDebugLogger("HttpResponseInputStream"::toString, DEBUG);
+
+ // A queue of yet unprocessed ByteBuffers received from the flow API.
+ private final BlockingQueue<List<ByteBuffer>> buffers;
+ private volatile Flow.Subscription subscription;
+ private volatile boolean closed;
+ private volatile Throwable failed;
+ private volatile Iterator<ByteBuffer> currentListItr;
+ private volatile ByteBuffer currentBuffer;
+
+ HttpResponseInputStream() {
+ this(MAX_BUFFERS_IN_QUEUE);
+ }
+
+ HttpResponseInputStream(int maxBuffers) {
+ int capacity = (maxBuffers <= 0 ? MAX_BUFFERS_IN_QUEUE : maxBuffers);
+ // 1 additional slot needed for LAST_LIST added by onComplete
+ this.buffers = new ArrayBlockingQueue<>(capacity + 1);
+ }
+
+ @Override
+ public CompletionStage<InputStream> getBody() {
+ // Returns the stream immediately, before the
+ // response body is received.
+ // This makes it possible for senAsync().get().body()
+ // to complete before the response body is received.
+ return CompletableFuture.completedStage(this);
+ }
+
+ // Returns the current byte buffer to read from.
+ // If the current buffer has no remaining data, this method will take the
+ // next buffer from the buffers queue, possibly blocking until
+ // a new buffer is made available through the Flow API, or the
+ // end of the flow has been reached.
+ private ByteBuffer current() throws IOException {
+ while (currentBuffer == null || !currentBuffer.hasRemaining()) {
+ // Check whether the stream is closed or exhausted
+ if (closed || failed != null) {
+ throw new IOException("closed", failed);
+ }
+ if (currentBuffer == LAST_BUFFER) break;
+
+ try {
+ if (currentListItr == null || !currentListItr.hasNext()) {
+ // Take a new list of buffers from the queue, blocking
+ // if none is available yet...
+
+ DEBUG_LOGGER.log(Level.DEBUG, "Taking list of Buffers");
+ List<ByteBuffer> lb = buffers.take();
+ currentListItr = lb.iterator();
+ DEBUG_LOGGER.log(Level.DEBUG, "List of Buffers Taken");
+
+ // Check whether an exception was encountered upstream
+ if (closed || failed != null)
+ throw new IOException("closed", failed);
+
+ // Check whether we're done.
+ if (lb == LAST_LIST) {
+ currentListItr = null;
+ currentBuffer = LAST_BUFFER;
+ break;
+ }
+
+ // Request another upstream item ( list of buffers )
+ Flow.Subscription s = subscription;
+ if (s != null) {
+ DEBUG_LOGGER.log(Level.DEBUG, "Increased demand by 1");
+ s.request(1);
+ }
+ }
+ assert currentListItr != null;
+ assert currentListItr.hasNext();
+ DEBUG_LOGGER.log(Level.DEBUG, "Next Buffer");
+ currentBuffer = currentListItr.next();
+ } catch (InterruptedException ex) {
+ // continue
+ }
+ }
+ assert currentBuffer == LAST_BUFFER || currentBuffer.hasRemaining();
+ return currentBuffer;
+ }
+
+ @Override
+ public int read(byte[] bytes, int off, int len) throws IOException {
+ // get the buffer to read from, possibly blocking if
+ // none is available
+ ByteBuffer buffer;
+ if ((buffer = current()) == LAST_BUFFER) return -1;
+
+ // don't attempt to read more than what is available
+ // in the current buffer.
+ int read = Math.min(buffer.remaining(), len);
+ assert read > 0 && read <= buffer.remaining();
+
+ // buffer.get() will do the boundary check for us.
+ buffer.get(bytes, off, read);
+ return read;
+ }
+
+ @Override
+ public int read() throws IOException {
+ ByteBuffer buffer;
+ if ((buffer = current()) == LAST_BUFFER) return -1;
+ return buffer.get() & 0xFF;
+ }
+
+ @Override
+ public void onSubscribe(Flow.Subscription s) {
+ if (this.subscription != null) {
+ s.cancel();
+ return;
+ }
+ this.subscription = s;
+ assert buffers.remainingCapacity() > 1; // should contain at least 2
+ DEBUG_LOGGER.log(Level.DEBUG, () -> "onSubscribe: requesting "
+ + Math.max(1, buffers.remainingCapacity() - 1));
+ s.request(Math.max(1, buffers.remainingCapacity() - 1));
+ }
+
+ @Override
+ public void onNext(List<ByteBuffer> t) {
+ try {
+ DEBUG_LOGGER.log(Level.DEBUG, "next item received");
+ if (!buffers.offer(t)) {
+ throw new IllegalStateException("queue is full");
+ }
+ DEBUG_LOGGER.log(Level.DEBUG, "item offered");
+ } catch (Exception ex) {
+ failed = ex;
+ try {
+ close();
+ } catch (IOException ex1) {
+ // OK
+ }
+ }
+ }
+
+ @Override
+ public void onError(Throwable thrwbl) {
+ subscription = null;
+ failed = thrwbl;
+ }
+
+ @Override
+ public void onComplete() {
+ subscription = null;
+ onNext(LAST_LIST);
+ }
+
+ @Override
+ public void close() throws IOException {
+ synchronized (this) {
+ if (closed) return;
+ closed = true;
+ }
+ Flow.Subscription s = subscription;
+ subscription = null;
+ if (s != null) {
+ s.cancel();
+ }
+ super.close();
+ }
+
+ }
+
+ static class MultiSubscriberImpl<V>
+ implements HttpResponse.MultiSubscriber<MultiMapResult<V>,V>
+ {
+ private final MultiMapResult<V> results;
+ private final Function<HttpRequest,Optional<HttpResponse.BodyHandler<V>>> pushHandler;
+ private final Function<HttpRequest,HttpResponse.BodyHandler<V>> requestHandler;
+ private final boolean completion; // aggregate completes on last PP received or overall completion
+
+ MultiSubscriberImpl(
+ Function<HttpRequest,HttpResponse.BodyHandler<V>> requestHandler,
+ Function<HttpRequest,Optional<HttpResponse.BodyHandler<V>>> pushHandler, boolean completion) {
+ this.results = new MultiMapResult<>(new ConcurrentHashMap<>());
+ this.requestHandler = requestHandler;
+ this.pushHandler = pushHandler;
+ this.completion = completion;
+ }
+
+ @Override
+ public HttpResponse.BodyHandler<V> onRequest(HttpRequest request) {
+ CompletableFuture<HttpResponse<V>> cf = MinimalFuture.newMinimalFuture();
+ results.put(request, cf);
+ return requestHandler.apply(request);
+ }
+
+ @Override
+ public Optional<HttpResponse.BodyHandler<V>> onPushPromise(HttpRequest push) {
+ CompletableFuture<HttpResponse<V>> cf = MinimalFuture.newMinimalFuture();
+ results.put(push, cf);
+ return pushHandler.apply(push);
+ }
+
+ @Override
+ public void onResponse(HttpResponse<V> response) {
+ HttpRequest request = response.request();
+ CompletableFuture<HttpResponse<V>> cf = results.get(response.request());
+ cf.complete(response);
+ }
+
+ @Override
+ public void onError(HttpRequest request, Throwable t) {
+ CompletableFuture<HttpResponse<V>> cf = results.get(request);
+ cf.completeExceptionally(t);
+ }
+
+ @Override
+ public CompletableFuture<MultiMapResult<V>> completion(
+ CompletableFuture<Void> onComplete, CompletableFuture<Void> onFinalPushPromise) {
+ if (completion)
+ return onComplete.thenApply((ignored)-> results);
+ else
+ return onFinalPushPromise.thenApply((ignored) -> results);
+ }
+ }
+
+ static class MultiFile {
+
+ final Path pathRoot;
+
+ MultiFile(Path destination) {
+ if (!destination.toFile().isDirectory())
+ throw new UncheckedIOException(new IOException("destination is not a directory"));
+ pathRoot = destination;
+ }
+
+ Optional<HttpResponse.BodyHandler<Path>> handlePush(HttpRequest request) {
+ final URI uri = request.uri();
+ String path = uri.getPath();
+ while (path.startsWith("/"))
+ path = path.substring(1);
+ Path p = pathRoot.resolve(path);
+ if (Log.trace()) {
+ Log.logTrace("Creating file body subscriber for URI={0}, path={1}",
+ uri, p);
+ }
+ try {
+ Files.createDirectories(p.getParent());
+ } catch (IOException ex) {
+ throw new UncheckedIOException(ex);
+ }
+
+ final HttpResponse.BodyHandler<Path> proc =
+ HttpResponse.BodyHandler.asFile(p);
+
+ return Optional.of(proc);
+ }
+ }
+
+ /**
+ * Currently this consumes all of the data and ignores it
+ */
+ static class NullSubscriber<T> implements HttpResponse.BodySubscriber<T> {
+
+ volatile Flow.Subscription subscription;
+ final CompletableFuture<T> cf = new MinimalFuture<>();
+ final Optional<T> result;
+
+ NullSubscriber(Optional<T> result) {
+ this.result = result;
+ }
+
+ @Override
+ public void onSubscribe(Flow.Subscription subscription) {
+ this.subscription = subscription;
+ subscription.request(Long.MAX_VALUE);
+ }
+
+ @Override
+ public void onNext(List<ByteBuffer> items) {
+ // NO-OP
+ }
+
+ @Override
+ public void onError(Throwable throwable) {
+ cf.completeExceptionally(throwable);
+ }
+
+ @Override
+ public void onComplete() {
+ if (result.isPresent()) {
+ cf.complete(result.get());
+ } else {
+ cf.complete(null);
+ }
+ }
+
+ @Override
+ public CompletionStage<T> getBody() {
+ return cf;
+ }
+ }
+}
--- a/src/jdk.incubator.httpclient/share/classes/jdk/incubator/http/SSLConnection.java Sun Nov 05 17:05:57 2017 +0000
+++ /dev/null Thu Jan 01 00:00:00 1970 +0000
@@ -1,205 +0,0 @@
-/*
- * Copyright (c) 2015, 2017, Oracle and/or its affiliates. All rights reserved.
- * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER.
- *
- * This code is free software; you can redistribute it and/or modify it
- * under the terms of the GNU General Public License version 2 only, as
- * published by the Free Software Foundation. Oracle designates this
- * particular file as subject to the "Classpath" exception as provided
- * by Oracle in the LICENSE file that accompanied this code.
- *
- * This code is distributed in the hope that it will be useful, but WITHOUT
- * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or
- * FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License
- * version 2 for more details (a copy is included in the LICENSE file that
- * accompanied this code).
- *
- * You should have received a copy of the GNU General Public License version
- * 2 along with this work; if not, write to the Free Software Foundation,
- * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA.
- *
- * Please contact Oracle, 500 Oracle Parkway, Redwood Shores, CA 94065 USA
- * or visit www.oracle.com if you need additional information or have any
- * questions.
- */
-
-package jdk.incubator.http;
-
-import java.io.IOException;
-import java.net.InetSocketAddress;
-import java.nio.ByteBuffer;
-import java.nio.channels.SocketChannel;
-import java.util.concurrent.CompletableFuture;
-import javax.net.ssl.SSLEngineResult.Status;
-import javax.net.ssl.SSLParameters;
-import jdk.incubator.http.SSLDelegate.WrapperResult;
-
-import jdk.incubator.http.internal.common.ByteBufferReference;
-import jdk.incubator.http.internal.common.MinimalFuture;
-import jdk.incubator.http.internal.common.Utils;
-
-/**
- * An SSL connection built on a Plain TCP connection.
- */
-class SSLConnection extends HttpConnection {
-
- PlainHttpConnection delegate;
- SSLDelegate sslDelegate;
- final String[] alpn;
- final String serverName;
-
- @Override
- public CompletableFuture<Void> connectAsync() {
- return delegate.connectAsync()
- .thenCompose((Void v) ->
- MinimalFuture.supply( () -> {
- this.sslDelegate = new SSLDelegate(delegate.channel(), client, alpn, serverName);
- return null;
- }));
- }
-
- @Override
- public void connect() throws IOException {
- delegate.connect();
- this.sslDelegate = new SSLDelegate(delegate.channel(), client, alpn, serverName);
- }
-
- SSLConnection(InetSocketAddress addr, HttpClientImpl client, String[] ap) {
- super(addr, client);
- this.alpn = ap;
- this.serverName = Utils.getServerName(addr);
- delegate = new PlainHttpConnection(addr, client);
- }
-
- /**
- * Create an SSLConnection from an existing connected AsyncSSLConnection.
- * Used when downgrading from HTTP/2 to HTTP/1.1
- */
- SSLConnection(AsyncSSLConnection c) {
- super(c.address, c.client);
- this.delegate = c.plainConnection();
- AsyncSSLDelegate adel = c.sslDelegate();
- this.sslDelegate = new SSLDelegate(adel.engine, delegate.channel(), client, adel.serverName);
- this.alpn = adel.alpn;
- this.serverName = adel.serverName;
- }
-
- @Override
- SSLParameters sslParameters() {
- return sslDelegate.getSSLParameters();
- }
-
- @Override
- public String toString() {
- return "SSLConnection: " + super.toString();
- }
-
- private static long countBytes(ByteBuffer[] buffers, int start, int length) {
- long c = 0;
- for (int i=0; i<length; i++) {
- c+= buffers[start+i].remaining();
- }
- return c;
- }
-
- @Override
- ConnectionPool.CacheKey cacheKey() {
- return ConnectionPool.cacheKey(address, null);
- }
-
- @Override
- long write(ByteBuffer[] buffers, int start, int number) throws IOException {
- //debugPrint("Send", buffers, start, number);
- long l = countBytes(buffers, start, number);
- WrapperResult r = sslDelegate.sendData(buffers, start, number);
- if (r.result.getStatus() == Status.CLOSED) {
- if (l > 0) {
- throw new IOException("SSLHttpConnection closed");
- }
- }
- return l;
- }
-
- @Override
- long write(ByteBuffer buffer) throws IOException {
- //debugPrint("Send", buffer);
- long l = buffer.remaining();
- WrapperResult r = sslDelegate.sendData(buffer);
- if (r.result.getStatus() == Status.CLOSED) {
- if (l > 0) {
- throw new IOException("SSLHttpConnection closed");
- }
- }
- return l;
- }
-
- @Override
- void writeAsync(ByteBufferReference[] buffers) throws IOException {
- write(ByteBufferReference.toBuffers(buffers), 0, buffers.length);
- }
-
- @Override
- void writeAsyncUnordered(ByteBufferReference[] buffers) throws IOException {
- write(ByteBufferReference.toBuffers(buffers), 0, buffers.length);
- }
-
- @Override
- void flushAsync() throws IOException {
- // nothing to do
- }
-
- @Override
- public void close() {
- Utils.close(delegate.channel());
- }
-
- @Override
- void shutdownInput() throws IOException {
- delegate.channel().shutdownInput();
- }
-
- @Override
- void shutdownOutput() throws IOException {
- delegate.channel().shutdownOutput();
- }
-
- @Override
- protected ByteBuffer readImpl() throws IOException {
- WrapperResult r = sslDelegate.recvData(ByteBuffer.allocate(8192));
- // TODO: check for closure
- int n = r.result.bytesProduced();
- if (n > 0) {
- return r.buf;
- } else if (n == 0) {
- return Utils.EMPTY_BYTEBUFFER;
- } else {
- return null;
- }
- }
-
- @Override
- boolean connected() {
- return delegate.connected();
- }
-
- @Override
- SocketChannel channel() {
- return delegate.channel();
- }
-
- @Override
- CompletableFuture<Void> whenReceivingResponse() {
- return delegate.whenReceivingResponse();
- }
-
- @Override
- boolean isSecure() {
- return true;
- }
-
- @Override
- boolean isProxied() {
- return false;
- }
-
-}
--- a/src/jdk.incubator.httpclient/share/classes/jdk/incubator/http/SSLDelegate.java Sun Nov 05 17:05:57 2017 +0000
+++ b/src/jdk.incubator.httpclient/share/classes/jdk/incubator/http/SSLDelegate.java Sun Nov 05 17:32:13 2017 +0000
@@ -41,7 +41,10 @@
/**
* Implements the mechanics of SSL by managing an SSLEngine object.
- * One of these is associated with each SSLConnection.
+ * <p>
+ * This class is only used to implement the {@link
+ * AbstractAsyncSSLConnection.SSLConnectionChannel} which is handed of
+ * to RawChannelImpl when creating a WebSocket.
*/
class SSLDelegate {
@@ -71,8 +74,7 @@
SSLContext context = client.sslContext();
engine = context.createSSLEngine();
engine.setUseClientMode(true);
- SSLParameters sslp = client.sslParameters()
- .orElseGet(context::getSupportedSSLParameters);
+ SSLParameters sslp = client.sslParameters();
sslParameters = Utils.copySSLParameters(sslp);
if (sn != null) {
SNIHostName sni = new SNIHostName(sn);
@@ -94,7 +96,7 @@
return sslParameters;
}
- private static long countBytes(ByteBuffer[] buffers, int start, int number) {
+ static long countBytes(ByteBuffer[] buffers, int start, int number) {
long c = 0;
for (int i=0; i<number; i++) {
c+= buffers[start+i].remaining();
@@ -191,7 +193,8 @@
SocketChannel chan;
SSLEngine engine;
- Object wrapLock, unwrapLock;
+ final Object wrapLock;
+ final Object unwrapLock;
ByteBuffer unwrap_src, wrap_dst;
boolean closed = false;
int u_remaining; // the number of bytes left in unwrap_src after an unwrap()
@@ -407,7 +410,7 @@
*/
@SuppressWarnings("fallthrough")
void doHandshake (HandshakeStatus hs_status) throws IOException {
- boolean wasBlocking = false;
+ boolean wasBlocking;
try {
wasBlocking = chan.isBlocking();
handshaking.lock();
--- a/src/jdk.incubator.httpclient/share/classes/jdk/incubator/http/SSLTunnelConnection.java Sun Nov 05 17:05:57 2017 +0000
+++ /dev/null Thu Jan 01 00:00:00 1970 +0000
@@ -1,207 +0,0 @@
-/*
- * Copyright (c) 2015, 2017, Oracle and/or its affiliates. All rights reserved.
- * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER.
- *
- * This code is free software; you can redistribute it and/or modify it
- * under the terms of the GNU General Public License version 2 only, as
- * published by the Free Software Foundation. Oracle designates this
- * particular file as subject to the "Classpath" exception as provided
- * by Oracle in the LICENSE file that accompanied this code.
- *
- * This code is distributed in the hope that it will be useful, but WITHOUT
- * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or
- * FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License
- * version 2 for more details (a copy is included in the LICENSE file that
- * accompanied this code).
- *
- * You should have received a copy of the GNU General Public License version
- * 2 along with this work; if not, write to the Free Software Foundation,
- * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA.
- *
- * Please contact Oracle, 500 Oracle Parkway, Redwood Shores, CA 94065 USA
- * or visit www.oracle.com if you need additional information or have any
- * questions.
- */
-
-package jdk.incubator.http;
-
-import java.io.IOException;
-import java.io.UncheckedIOException;
-import java.net.InetSocketAddress;
-import java.nio.ByteBuffer;
-import java.nio.channels.SocketChannel;
-import java.util.concurrent.CompletableFuture;
-import javax.net.ssl.SSLEngineResult.Status;
-import javax.net.ssl.SSLParameters;
-import jdk.incubator.http.SSLDelegate.WrapperResult;
-
-import jdk.incubator.http.internal.common.ByteBufferReference;
-import jdk.incubator.http.internal.common.Utils;
-
-/**
- * An SSL tunnel built on a Plain (CONNECT) TCP tunnel.
- */
-class SSLTunnelConnection extends HttpConnection {
-
- final PlainTunnelingConnection delegate;
- protected SSLDelegate sslDelegate;
- private volatile boolean connected;
- final String serverName;
-
- @Override
- public void connect() throws IOException, InterruptedException {
- delegate.connect();
- this.sslDelegate = new SSLDelegate(delegate.channel(), client, null, serverName);
- connected = true;
- }
-
- @Override
- boolean connected() {
- return connected && delegate.connected();
- }
-
- @Override
- public CompletableFuture<Void> connectAsync() {
- return delegate.connectAsync()
- .thenAccept((Void v) -> {
- try {
- // can this block?
- this.sslDelegate = new SSLDelegate(delegate.channel(),
- client,
- null, serverName);
- connected = true;
- } catch (IOException e) {
- throw new UncheckedIOException(e);
- }
- });
- }
-
- SSLTunnelConnection(InetSocketAddress addr,
- HttpClientImpl client,
- InetSocketAddress proxy)
- {
- super(addr, client);
- this.serverName = Utils.getServerName(addr);
- delegate = new PlainTunnelingConnection(addr, proxy, client);
- }
-
- /**
- * Create an SSLTunnelConnection from an existing connected AsyncSSLTunnelConnection.
- * Used when downgrading from HTTP/2 to HTTP/1.1
- */
- SSLTunnelConnection(AsyncSSLTunnelConnection c) {
- super(c.address, c.client);
- this.delegate = c.plainConnection();
- AsyncSSLDelegate adel = c.sslDelegate();
- this.sslDelegate = new SSLDelegate(adel.engine, delegate.channel(), client, adel.serverName);
- this.serverName = adel.serverName;
- connected = c.connected();
- }
-
- @Override
- SSLParameters sslParameters() {
- return sslDelegate.getSSLParameters();
- }
-
- @Override
- public String toString() {
- return "SSLTunnelConnection: " + super.toString();
- }
-
- private static long countBytes(ByteBuffer[] buffers, int start, int number) {
- long c = 0;
- for (int i=0; i<number; i++) {
- c+= buffers[start+i].remaining();
- }
- return c;
- }
-
- @Override
- ConnectionPool.CacheKey cacheKey() {
- return ConnectionPool.cacheKey(address, delegate.proxyAddr);
- }
-
- @Override
- long write(ByteBuffer[] buffers, int start, int number) throws IOException {
- //debugPrint("Send", buffers, start, number);
- long l = countBytes(buffers, start, number);
- WrapperResult r = sslDelegate.sendData(buffers, start, number);
- if (r.result.getStatus() == Status.CLOSED) {
- if (l > 0) {
- throw new IOException("SSLHttpConnection closed");
- }
- }
- return l;
- }
-
- @Override
- long write(ByteBuffer buffer) throws IOException {
- //debugPrint("Send", buffer);
- long l = buffer.remaining();
- WrapperResult r = sslDelegate.sendData(buffer);
- if (r.result.getStatus() == Status.CLOSED) {
- if (l > 0) {
- throw new IOException("SSLHttpConnection closed");
- }
- }
- return l;
- }
-
- @Override
- void writeAsync(ByteBufferReference[] buffers) throws IOException {
- write(ByteBufferReference.toBuffers(buffers), 0, buffers.length);
- }
-
- @Override
- void writeAsyncUnordered(ByteBufferReference[] buffers) throws IOException {
- write(ByteBufferReference.toBuffers(buffers), 0, buffers.length);
- }
-
- @Override
- void flushAsync() throws IOException {
- // nothing to do
- }
-
- @Override
- public void close() {
- Utils.close(delegate.channel());
- }
-
- @Override
- void shutdownInput() throws IOException {
- delegate.channel().shutdownInput();
- }
-
- @Override
- void shutdownOutput() throws IOException {
- delegate.channel().shutdownOutput();
- }
-
- @Override
- protected ByteBuffer readImpl() throws IOException {
- ByteBuffer buf = Utils.getBuffer();
-
- WrapperResult r = sslDelegate.recvData(buf);
- return r.buf;
- }
-
- @Override
- SocketChannel channel() {
- return delegate.channel();
- }
-
- @Override
- CompletableFuture<Void> whenReceivingResponse() {
- return delegate.whenReceivingResponse();
- }
-
- @Override
- boolean isSecure() {
- return true;
- }
-
- @Override
- boolean isProxied() {
- return true;
- }
-}
--- /dev/null Thu Jan 01 00:00:00 1970 +0000
+++ b/src/jdk.incubator.httpclient/share/classes/jdk/incubator/http/SocketTube.java Sun Nov 05 17:32:13 2017 +0000
@@ -0,0 +1,956 @@
+/*
+ * Copyright (c) 2017, Oracle and/or its affiliates. All rights reserved.
+ * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER.
+ *
+ * This code is free software; you can redistribute it and/or modify it
+ * under the terms of the GNU General Public License version 2 only, as
+ * published by the Free Software Foundation. Oracle designates this
+ * particular file as subject to the "Classpath" exception as provided
+ * by Oracle in the LICENSE file that accompanied this code.
+ *
+ * This code is distributed in the hope that it will be useful, but WITHOUT
+ * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or
+ * FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License
+ * version 2 for more details (a copy is included in the LICENSE file that
+ * accompanied this code).
+ *
+ * You should have received a copy of the GNU General Public License version
+ * 2 along with this work; if not, write to the Free Software Foundation,
+ * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA.
+ *
+ * Please contact Oracle, 500 Oracle Parkway, Redwood Shores, CA 94065 USA
+ * or visit www.oracle.com if you need additional information or have any
+ * questions.
+ */
+
+package jdk.incubator.http;
+
+import java.io.EOFException;
+import java.io.IOException;
+import java.lang.System.Logger.Level;
+import java.nio.ByteBuffer;
+import java.util.List;
+import java.util.Objects;
+import java.util.concurrent.Flow;
+import java.util.concurrent.atomic.AtomicLong;
+import java.util.concurrent.atomic.AtomicReference;
+import java.nio.channels.SelectableChannel;
+import java.nio.channels.SelectionKey;
+import java.nio.channels.SocketChannel;
+import java.util.ArrayList;
+import java.util.function.Consumer;
+import java.util.function.Supplier;
+
+import jdk.incubator.http.internal.common.Demand;
+import jdk.incubator.http.internal.common.FlowTube;
+import jdk.incubator.http.internal.common.SequentialScheduler;
+import jdk.incubator.http.internal.common.SequentialScheduler.DeferredCompleter;
+import jdk.incubator.http.internal.common.SequentialScheduler.RestartableTask;
+import jdk.incubator.http.internal.common.Utils;
+
+/**
+ * A SocketTube is a terminal tube plugged directly into the socket.
+ * The read subscriber should call {@code subscribe} on the SocketTube before
+ * the SocketTube can be subscribed to the write publisher.
+ */
+final class SocketTube implements FlowTube {
+
+ static final boolean DEBUG = Utils.DEBUG; // revisit: temporary developer's flag
+ final System.Logger debug = Utils.getDebugLogger(this::dbgString, DEBUG);
+ static final AtomicLong IDS = new AtomicLong();
+
+ private final HttpClientImpl client;
+ private final SocketChannel channel;
+ private final Supplier<ByteBuffer> buffersSource;
+ private final Object lock = new Object();
+ private final AtomicReference<Throwable> errorRef = new AtomicReference<>();
+ private final InternalReadPublisher readPublisher;
+ private final InternalWriteSubscriber writeSubscriber;
+ private final long id = IDS.incrementAndGet();
+
+ public SocketTube(HttpClientImpl client, SocketChannel channel,
+ Supplier<ByteBuffer> buffersSource) {
+ this.client = client;
+ this.channel = channel;
+ this.buffersSource = buffersSource;
+ this.readPublisher = new InternalReadPublisher();
+ this.writeSubscriber = new InternalWriteSubscriber();
+ }
+
+ private static Flow.Subscription nopSubscription() {
+ return new Flow.Subscription() {
+ @Override public void request(long n) { }
+ @Override public void cancel() { }
+ };
+ }
+
+ /**
+ * Returns {@code true} if this flow is finished.
+ * This happens when this flow internal read subscription is completed,
+ * either normally (EOF reading) or exceptionally (EOF writing, or
+ * underlying socket closed, or some exception occurred while reading or
+ * writing to the socket).
+ *
+ * @return {@code true} if this flow is finished.
+ */
+ public boolean isFinished() {
+ InternalReadPublisher.InternalReadSubscription subscription =
+ readPublisher.subscriptionImpl;
+ return subscription != null && subscription.completed
+ || subscription == null && errorRef.get() != null;
+ }
+
+ // ===================================================================== //
+ // Flow.Publisher //
+ // ======================================================================//
+
+ /**
+ * {@inheritDoc }
+ * @apiNote This method should be called first. In particular, the caller
+ * must ensure that this method must be called by the read
+ * subscriber before the write publisher can call {@code onSubscribe}.
+ * Failure to adhere to this contract may result in assertion errors.
+ */
+ @Override
+ public void subscribe(Flow.Subscriber<? super List<ByteBuffer>> s) {
+ Objects.requireNonNull(s);
+ assert s instanceof TubeSubscriber : "Expected TubeSubscriber, got:" + s;
+ readPublisher.subscribe(s);
+ }
+
+
+ // ===================================================================== //
+ // Flow.Subscriber //
+ // ======================================================================//
+
+ /**
+ * {@inheritDoc }
+ * @apiNote The caller must ensure that {@code subscribe} is called by
+ * the read subscriber before {@code onSubscribe} is called by
+ * the write publisher.
+ * Failure to adhere to this contract may result in assertion errors.
+ */
+ @Override
+ public void onSubscribe(Flow.Subscription subscription) {
+ writeSubscriber.onSubscribe(subscription);
+ }
+
+ @Override
+ public void onNext(List<ByteBuffer> item) {
+ writeSubscriber.onNext(item);
+ }
+
+ @Override
+ public void onError(Throwable throwable) {
+ writeSubscriber.onError(throwable);
+ }
+
+ @Override
+ public void onComplete() {
+ writeSubscriber.onComplete();
+ }
+
+ // ===================================================================== //
+ // Events //
+ // ======================================================================//
+
+ /**
+ * A restartable task used to process tasks in sequence.
+ */
+ private static class SocketFlowTask implements RestartableTask {
+ final Runnable task;
+ private final Object monitor = new Object();
+ SocketFlowTask(Runnable task) {
+ this.task = task;
+ }
+ @Override
+ public final void run(DeferredCompleter taskCompleter) {
+ try {
+ // non contentious synchronized for visibility.
+ synchronized(monitor) {
+ task.run();
+ }
+ } finally {
+ taskCompleter.complete();
+ }
+ }
+ }
+
+ // This is best effort - there's no guarantee that the printed set
+ // of values is consistent. It should only be considered as
+ // weakly accurate - in particular in what concerns the events states,
+ // especially when displaying a read event state from a write event
+ // callback and conversely.
+ void debugState(String when) {
+ if (debug.isLoggable(Level.DEBUG)) {
+ StringBuilder state = new StringBuilder();
+
+ InternalReadPublisher.InternalReadSubscription sub =
+ readPublisher.subscriptionImpl;
+ InternalReadPublisher.ReadEvent readEvent =
+ sub == null ? null : sub.readEvent;
+ Demand rdemand = sub == null ? null : sub.demand;
+ InternalWriteSubscriber.WriteEvent writeEvent =
+ writeSubscriber.writeEvent;
+ AtomicLong wdemand = writeSubscriber.writeDemand;
+ int rops = readEvent == null ? 0 : readEvent.interestOps();
+ long rd = rdemand == null ? 0 : rdemand.get();
+ int wops = writeEvent == null ? 0 : writeEvent.interestOps();
+ long wd = wdemand == null ? 0 : wdemand.get();
+
+ state.append(when).append(" Reading: [ops=")
+ .append(rops).append(", demand=").append(rd)
+ .append(", stopped=")
+ .append((sub == null ? false : sub.readScheduler.isStopped()))
+ .append("], Writing: [ops=").append(wops)
+ .append(", demand=").append(wd)
+ .append("]");
+ debug.log(Level.DEBUG, state.toString());
+ }
+ }
+
+ /**
+ * A repeatable event that can be paused or resumed by changing
+ * its interestOps.
+ * When the event is fired, it is first paused before being signaled.
+ * It is the responsibility of the code triggered by {@code signalEvent}
+ * to resume the event if required.
+ */
+ private static abstract class SocketFlowEvent extends AsyncEvent {
+ final SocketChannel channel;
+ final int defaultInterest;
+ volatile int interestOps;
+ volatile boolean registered;
+ SocketFlowEvent(int defaultInterest, SocketChannel channel) {
+ super(AsyncEvent.REPEATING);
+ this.defaultInterest = defaultInterest;
+ this.channel = channel;
+ }
+ final boolean registered() {return registered;}
+ final void resume() {
+ interestOps = defaultInterest;
+ registered = true;
+ }
+ final void pause() {interestOps = 0;}
+ @Override
+ public final SelectableChannel channel() {return channel;}
+ @Override
+ public final int interestOps() {return interestOps;}
+
+ @Override
+ public final void handle() {
+ pause(); // pause, then signal
+ signalEvent(); // won't be fired again until resumed.
+ }
+ @Override
+ public final void abort(IOException error) {
+ debug().log(Level.DEBUG, () -> "abort: " + error);
+ pause(); // pause, then signal
+ signalError(error); // should not be resumed after abort (not checked)
+ }
+
+ protected abstract void signalEvent();
+ protected abstract void signalError(Throwable error);
+ abstract System.Logger debug();
+ }
+
+ // ===================================================================== //
+ // Writing //
+ // ======================================================================//
+
+ // This class makes the assumption that the publisher will call
+ // onNext sequentially, and that onNext won't be called if the demand
+ // has not been incremented by request(1).
+ // It has a 'queue of 1' meaning that it will call request(1) in
+ // onSubscribe, and then only after its 'current' buffer list has been
+ // fully written and current set to null;
+ private final class InternalWriteSubscriber
+ implements Flow.Subscriber<List<ByteBuffer>> {
+
+ volatile Flow.Subscription subscription;
+ volatile List<ByteBuffer> current;
+ volatile boolean completed;
+ final WriteEvent writeEvent = new WriteEvent(channel, this);
+ final AtomicLong writeDemand = new AtomicLong();
+
+ @Override
+ public void onSubscribe(Flow.Subscription subscription) {
+ Flow.Subscription previous = this.subscription;
+ this.subscription = subscription;
+ debug.log(Level.DEBUG, "subscribed for writing");
+ if (current == null) {
+ if (previous == subscription || previous == null) {
+ if (writeDemand.compareAndSet(0, 1)) {
+ subscription.request(1);
+ }
+ } else {
+ writeDemand.set(1);
+ subscription.request(1);
+ }
+ }
+ }
+
+ @Override
+ public void onNext(List<ByteBuffer> bufs) {
+ assert current == null; // this is a queue of 1.
+ assert subscription != null;
+ current = bufs;
+ tryFlushCurrent(client.isSelectorThread()); // may be in selector thread
+ // For instance in HTTP/2, a received SETTINGS frame might trigger
+ // the sending of a SETTINGS frame in turn which might cause
+ // onNext to be called from within the same selector thread that the
+ // original SETTINGS frames arrived on. If rs is the read-subscriber
+ // and ws is the write-subscriber then the following can occur:
+ // ReadEvent -> rs.onNext(bytes) -> process server SETTINGS -> write
+ // client SETTINGS -> ws.onNext(bytes) -> tryFlushCurrent
+ debugState("leaving w.onNext");
+ }
+
+ // we don't use a SequentialScheduler here: we rely on
+ // onNext() being called sequentially, and not being called
+ // if we haven't call request(1)
+ // onNext is usually called from within a user/executor thread.
+ // we will perform the initial writing in that thread.
+ // if for some reason, not all data can be written, the writeEvent
+ // will be resumed, and the rest of the data will be written from
+ // the selector manager thread when the writeEvent is fired.
+ // If we are in the selector manager thread, then we will use the executor
+ // to call request(1), ensuring that onNext() won't be called from
+ // within the selector thread.
+ // If we are not in the selector manager thread, then we don't care.
+ void tryFlushCurrent(boolean inSelectorThread) {
+ List<ByteBuffer> bufs = current;
+ if (bufs == null) return;
+ try {
+ assert inSelectorThread == client.isSelectorThread() :
+ "should " + (inSelectorThread ? "" : "not ")
+ + " be in the selector thread";
+ long remaining = Utils.remaining(bufs);
+ debug.log(Level.DEBUG, "trying to write: %d", remaining);
+ long written = writeAvailable(bufs);
+ debug.log(Level.DEBUG, "wrote: %d", remaining);
+ if (written == -1) {
+ signalError(new EOFException("EOF reached while writing"));
+ return;
+ }
+ assert written <= remaining;
+ if (remaining - written == 0) {
+ current = null;
+ writeDemand.decrementAndGet();
+ Runnable requestMore = this::requestMore;
+ if (inSelectorThread) {
+ assert client.isSelectorThread();
+ client.theExecutor().execute(requestMore);
+ } else {
+ assert !client.isSelectorThread();
+ requestMore.run();
+ }
+ } else {
+ resumeWriteEvent(inSelectorThread);
+ }
+ } catch (Throwable t) {
+ signalError(t);
+ subscription.cancel();
+ }
+ }
+
+ void requestMore() {
+ try {
+ if (completed) return;
+ long d = writeDemand.get();
+ if (writeDemand.compareAndSet(0,1)) {
+ debug.log(Level.DEBUG, "write: requesting more...");
+ subscription.request(1);
+ } else {
+ debug.log(Level.DEBUG, "write: no need to request more: %d", d);
+ }
+ } catch (Throwable t) {
+ debug.log(Level.DEBUG, () ->
+ "write: error while requesting more: " + t);
+ signalError(t);
+ subscription.cancel();
+ } finally {
+ debugState("leaving requestMore: ");
+ }
+ }
+
+ @Override
+ public void onError(Throwable throwable) {
+ signalError(throwable);
+ }
+
+ @Override
+ public void onComplete() {
+ completed = true;
+ // no need to pause the write event here: the write event will
+ // be paused if there is nothing more to write.
+ List<ByteBuffer> bufs = current;
+ long remaining = bufs == null ? 0 : Utils.remaining(bufs);
+ debug.log(Level.DEBUG, "write completed, %d yet to send", remaining);
+ debugState("InternalWriteSubscriber::onComplete");
+ }
+
+ void resumeWriteEvent(boolean inSelectorThread) {
+ debug.log(Level.DEBUG, "scheduling write event");
+ resumeEvent(writeEvent, this::signalError);
+ }
+
+ void pauseWriteEvent() {
+ debug.log(Level.DEBUG, "pausing write event");
+ pauseEvent(writeEvent, this::signalError);
+ }
+
+ void signalWritable() {
+ debug.log(Level.DEBUG, "channel is writable");
+ tryFlushCurrent(true);
+ }
+
+ void signalError(Throwable error) {
+ debug.log(Level.DEBUG, () -> "write error: " + error);
+ completed = true;
+ readPublisher.signalError(error);
+ }
+
+ // A repeatable WriteEvent which is paused after firing and can
+ // be resumed if required - see SocketFlowEvent;
+ final class WriteEvent extends SocketFlowEvent {
+ final InternalWriteSubscriber sub;
+ WriteEvent(SocketChannel channel, InternalWriteSubscriber sub) {
+ super(SelectionKey.OP_WRITE, channel);
+ this.sub = sub;
+ }
+ @Override
+ protected final void signalEvent() {
+ try {
+ client.eventUpdated(this);
+ sub.signalWritable();
+ } catch(Throwable t) {
+ sub.signalError(t);
+ }
+ }
+
+ @Override
+ protected void signalError(Throwable error) {
+ sub.signalError(error);
+ }
+
+ @Override
+ System.Logger debug() {
+ return debug;
+ }
+
+ }
+
+ }
+
+ // ===================================================================== //
+ // Reading //
+ // ===================================================================== //
+
+ // The InternalReadPublisher uses a SequentialScheduler to ensure that
+ // onNext/onError/onComplete are called sequentially on the caller's
+ // subscriber.
+ // However, it relies on the fact that the only time where
+ // runOrSchedule() is called from a user/executor thread is in signalError,
+ // right after the errorRef has been set.
+ // Because the sequential scheduler's task always checks for errors first,
+ // and always terminate the scheduler on error, then it is safe to assume
+ // that if it reaches the point where it reads from the channel, then
+ // it is running in the SelectorManager thread. This is because all
+ // other invocation of runOrSchedule() are triggered from within a
+ // ReadEvent.
+ //
+ // When pausing/resuming the event, some shortcuts can then be taken
+ // when we know we're running in the selector manager thread
+ // (in that case there's no need to call client.eventUpdated(readEvent);
+ //
+ private final class InternalReadPublisher
+ implements Flow.Publisher<List<ByteBuffer>> {
+ private final InternalReadSubscription subscriptionImpl
+ = new InternalReadSubscription();
+ AtomicReference<ReadSubscription> pendingSubscription = new AtomicReference<>();
+ private volatile ReadSubscription subscription;
+
+ @Override
+ public void subscribe(Flow.Subscriber<? super List<ByteBuffer>> s) {
+ Objects.requireNonNull(s);
+
+ TubeSubscriber sub = FlowTube.asTubeSubscriber(s);
+ ReadSubscription target = new ReadSubscription(subscriptionImpl, sub);
+ ReadSubscription previous = pendingSubscription.getAndSet(target);
+
+ if (previous != null && previous != target) {
+ debug.log(Level.DEBUG,
+ () -> "read publisher: dropping pending subscriber: "
+ + previous.subscriber);
+ previous.errorRef.compareAndSet(null, errorRef.get());
+ previous.signalOnSubscribe();
+ if (subscriptionImpl.completed) {
+ previous.signalCompletion();
+ } else {
+ previous.subscriber.dropSubscription();
+ }
+ }
+
+ debug.log(Level.DEBUG, "read publisher got subscriber");
+ subscriptionImpl.signalSubscribe();
+ debugState("leaving read.subscribe: ");
+ }
+
+ void signalError(Throwable error) {
+ if (!errorRef.compareAndSet(null, error)) {
+ return;
+ }
+ subscriptionImpl.handleError();
+ }
+
+ final class ReadSubscription implements Flow.Subscription {
+ final InternalReadSubscription impl;
+ final TubeSubscriber subscriber;
+ final AtomicReference<Throwable> errorRef = new AtomicReference<>();
+ volatile boolean subscribed;
+ volatile boolean cancelled;
+ volatile boolean completed;
+
+ public ReadSubscription(InternalReadSubscription impl,
+ TubeSubscriber subscriber) {
+ this.impl = impl;
+ this.subscriber = subscriber;
+ }
+
+ @Override
+ public void cancel() {
+ cancelled = true;
+ }
+
+ @Override
+ public void request(long n) {
+ if (!cancelled) {
+ impl.request(n);
+ } else {
+ debug.log(Level.DEBUG,
+ "subscription cancelled, ignoring request %d", n);
+ }
+ }
+
+ void signalCompletion() {
+ assert subscribed || cancelled;
+ if (completed || cancelled) return;
+ synchronized (this) {
+ if (completed) return;
+ completed = true;
+ }
+ Throwable error = errorRef.get();
+ if (error != null) {
+ debug.log(Level.DEBUG, () ->
+ "forwarding error to subscriber: "
+ + error);
+ subscriber.onError(error);
+ } else {
+ debug.log(Level.DEBUG, "completing subscriber");
+ subscriber.onComplete();
+ }
+ }
+
+ void signalOnSubscribe() {
+ if (subscribed || cancelled) return;
+ synchronized (this) {
+ if (subscribed || cancelled) return;
+ subscribed = true;
+ }
+ subscriber.onConnection(this);
+ debug.log(Level.DEBUG, "onConnection called");
+ if (errorRef.get() != null) {
+ signalCompletion();
+ }
+ }
+ }
+
+ final class InternalReadSubscription implements Flow.Subscription {
+
+ private final Demand demand = new Demand();
+ final SequentialScheduler readScheduler;
+ private volatile boolean completed;
+ private final ReadEvent readEvent;
+ private final AsyncEvent subscribeEvent;
+
+ InternalReadSubscription() {
+ readScheduler = new SequentialScheduler(new SocketFlowTask(this::read));
+ subscribeEvent = new AsyncTriggerEvent(this::signalError,
+ this::handleSubscribeEvent);
+ readEvent = new ReadEvent(channel, this);
+ }
+
+ /*
+ * This method must be invoked before any other method of this class.
+ */
+ final void signalSubscribe() {
+ if (readScheduler.isStopped() || completed) {
+ // if already completed or stopped we can handle any
+ // pending connection directly from here.
+ debug.log(Level.DEBUG,
+ "handling pending subscription while completed");
+ handlePending();
+ } else {
+ try {
+ debug.log(Level.DEBUG,
+ "registering subscribe event");
+ client.registerEvent(subscribeEvent);
+ } catch (Throwable t) {
+ signalError(t);
+ handlePending();
+ }
+ }
+ }
+
+ final void handleSubscribeEvent() {
+ assert client.isSelectorThread();
+ debug.log(Level.DEBUG, "subscribe event raised");
+ readScheduler.runOrSchedule();
+ if (readScheduler.isStopped() || completed) {
+ // if already completed or stopped we can handle any
+ // pending connection directly from here.
+ debug.log(Level.DEBUG,
+ "handling pending subscription when completed");
+ handlePending();
+ }
+ }
+
+
+ /*
+ * Although this method is thread-safe, the Reactive-Streams spec seems
+ * to not require it to be as such. It's a responsibility of the
+ * subscriber to signal demand in a thread-safe manner.
+ *
+ * https://github.com/reactive-streams/reactive-streams-jvm/blob/dd24d2ab164d7de6c316f6d15546f957bec29eaa/README.md
+ * (rules 2.7 and 3.4)
+ */
+ @Override
+ public final void request(long n) {
+ if (n > 0L) {
+ boolean wasFulfilled = demand.increase(n);
+ if (wasFulfilled) {
+ debug.log(Level.DEBUG, "got some demand for reading");
+ resumeReadEvent();
+ // if demand has been changed from fulfilled
+ // to unfulfilled register read event;
+ }
+ } else {
+ signalError(new IllegalArgumentException("non-positive request"));
+ }
+ debugState("leaving request("+n+"): ");
+ }
+
+ @Override
+ public final void cancel() {
+ pauseReadEvent();
+ readScheduler.stop();
+ }
+
+ private void resumeReadEvent() {
+ debug.log(Level.DEBUG, "resuming read event");
+ resumeEvent(readEvent, this::signalError);
+ }
+
+ private void pauseReadEvent() {
+ debug.log(Level.DEBUG, "pausing read event");
+ pauseEvent(readEvent, this::signalError);
+ }
+
+
+ final void handleError() {
+ assert errorRef.get() != null;
+ readScheduler.runOrSchedule();
+ }
+
+ final void signalError(Throwable error) {
+ if (!errorRef.compareAndSet(null, error)) {
+ return;
+ }
+ debug.log(Level.DEBUG, () -> "got read error: " + error);
+ readScheduler.runOrSchedule();
+ }
+
+ final void signalReadable() {
+ readScheduler.runOrSchedule();
+ }
+
+ /** The body of the task that runs in SequentialScheduler. */
+ final void read() {
+ // It is important to only call pauseReadEvent() when stopping
+ // the scheduler. The event is automatically paused before
+ // firing, and trying to pause it again could cause a race
+ // condition between this loop, which calls tryDecrementDemand(),
+ // and the thread that calls request(n), which will try to resume
+ // reading.
+ try {
+ while(!readScheduler.isStopped()) {
+ if (completed) return;
+
+ // make sure we have a subscriber
+ if (handlePending()) {
+ debug.log(Level.DEBUG, "pending subscriber subscribed");
+ return;
+ }
+
+ // If an error was signaled, we might not be in the
+ // the selector thread, and that is OK, because we
+ // will just call onError and return.
+ ReadSubscription current = subscription;
+ TubeSubscriber subscriber = current.subscriber;
+ Throwable error = errorRef.get();
+ if (error != null) {
+ completed = true;
+ // safe to pause here because we're finished anyway.
+ pauseReadEvent();
+ debug.log(Level.DEBUG, () -> "Sending error " + error
+ + " to subscriber " + subscriber);
+ current.errorRef.compareAndSet(null, error);
+ current.signalCompletion();
+ readScheduler.stop();
+ debugState("leaving read() loop with error: ");
+ return;
+ }
+
+ // If we reach here then we must be in the selector thread.
+ assert client.isSelectorThread();
+ if (demand.tryDecrement()) {
+ // we have demand.
+ try {
+ List<ByteBuffer> bytes = readAvailable();
+ if (bytes == EOF) {
+ if (!completed) {
+ debug.log(Level.DEBUG, "got read EOF");
+ completed = true;
+ // safe to pause here because we're finished
+ // anyway.
+ pauseReadEvent();
+ current.signalCompletion();
+ readScheduler.stop();
+ }
+ debugState("leaving read() loop after EOF: ");
+ return;
+ } else if (Utils.remaining(bytes) > 0) {
+ // the subscriber is responsible for offloading
+ // to another thread if needed.
+ debug.log(Level.DEBUG, () -> "read bytes: "
+ + Utils.remaining(bytes));
+ assert !current.completed;
+ subscriber.onNext(bytes);
+ // we could continue looping until the demand
+ // reaches 0. However, that would risk starving
+ // other connections (bound to other socket
+ // channels) - as other selected keys activated
+ // by the selector manager thread might be
+ // waiting for this event to terminate.
+ // So resume the read event and return now...
+ resumeReadEvent();
+ debugState("leaving read() loop after onNext: ");
+ return;
+ } else {
+ // nothing available!
+ debug.log(Level.DEBUG, "no more bytes available");
+ // re-increment the demand and resume the read
+ // event. This ensures that this loop is
+ // executed again when the socket becomes
+ // readable again.
+ demand.increase(1);
+ resumeReadEvent();
+ debugState("leaving read() loop with no bytes");
+ return;
+ }
+ } catch (Throwable x) {
+ signalError(x);
+ continue;
+ }
+ } else {
+ debug.log(Level.DEBUG, "no more demand for reading");
+ // the event is paused just after firing, so it should
+ // still be paused here, unless the demand was just
+ // incremented from 0 to n, in which case, the
+ // event will be resumed, causing this loop to be
+ // invoked again when the socket becomes readable:
+ // This is what we want.
+ // Trying to pause the event here would actually
+ // introduce a race condition between this loop and
+ // request(n).
+ debugState("leaving read() loop with no demand");
+ break;
+ }
+ }
+ } catch (Throwable t) {
+ debug.log(Level.DEBUG, "Unexpected exception in read loop", t);
+ signalError(t);
+ } finally {
+ handlePending();
+ }
+ }
+
+ boolean handlePending() {
+ ReadSubscription pending = pendingSubscription.getAndSet(null);
+ if (pending == null) return false;
+ debug.log(Level.DEBUG, "handling pending subscription for %s",
+ pending.subscriber);
+ ReadSubscription current = subscription;
+ if (current != null && current != pending && !completed) {
+ current.subscriber.dropSubscription();
+ }
+ debug.log(Level.DEBUG, "read demand reset to 0");
+ subscriptionImpl.demand.reset(); // subscriber will increase demand if it needs to.
+ pending.errorRef.compareAndSet(null, errorRef.get());
+ if (!readScheduler.isStopped()) {
+ subscription = pending;
+ } else {
+ debug.log(Level.DEBUG, "socket tube is already stopped");
+ }
+ debug.log(Level.DEBUG, "calling onSubscribe");
+ pending.signalOnSubscribe();
+ if (completed) {
+ pending.errorRef.compareAndSet(null, errorRef.get());
+ pending.signalCompletion();
+ }
+ return true;
+ }
+ }
+
+
+ // A repeatable ReadEvent which is paused after firing and can
+ // be resumed if required - see SocketFlowEvent;
+ final class ReadEvent extends SocketFlowEvent {
+ final InternalReadSubscription sub;
+ ReadEvent(SocketChannel channel, InternalReadSubscription sub) {
+ super(SelectionKey.OP_READ, channel);
+ this.sub = sub;
+ }
+ @Override
+ protected final void signalEvent() {
+ try {
+ client.eventUpdated(this);
+ sub.signalReadable();
+ } catch(Throwable t) {
+ sub.signalError(t);
+ }
+ }
+
+ @Override
+ protected final void signalError(Throwable error) {
+ sub.signalError(error);
+ }
+
+ @Override
+ System.Logger debug() {
+ return debug;
+ }
+ }
+
+ }
+
+ // ===================================================================== //
+ // Socket Channel Read/Write //
+ // ===================================================================== //
+ static final int MAX_BUFFERS = 3;
+ static final List<ByteBuffer> EOF = List.of();
+
+ private List<ByteBuffer> readAvailable() throws IOException {
+ ByteBuffer buf = buffersSource.get();
+ assert buf.hasRemaining();
+
+ int read;
+ int pos = buf.position();
+ List<ByteBuffer> list = null;
+ while (buf.hasRemaining()) {
+ while ((read = channel.read(buf)) > 0) {
+ if (!buf.hasRemaining()) break;
+ }
+
+ // nothing read;
+ if (buf.position() == pos) {
+ // An empty list signal the end of data, and should only be
+ // returned if read == -1.
+ // If we already read some data, then we must return what we have
+ // read, and -1 will be returned next time the caller attempts to
+ // read something.
+ if (list == null && read == -1) { // eof
+ list = EOF;
+ break;
+ }
+ }
+ buf.limit(buf.position());
+ buf.position(pos);
+ if (list == null) {
+ list = List.of(buf);
+ } else {
+ if (!(list instanceof ArrayList)) {
+ list = new ArrayList<>(list);
+ }
+ list.add(buf);
+ }
+ if (read <= 0 || list.size() == MAX_BUFFERS) break;
+ buf = buffersSource.get();
+ pos = buf.position();
+ assert buf.hasRemaining();
+ }
+ return list;
+ }
+
+ private long writeAvailable(List<ByteBuffer> bytes) throws IOException {
+ ByteBuffer[] srcs = bytes.toArray(Utils.EMPTY_BB_ARRAY);
+ final long remaining = Utils.remaining(srcs);
+ long written = 0;
+ while (remaining > written) {
+ long w = channel.write(srcs);
+ if (w == -1 && written == 0) return -1;
+ if (w == 0) break;
+ written += w;
+ }
+ return written;
+ }
+
+ private void resumeEvent(SocketFlowEvent event,
+ Consumer<Throwable> errorSignaler) {
+ boolean registrationRequired;
+ synchronized(lock) {
+ registrationRequired = !event.registered();
+ event.resume();
+ }
+ try {
+ if (registrationRequired) {
+ client.registerEvent(event);
+ } else {
+ client.eventUpdated(event);
+ }
+ } catch(Throwable t) {
+ errorSignaler.accept(t);
+ }
+ }
+
+ private void pauseEvent(SocketFlowEvent event,
+ Consumer<Throwable> errorSignaler) {
+ synchronized(lock) {
+ event.pause();
+ }
+ try {
+ client.eventUpdated(event);
+ } catch(Throwable t) {
+ errorSignaler.accept(t);
+ }
+ }
+
+ @Override
+ public void connectFlows(TubePublisher writePublisher,
+ TubeSubscriber readSubscriber) {
+ debug.log(Level.DEBUG, "connecting flows");
+ this.subscribe(readSubscriber);
+ writePublisher.subscribe(this);
+ }
+
+
+ @Override
+ public String toString() {
+ return dbgString();
+ }
+
+ final String dbgString() {
+ return "SocketTube("+id+")";
+ }
+}
--- a/src/jdk.incubator.httpclient/share/classes/jdk/incubator/http/Stream.java Sun Nov 05 17:05:57 2017 +0000
+++ b/src/jdk.incubator.httpclient/share/classes/jdk/incubator/http/Stream.java Sun Nov 05 17:32:13 2017 +0000
@@ -26,24 +26,28 @@
package jdk.incubator.http;
import java.io.IOException;
+import java.lang.System.Logger.Level;
import java.net.URI;
import java.nio.ByteBuffer;
import java.util.ArrayList;
+import java.util.Arrays;
+import java.util.Collections;
import java.util.List;
import java.util.Optional;
import java.util.concurrent.CompletableFuture;
-import java.util.concurrent.CompletionException;
-import java.util.concurrent.ExecutionException;
+import java.util.concurrent.ConcurrentLinkedQueue;
import java.util.concurrent.Executor;
import java.util.concurrent.Flow;
import java.util.concurrent.Flow.Subscription;
-import java.util.concurrent.TimeUnit;
-import java.util.concurrent.TimeoutException;
-import java.util.function.Consumer;
+import java.util.concurrent.atomic.AtomicReference;
+import java.util.stream.Collectors;
import jdk.incubator.http.internal.common.*;
+import jdk.incubator.http.internal.common.SequentialScheduler;
+import jdk.incubator.http.internal.common.SequentialScheduler.SynchronizedRestartableTask;
import jdk.incubator.http.internal.frame.*;
import jdk.incubator.http.internal.hpack.DecodingCallback;
+import static java.util.stream.Collectors.toList;
/**
* Http/2 Stream handling.
@@ -54,10 +58,6 @@
*
* sendRequest() -- sendHeadersOnly() + sendBody()
*
- * sendBody() -- in calling thread: obeys all flow control (so may block)
- * obtains data from request body processor and places on connection
- * outbound Q.
- *
* sendBodyAsync() -- calls sendBody() in an executor thread.
*
* sendHeadersAsync() -- calls sendHeadersOnly() which does not block
@@ -77,10 +77,6 @@
*
* getResponse() -- calls getResponseAsync() and waits for CF to complete
*
- * responseBody() -- in calling thread: blocks for incoming DATA frames on
- * stream inputQ. Obeys remote and local flow control so may block.
- * Calls user response body processor with data buffers.
- *
* responseBodyAsync() -- calls responseBody() in an executor thread.
*
* incoming() -- entry point called from connection reader thread. Frames are
@@ -98,7 +94,13 @@
*/
class Stream<T> extends ExchangeImpl<T> {
- final AsyncDataReadQueue inputQ = new AsyncDataReadQueue();
+ final static boolean DEBUG = Utils.DEBUG; // Revisit: temporary developer's flag
+ final System.Logger debug = Utils.getDebugLogger(this::dbgString, DEBUG);
+
+ final ConcurrentLinkedQueue<Http2Frame> inputQ = new ConcurrentLinkedQueue<>();
+ final SequentialScheduler sched =
+ new SequentialScheduler(new SynchronizedRestartableTask(this::schedule));
+ final SubscriptionBase userSubscription = new SubscriptionBase(sched, this::cancel);
/**
* This stream's identifier. Assigned lazily by the HTTP2Connection before
@@ -117,13 +119,15 @@
HttpHeadersImpl responseHeaders;
final HttpHeadersImpl requestHeaders;
final HttpHeadersImpl requestPseudoHeaders;
- HttpResponse.BodyProcessor<T> responseProcessor;
- final HttpRequest.BodyProcessor requestProcessor;
+ volatile HttpResponse.BodySubscriber<T> responseSubscriber;
+ final HttpRequest.BodyPublisher requestPublisher;
+ volatile RequestSubscriber requestSubscriber;
volatile int responseCode;
volatile Response response;
volatile CompletableFuture<Response> responseCF;
- final AbstractPushPublisher<ByteBuffer> publisher;
+ volatile Throwable failed; // The exception with which this stream was canceled.
final CompletableFuture<Void> requestBodyCF = new MinimalFuture<>();
+ volatile CompletableFuture<T> responseBodyCF;
/** True if END_STREAM has been seen in a frame received on this stream. */
private volatile boolean remotelyClosed;
@@ -146,15 +150,91 @@
return connection.connection;
}
+ /**
+ * Invoked either from incoming() -> {receiveDataFrame() or receiveResetFrame() }
+ * of after user subscription window has re-opened, from SubscriptionBase.request()
+ */
+ private void schedule() {
+ if (responseSubscriber == null)
+ // can't process anything yet
+ return;
+
+ while (!inputQ.isEmpty()) {
+ Http2Frame frame = inputQ.peek();
+ if (frame instanceof ResetFrame) {
+ inputQ.remove();
+ handleReset((ResetFrame)frame);
+ return;
+ }
+ DataFrame df = (DataFrame)frame;
+ boolean finished = df.getFlag(DataFrame.END_STREAM);
+
+ ByteBufferReference[] buffers = df.getData();
+ List<ByteBuffer> dsts = Arrays.stream(buffers)
+ .map(ByteBufferReference::get)
+ .filter(ByteBuffer::hasRemaining)
+ .collect(Collectors.collectingAndThen(toList(), Collections::unmodifiableList));
+ int size = (int)Utils.remaining(dsts);
+ if (size == 0 && finished) {
+ inputQ.remove();
+ Log.logTrace("responseSubscriber.onComplete");
+ debug.log(Level.DEBUG, "incoming: onComplete");
+ sched.stop();
+ responseSubscriber.onComplete();
+ setEndStreamReceived();
+ return;
+ } else if (userSubscription.tryDecrement()) {
+ inputQ.remove();
+ Log.logTrace("responseSubscriber.onNext {0}", size);
+ debug.log(Level.DEBUG, "incoming: onNext(%d)", size);
+ responseSubscriber.onNext(dsts);
+ if (consumed(df)) {
+ Log.logTrace("responseSubscriber.onComplete");
+ debug.log(Level.DEBUG, "incoming: onComplete");
+ sched.stop();
+ responseSubscriber.onComplete();
+ setEndStreamReceived();
+ return;
+ }
+ } else {
+ return;
+ }
+ }
+ Throwable t = failed;
+ if (t != null) {
+ sched.stop();
+ responseSubscriber.onError(t);
+ close();
+ }
+ }
+
+ // Callback invoked after the Response BodyProcessor has consumed the
+ // buffers contained in a DataFrame.
+ // Returns true if END_STREAM is reached, false otherwise.
+ private boolean consumed(DataFrame df) {
+ // RFC 7540 6.1:
+ // The entire DATA frame payload is included in flow control,
+ // including the Pad Length and Padding fields if present
+ int len = df.payloadLength();
+ connection.windowUpdater.update(len);
+
+ if (!df.getFlag(DataFrame.END_STREAM)) {
+ // Don't send window update on a stream which is
+ // closed or half closed.
+ windowUpdater.update(len);
+ return false; // more data coming
+ }
+ return true; // end of stream
+ }
+
@Override
CompletableFuture<T> readBodyAsync(HttpResponse.BodyHandler<T> handler,
boolean returnConnectionToPool,
Executor executor)
{
Log.logTrace("Reading body on stream {0}", streamid);
- responseProcessor = handler.apply(responseCode, responseHeaders);
- publisher.subscribe(responseProcessor);
- CompletableFuture<T> cf = receiveData(executor);
+ responseSubscriber = handler.apply(responseCode, responseHeaders);
+ CompletableFuture<T> cf = receiveData();
PushGroup<?,?> pg = exchange.getPushGroup();
if (pg != null) {
@@ -165,20 +245,6 @@
}
@Override
- T readBody(HttpResponse.BodyHandler<T> handler, boolean returnConnectionToPool)
- throws IOException
- {
- CompletableFuture<T> cf = readBodyAsync(handler,
- returnConnectionToPool,
- null);
- try {
- return cf.join();
- } catch (CompletionException e) {
- throw Utils.getIOException(e);
- }
- }
-
- @Override
public String toString() {
StringBuilder sb = new StringBuilder();
sb.append("streamid: ")
@@ -186,67 +252,38 @@
return sb.toString();
}
- private boolean receiveDataFrame(Http2Frame frame) throws IOException, InterruptedException {
- if (frame instanceof ResetFrame) {
- handleReset((ResetFrame) frame);
- return true;
- } else if (!(frame instanceof DataFrame)) {
- assert false;
- return true;
- }
- DataFrame df = (DataFrame) frame;
- // RFC 7540 6.1:
- // The entire DATA frame payload is included in flow control,
- // including the Pad Length and Padding fields if present
- int len = df.payloadLength();
- ByteBufferReference[] buffers = df.getData();
- for (ByteBufferReference b : buffers) {
- ByteBuffer buf = b.get();
- if (buf.hasRemaining()) {
- publisher.acceptData(Optional.of(buf));
- }
- }
- connection.windowUpdater.update(len);
- if (df.getFlag(DataFrame.END_STREAM)) {
- setEndStreamReceived();
- publisher.acceptData(Optional.empty());
- return false;
- }
- // Don't send window update on a stream which is
- // closed or half closed.
- windowUpdater.update(len);
- return true;
+ private void receiveDataFrame(DataFrame df) {
+ inputQ.add(df);
+ sched.runOrSchedule();
+ }
+
+ /**
+ * RESET always handled inline in queue
+ */
+ private void receiveResetFrame(ResetFrame frame) {
+ inputQ.add(frame);
+ sched.runOrSchedule();
}
- // pushes entire response body into response processor
+ // pushes entire response body into response subscriber
// blocking when required by local or remote flow control
- CompletableFuture<T> receiveData(Executor executor) {
- CompletableFuture<T> cf = responseProcessor
+ CompletableFuture<T> receiveData() {
+ responseBodyCF = responseSubscriber
.getBody()
.toCompletableFuture();
- Consumer<Throwable> onError = e -> {
- Log.logTrace("receiveData: {0}", e.toString());
- e.printStackTrace();
- cf.completeExceptionally(e);
- publisher.acceptError(e);
- };
- if (executor == null) {
- inputQ.blockingReceive(this::receiveDataFrame, onError);
+
+ if (isCanceled()) {
+ Throwable t = getCancelCause();
+ responseBodyCF.completeExceptionally(t);
+ sched.runOrSchedule();
} else {
- inputQ.asyncReceive(executor, this::receiveDataFrame, onError);
+ responseSubscriber.onSubscribe(userSubscription);
+ sched.runOrSchedule(); // in case data waiting already to be processed
}
- return cf;
+ return responseBodyCF;
}
@Override
- void sendBody() throws IOException {
- try {
- sendBodyImpl().join();
- } catch (CompletionException e) {
- throw Utils.getIOException(e);
- }
- }
-
CompletableFuture<ExchangeImpl<T>> sendBodyAsync() {
return sendBodyImpl().thenApply( v -> this);
}
@@ -262,7 +299,7 @@
this.connection = connection;
this.windowController = windowController;
this.request = e.request();
- this.requestProcessor = request.requestProcessor;
+ this.requestPublisher = request.requestPublisher; // may be null
responseHeaders = new HttpHeadersImpl();
requestHeaders = new HttpHeadersImpl();
rspHeadersConsumer = (name, value) -> {
@@ -274,7 +311,6 @@
};
this.requestPseudoHeaders = new HttpHeadersImpl();
// NEW
- this.publisher = new BlockingPushPublisher<>();
this.windowUpdater = new StreamWindowUpdateSender(connection);
}
@@ -284,17 +320,18 @@
* Data frames will be removed by response body thread.
*/
void incoming(Http2Frame frame) throws IOException {
+ debug.log(Level.DEBUG, "incoming: %s", frame);
if ((frame instanceof HeaderFrame)) {
HeaderFrame hframe = (HeaderFrame)frame;
if (hframe.endHeaders()) {
Log.logTrace("handling response (streamid={0})", streamid);
handleResponse();
if (hframe.getFlag(HeaderFrame.END_STREAM)) {
- inputQ.put(new DataFrame(streamid, DataFrame.END_STREAM, new ByteBufferReference[0]));
+ receiveDataFrame(new DataFrame(streamid, DataFrame.END_STREAM, new ByteBufferReference[0]));
}
}
} else if (frame instanceof DataFrame) {
- inputQ.put(frame);
+ receiveDataFrame((DataFrame)frame);
} else {
otherFrame(frame);
}
@@ -349,50 +386,32 @@
completeResponse(response);
}
- void incoming_reset(ResetFrame frame) throws IOException {
+ void incoming_reset(ResetFrame frame) {
Log.logTrace("Received RST_STREAM on stream {0}", streamid);
if (endStreamReceived()) {
Log.logTrace("Ignoring RST_STREAM frame received on remotely closed stream {0}", streamid);
} else if (closed) {
Log.logTrace("Ignoring RST_STREAM frame received on closed stream {0}", streamid);
} else {
- boolean pushedToQueue = false;
- synchronized(this) {
- // if the response headers are not yet
- // received, or the inputQueue is closed, handle reset directly.
- // Otherwise, put it in the input queue in order to read all
- // pending data frames first. Indeed, a server may send
- // RST_STREAM after sending END_STREAM, in which case we should
- // ignore it. However, we won't know if we have received END_STREAM
- // or not until all pending data frames are read.
- // Because the inputQ will not be read until the response
- // headers are received, and because response headers won't be
- // sent if the server sent RST_STREAM, then we must handle
- // reset here directly unless responseHeadersReceived is true.
- pushedToQueue = !closed && responseHeadersReceived && inputQ.tryPut(frame);
- }
- if (!pushedToQueue) {
- // RST_STREAM was not pushed to the queue: handle it.
- try {
- handleReset(frame);
- } catch (IOException io) {
- completeResponseExceptionally(io);
- }
- } else {
- // RST_STREAM was pushed to the queue. It will be handled by
- // asyncReceive after all pending data frames have been
- // processed.
- Log.logTrace("RST_STREAM pushed in queue for stream {0}", streamid);
- }
+ // put it in the input queue in order to read all
+ // pending data frames first. Indeed, a server may send
+ // RST_STREAM after sending END_STREAM, in which case we should
+ // ignore it. However, we won't know if we have received END_STREAM
+ // or not until all pending data frames are read.
+ receiveResetFrame(frame);
+ // RST_STREAM was pushed to the queue. It will be handled by
+ // asyncReceive after all pending data frames have been
+ // processed.
+ Log.logTrace("RST_STREAM pushed in queue for stream {0}", streamid);
}
}
- void handleReset(ResetFrame frame) throws IOException {
+ void handleReset(ResetFrame frame) {
Log.logTrace("Handling RST_STREAM on stream {0}", streamid);
if (!closed) {
close();
int error = frame.getErrorCode();
- throw new IOException(ErrorFrame.stringForCode(error));
+ completeResponseExceptionally(new IOException(ErrorFrame.stringForCode(error)));
} else {
Log.logTrace("Ignoring RST_STREAM frame received on closed stream {0}", streamid);
}
@@ -431,20 +450,21 @@
if (pushGroup == null || pushGroup.noMorePushes()) {
cancelImpl(new IllegalStateException("unexpected push promise"
+ " on stream " + streamid));
+ return;
}
- HttpResponse.MultiProcessor<?,T> proc = pushGroup.processor();
+ HttpResponse.MultiSubscriber<?,T> proc = pushGroup.subscriber();
CompletableFuture<HttpResponse<T>> cf = pushStream.responseCF();
- Optional<HttpResponse.BodyHandler<T>> bpOpt = proc.onRequest(
- pushReq);
+ Optional<HttpResponse.BodyHandler<T>> bpOpt =
+ pushGroup.handlerForPushRequest(pushReq);
if (!bpOpt.isPresent()) {
IOException ex = new IOException("Stream "
+ streamid + " cancelled by user");
if (Log.trace()) {
- Log.logTrace("No body processor for {0}: {1}", pushReq,
+ Log.logTrace("No body subscriber for {0}: {1}", pushReq,
ex.getMessage());
}
pushStream.cancelImpl(ex);
@@ -458,6 +478,7 @@
// setup housekeeping for when the push is received
// TODO: deal with ignoring of CF anti-pattern
cf.whenComplete((HttpResponse<T> resp, Throwable t) -> {
+ t = Utils.getCompletionCause(t);
if (Log.trace()) {
Log.logTrace("Push completed on stream {0} for {1}{2}",
pushStream.streamid, resp,
@@ -516,34 +537,6 @@
return requestPseudoHeaders;
}
- @Override
- Response getResponse() throws IOException {
- try {
- if (request.duration() != null) {
- Log.logTrace("Waiting for response (streamid={0}, timeout={1}ms)",
- streamid,
- request.duration().toMillis());
- return getResponseAsync(null).get(
- request.duration().toMillis(), TimeUnit.MILLISECONDS);
- } else {
- Log.logTrace("Waiting for response (streamid={0})", streamid);
- return getResponseAsync(null).join();
- }
- } catch (TimeoutException e) {
- Log.logTrace("Response timeout (streamid={0})", streamid);
- throw new HttpTimeoutException("Response timed out");
- } catch (InterruptedException | ExecutionException | CompletionException e) {
- Throwable t = e.getCause();
- Log.logTrace("Response failed (streamid={0}): {1}", streamid, t);
- if (t instanceof IOException) {
- throw (IOException)t;
- }
- throw new IOException(e);
- } finally {
- Log.logTrace("Got response or failed (streamid={0})", streamid);
- }
- }
-
/** Sets endStreamReceived. Should be called only once. */
void setEndStreamReceived() {
assert remotelyClosed == false: "Unexpected endStream already set";
@@ -558,99 +551,219 @@
}
@Override
- void sendHeadersOnly() throws IOException, InterruptedException {
+ CompletableFuture<ExchangeImpl<T>> sendHeadersAsync() {
+ debug.log(Level.DEBUG, "sendHeadersOnly()");
if (Log.requests() && request != null) {
Log.logRequest(request.toString());
}
- requestContentLen = requestProcessor.contentLength();
+ if (requestPublisher != null) {
+ requestContentLen = requestPublisher.contentLength();
+ } else {
+ requestContentLen = 0;
+ }
OutgoingHeaders<Stream<T>> f = headerFrame(requestContentLen);
connection.sendFrame(f);
+ CompletableFuture<ExchangeImpl<T>> cf = new CompletableFuture<ExchangeImpl<T>>();
+ cf.complete(this); // #### good enough for now
+ return cf;
+ }
+
+ @Override
+ void released() {
+ if (streamid > 0) {
+ debug.log(Level.DEBUG, "Released stream %d", streamid);
+ // remove this stream from the Http2Connection map.
+ connection.closeStream(streamid);
+ } else {
+ debug.log(Level.DEBUG, "Can't release stream %d", streamid);
+ }
+ }
+
+ @Override
+ void completed() {
+ // There should be nothing to do here: the stream should have
+ // been already closed (or will be closed shortly after).
}
void registerStream(int id) {
this.streamid = id;
connection.putStream(this, streamid);
+ debug.log(Level.DEBUG, "Registered stream %d", id);
}
+ void signalWindowUpdate() {
+ RequestSubscriber subscriber = requestSubscriber;
+ assert subscriber != null;
+ debug.log(Level.DEBUG, "Signalling window update");
+ subscriber.sendScheduler.runOrSchedule();
+ }
+
+ static final ByteBuffer COMPLETED = ByteBuffer.allocate(0);
class RequestSubscriber implements Flow.Subscriber<ByteBuffer> {
// can be < 0 if the actual length is not known.
+ private final long contentLength;
private volatile long remainingContentLength;
private volatile Subscription subscription;
+ private volatile ByteBuffer current;
+ private final AtomicReference<Throwable> errorRef = new AtomicReference<>();
+ // A scheduler used to honor window updates. Writing must be paused
+ // when the window is exhausted, and resumed when the window acquires
+ // some space. The sendScheduler makes it possible to implement this
+ // behaviour in an asynchronous non-blocking way.
+ // See RequestSubscriber::trySend below.
+ final SequentialScheduler sendScheduler;
RequestSubscriber(long contentLen) {
+ this.contentLength = contentLen;
this.remainingContentLength = contentLen;
+ this.sendScheduler = new SequentialScheduler(
+ new SynchronizedRestartableTask(this::trySend));
}
@Override
public void onSubscribe(Flow.Subscription subscription) {
if (this.subscription != null) {
- throw new IllegalStateException();
+ throw new IllegalStateException("already subscribed");
}
this.subscription = subscription;
+ debug.log(Level.DEBUG, "RequestSubscriber: onSubscribe, request 1");
subscription.request(1);
}
@Override
public void onNext(ByteBuffer item) {
+ debug.log(Level.DEBUG,
+ "RequestSubscriber: onNext(%d)", item.remaining());
+ // Got some more request body bytes to send.
if (requestBodyCF.isDone()) {
- throw new IllegalStateException();
+ // stream already cancelled, probably in timeout
+ sendScheduler.stop();
+ subscription.cancel();
+ return;
+ }
+ ByteBuffer prev = current;
+ assert prev == null;
+ current = item;
+ sendScheduler.runOrSchedule();
+ }
+
+ @Override
+ public void onError(Throwable throwable) {
+ debug.log(Level.DEBUG,
+ () -> "RequestSubscriber: onError: " + throwable);
+ // ensure that errors are handled within the flow.
+ if (errorRef.compareAndSet(null, throwable)) {
+ sendScheduler.runOrSchedule();
}
+ }
+ @Override
+ public void onComplete() {
+ debug.log(Level.DEBUG, "RequestSubscriber: onComplete");
+ // last byte of request body has been obtained.
+ // ensure that everything is completed within the flow.
+ onNext(COMPLETED);
+ }
+
+ // Attempts to send the data, if any.
+ // Handles errors and completion state.
+ // Pause writing if the send window is exhausted, resume it if the
+ // send window has some bytes that can be acquired.
+ void trySend() {
try {
+ // handle errors raised by onError;
+ Throwable t = errorRef.get();
+ if (t != null) {
+ sendScheduler.stop();
+ if (requestBodyCF.isDone()) return;
+ subscription.cancel();
+ requestBodyCF.completeExceptionally(t);
+ return;
+ }
+
+ // handle COMPLETED;
+ ByteBuffer item = current;
+ if (item == null) return;
+ else if (item == COMPLETED) {
+ sendScheduler.stop();
+ complete();
+ return;
+ }
+
+ // handle bytes to send downstream
while (item.hasRemaining()) {
+ debug.log(Level.DEBUG, "trySend: %d", item.remaining());
assert !endStreamSent : "internal error, send data after END_STREAM flag";
DataFrame df = getDataFrame(item);
- if (remainingContentLength > 0) {
+ if (df == null) {
+ debug.log(Level.DEBUG, "trySend: can't send yet: %d",
+ item.remaining());
+ return; // the send window is exhausted: come back later
+ }
+
+ if (contentLength > 0) {
remainingContentLength -= df.getDataLength();
- assert remainingContentLength >= 0;
- if (remainingContentLength == 0) {
+ if (remainingContentLength < 0) {
+ String msg = connection().getConnectionFlow()
+ + " stream=" + streamid + " "
+ + "[" + Thread.currentThread().getName() +"] "
+ + "Too many bytes in request body. Expected: "
+ + contentLength + ", got: "
+ + (contentLength - remainingContentLength);
+ connection.resetStream(streamid, ResetFrame.PROTOCOL_ERROR);
+ throw new IOException(msg);
+ } else if (remainingContentLength == 0) {
df.setFlag(DataFrame.END_STREAM);
endStreamSent = true;
}
}
+ debug.log(Level.DEBUG, "trySend: sending: %d", df.getDataLength());
connection.sendDataFrame(df);
}
+ assert !item.hasRemaining();
+ current = null;
+ debug.log(Level.DEBUG, "trySend: request 1");
subscription.request(1);
- } catch (InterruptedException ex) {
+ } catch (Throwable ex) {
+ debug.log(Level.DEBUG, "trySend: ", ex);
+ sendScheduler.stop();
subscription.cancel();
requestBodyCF.completeExceptionally(ex);
}
}
- @Override
- public void onError(Throwable throwable) {
- if (requestBodyCF.isDone()) {
- return;
+ private void complete() throws IOException {
+ long remaining = remainingContentLength;
+ long written = contentLength - remaining;
+ if (remaining > 0) {
+ connection.resetStream(streamid, ResetFrame.PROTOCOL_ERROR);
+ // let trySend() handle the exception
+ throw new IOException(connection().getConnectionFlow()
+ + " stream=" + streamid + " "
+ + "[" + Thread.currentThread().getName() +"] "
+ + "Too few bytes returned by the publisher ("
+ + written + "/"
+ + contentLength + ")");
}
- subscription.cancel();
- requestBodyCF.completeExceptionally(throwable);
- }
-
- @Override
- public void onComplete() {
- assert endStreamSent || remainingContentLength < 0;
- try {
- if (!endStreamSent) {
- endStreamSent = true;
- connection.sendDataFrame(getEmptyEndStreamDataFrame());
- }
- requestBodyCF.complete(null);
- } catch (InterruptedException ex) {
- requestBodyCF.completeExceptionally(ex);
+ if (!endStreamSent) {
+ endStreamSent = true;
+ connection.sendDataFrame(getEmptyEndStreamDataFrame());
}
+ requestBodyCF.complete(null);
}
}
- DataFrame getDataFrame(ByteBuffer buffer) throws InterruptedException {
+ DataFrame getDataFrame(ByteBuffer buffer) {
int requestAmount = Math.min(connection.getMaxSendFrameSize(), buffer.remaining());
// blocks waiting for stream send window, if exhausted
- int actualAmount = windowController.tryAcquire(requestAmount, streamid);
+ int actualAmount = windowController.tryAcquire(requestAmount, streamid, this);
+ if (actualAmount <= 0) return null;
ByteBuffer outBuf = Utils.slice(buffer, actualAmount);
DataFrame df = new DataFrame(streamid, 0 , ByteBufferReference.of(outBuf));
return df;
}
- private DataFrame getEmptyEndStreamDataFrame() throws InterruptedException {
+ private DataFrame getEmptyEndStreamDataFrame() {
return new DataFrame(streamid, DataFrame.END_STREAM, new ByteBufferReference[0]);
}
@@ -666,7 +779,7 @@
@Override
CompletableFuture<Response> getResponseAsync(Executor executor) {
- CompletableFuture<Response> cf = null;
+ CompletableFuture<Response> cf;
// The code below deals with race condition that can be caused when
// completeResponse() is being called before getResponseAsync()
synchronized (response_cfs) {
@@ -693,7 +806,7 @@
PushGroup<?,?> pg = exchange.getPushGroup();
if (pg != null) {
// if an error occurs make sure it is recorded in the PushGroup
- cf = cf.whenComplete((t,e) -> pg.pushError(e));
+ cf = cf.whenComplete((t,e) -> pg.pushError(Utils.getCompletionCause(e)));
}
return cf;
}
@@ -763,9 +876,15 @@
}
CompletableFuture<Void> sendBodyImpl() {
- RequestSubscriber subscriber = new RequestSubscriber(requestContentLen);
- requestProcessor.subscribe(subscriber);
- requestBodyCF.whenComplete((v,t) -> requestSent());
+ requestBodyCF.whenComplete((v, t) -> requestSent());
+ if (requestPublisher != null) {
+ final RequestSubscriber subscriber = new RequestSubscriber(requestContentLen);
+ requestPublisher.subscribe(requestSubscriber = subscriber);
+ } else {
+ // there is no request body, therefore the request is complete,
+ // END_STREAM has already sent with outgoing headers
+ requestBodyCF.complete(null);
+ }
return requestBodyCF;
}
@@ -787,15 +906,23 @@
boolean closing;
if (closing = !closed) { // assigning closing to !closed
synchronized (this) {
+ failed = e;
if (closing = !closed) { // assigning closing to !closed
closed=true;
}
}
}
if (closing) { // true if the stream has not been closed yet
- inputQ.close();
+ if (responseSubscriber != null)
+ sched.runOrSchedule();
}
completeResponseExceptionally(e);
+ if (!requestBodyCF.isDone()) {
+ requestBodyCF.completeExceptionally(e); // we may be sending the body..
+ }
+ if (responseBodyCF != null) {
+ responseBodyCF.completeExceptionally(e);
+ }
try {
// will send a RST_STREAM frame
if (streamid != 0) {
@@ -814,7 +941,6 @@
closed = true;
}
Log.logTrace("Closing stream {0}", streamid);
- inputQ.close();
connection.closeStream(streamid);
Log.logTrace("Stream {0} closed", streamid);
}
@@ -860,18 +986,21 @@
@Override
CompletableFuture<ExchangeImpl<T>> sendBodyAsync() {
return super.sendBodyAsync()
- .whenComplete((ExchangeImpl<T> v, Throwable t) -> pushGroup.pushError(t));
+ .whenComplete((ExchangeImpl<T> v, Throwable t)
+ -> pushGroup.pushError(Utils.getCompletionCause(t)));
}
@Override
CompletableFuture<ExchangeImpl<T>> sendHeadersAsync() {
return super.sendHeadersAsync()
- .whenComplete((ExchangeImpl<T> ex, Throwable t) -> pushGroup.pushError(t));
+ .whenComplete((ExchangeImpl<T> ex, Throwable t)
+ -> pushGroup.pushError(Utils.getCompletionCause(t)));
}
@Override
CompletableFuture<Response> getResponseAsync(Executor executor) {
- CompletableFuture<Response> cf = pushCF.whenComplete((v, t) -> pushGroup.pushError(t));
+ CompletableFuture<Response> cf = pushCF.whenComplete(
+ (v, t) -> pushGroup.pushError(Utils.getCompletionCause(t)));
if(executor!=null && !cf.isDone()) {
cf = cf.thenApplyAsync( r -> r, executor);
}
@@ -890,7 +1019,7 @@
@Override
void completeResponse(Response r) {
- HttpResponseImpl.logResponse(r);
+ Log.logResponse(r::toString);
pushCF.complete(r); // not strictly required for push API
// start reading the body using the obtained BodyProcessor
CompletableFuture<Void> start = new MinimalFuture<>();
@@ -899,8 +1028,9 @@
if (t != null) {
responseCF.completeExceptionally(t);
} else {
- HttpResponseImpl<T> response = new HttpResponseImpl<>(r.request, r, body, getExchange());
- responseCF.complete(response);
+ HttpResponseImpl<T> resp =
+ new HttpResponseImpl<>(r.request, r, body, getExchange());
+ responseCF.complete(resp);
}
});
start.completeAsync(() -> null, getExchange().executor());
@@ -960,4 +1090,23 @@
}
}
+ /**
+ * Returns true if this exchange was canceled.
+ * @return true if this exchange was canceled.
+ */
+ synchronized boolean isCanceled() {
+ return failed != null;
+ }
+
+ /**
+ * Returns the cause for which this exchange was canceled, if available.
+ * @return the cause for which this exchange was canceled, if available.
+ */
+ synchronized Throwable getCancelCause() {
+ return failed;
+ }
+
+ final String dbgString() {
+ return connection.dbgString() + "/Stream("+streamid+")";
+ }
}
--- a/src/jdk.incubator.httpclient/share/classes/jdk/incubator/http/WindowController.java Sun Nov 05 17:05:57 2017 +0000
+++ b/src/jdk.incubator.httpclient/share/classes/jdk/incubator/http/WindowController.java Sun Nov 05 17:32:13 2017 +0000
@@ -1,5 +1,5 @@
/*
- * Copyright (c) 2016, Oracle and/or its affiliates. All rights reserved.
+ * Copyright (c) 2016, 2017, Oracle and/or its affiliates. All rights reserved.
* DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER.
*
* This code is free software; you can redistribute it and/or modify it
@@ -25,14 +25,19 @@
package jdk.incubator.http;
+import java.lang.System.Logger.Level;
+import java.util.ArrayList;
import java.util.Map;
import java.util.HashMap;
-import java.util.concurrent.locks.Condition;
+import java.util.Iterator;
+import java.util.LinkedHashMap;
+import java.util.List;
import java.util.concurrent.locks.ReentrantLock;
+import jdk.incubator.http.internal.common.Utils;
/**
- * A Simple blocking Send Window Flow-Controller that is used to control
- * outgoing Connection and Stream flows, per HTTP/2 connection.
+ * A Send Window Flow-Controller that is used to control outgoing Connection
+ * and Stream flows, per HTTP/2 connection.
*
* A Http2Connection has its own unique single instance of a WindowController
* that it shares with its Streams. Each stream must acquire the appropriate
@@ -44,6 +49,10 @@
*/
final class WindowController {
+ static final boolean DEBUG = Utils.DEBUG; // Revisit: temporary developer's flag
+ static final System.Logger DEBUG_LOGGER =
+ Utils.getDebugLogger("WindowController"::toString, DEBUG);
+
/**
* Default initial connection Flow-Control Send Window size, as per HTTP/2.
*/
@@ -54,11 +63,14 @@
/** A Map of the active streams, where the key is the stream id, and the
* value is the stream's Send Window size, which may be negative. */
private final Map<Integer,Integer> streams = new HashMap<>();
+ /** A Map of streams awaiting Send Window. The key is the stream id. The
+ * value is a pair of the Stream ( representing the key's stream id ) and
+ * the requested amount of send Window. */
+ private final Map<Integer, Map.Entry<Stream<?>, Integer>> pending
+ = new LinkedHashMap<>();
private final ReentrantLock controllerLock = new ReentrantLock();
- private final Condition notExhausted = controllerLock.newCondition();
-
/** A Controller with the default initial window size. */
WindowController() {
connectionWindowSize = DEFAULT_INITIAL_WINDOW_SIZE;
@@ -75,7 +87,8 @@
try {
Integer old = streams.put(streamid, initialStreamWindowSize);
if (old != null)
- throw new InternalError("Unexpected entry [" + old + "] for streamid: " + streamid);
+ throw new InternalError("Unexpected entry ["
+ + old + "] for streamid: " + streamid);
} finally {
controllerLock.unlock();
}
@@ -109,28 +122,45 @@
* 1) the requested amount, 2) the stream's Send Window, and 3) the
* connection's Send Window.
*
- * This method ( currently ) blocks until some positive amount of Send
- * Window is available.
+ * A negative or zero value is returned if there's no window available.
+ * When the result is negative or zero, this method arranges for the
+ * given stream's {@link Stream#signalWindowUpdate()} method to be invoke at
+ * a later time when the connection and/or stream window's have been
+ * increased. The {@code tryAcquire} method should then be invoked again to
+ * attempt to acquire the available window.
*/
- int tryAcquire(int requestAmount, int streamid) throws InterruptedException {
+ int tryAcquire(int requestAmount, int streamid, Stream<?> stream) {
controllerLock.lock();
try {
- int x = 0;
- Integer streamSize = 0;
- while (x <= 0) {
- streamSize = streams.get(streamid);
- if (streamSize == null)
- throw new InternalError("Expected entry for streamid: " + streamid);
- x = Math.min(requestAmount,
+ Integer streamSize = streams.get(streamid);
+ if (streamSize == null)
+ throw new InternalError("Expected entry for streamid: "
+ + streamid);
+ int x = Math.min(requestAmount,
Math.min(streamSize, connectionWindowSize));
- if (x <= 0) // stream window size may be negative
- notExhausted.await();
+ if (x <= 0) { // stream window size may be negative
+ DEBUG_LOGGER.log(Level.DEBUG,
+ "Stream %d requesting %d but only %d available (stream: %d, connection: %d)",
+ streamid, requestAmount, Math.min(streamSize, connectionWindowSize),
+ streamSize, connectionWindowSize);
+ // If there's not enough window size available, put the
+ // caller in a pending list.
+ pending.put(streamid, Map.entry(stream, requestAmount));
+ return x;
}
+ // Remove the caller from the pending list ( if was waiting ).
+ pending.remove(streamid);
+
+ // Update window sizes and return the allocated amount to the caller.
streamSize -= x;
streams.put(streamid, streamSize);
connectionWindowSize -= x;
+ DEBUG_LOGGER.log(Level.DEBUG,
+ "Stream %d amount allocated %d, now %d available (stream: %d, connection: %d)",
+ streamid, x, Math.min(streamSize, connectionWindowSize),
+ streamSize, connectionWindowSize);
return x;
} finally {
controllerLock.unlock();
@@ -140,10 +170,15 @@
/**
* Increases the Send Window size for the connection.
*
+ * A number of awaiting requesters, from unfulfilled tryAcquire requests,
+ * may have their stream's {@link Stream#signalWindowUpdate()} method
+ * scheduled to run ( i.e. awake awaiters ).
+ *
* @return false if, and only if, the addition of the given amount would
* cause the Send Window to exceed 2^31-1 (overflow), otherwise true
*/
boolean increaseConnectionWindow(int amount) {
+ List<Stream<?>> candidates = null;
controllerLock.lock();
try {
int size = connectionWindowSize;
@@ -151,20 +186,54 @@
if (size < 0)
return false;
connectionWindowSize = size;
- notExhausted.signalAll();
+ DEBUG_LOGGER.log(Level.DEBUG, "Connection window size is now %d", size);
+
+ // Notify waiting streams, until the new increased window size is
+ // effectively exhausted.
+ Iterator<Map.Entry<Integer,Map.Entry<Stream<?>,Integer>>> iter =
+ pending.entrySet().iterator();
+
+ while (iter.hasNext() && size > 0) {
+ Map.Entry<Integer,Map.Entry<Stream<?>,Integer>> item = iter.next();
+ Integer streamSize = streams.get(item.getKey());
+ if (streamSize == null) {
+ iter.remove();
+ } else {
+ Map.Entry<Stream<?>,Integer> e = item.getValue();
+ int requestedAmount = e.getValue();
+ // only wakes up the pending streams for which there is
+ // at least 1 byte of space in both windows
+ int minAmount = 1;
+ if (size >= minAmount && streamSize >= minAmount) {
+ size -= Math.min(streamSize, requestedAmount);
+ iter.remove();
+ if (candidates == null)
+ candidates = new ArrayList<>();
+ candidates.add(e.getKey());
+ }
+ }
+ }
} finally {
controllerLock.unlock();
}
+ if (candidates != null) {
+ candidates.forEach(Stream::signalWindowUpdate);
+ }
return true;
}
/**
* Increases the Send Window size for the given stream.
*
+ * If the given stream is awaiting window size, from an unfulfilled
+ * tryAcquire request, it will have its stream's {@link
+ * Stream#signalWindowUpdate()} method scheduled to run ( i.e. awoken ).
+ *
* @return false if, and only if, the addition of the given amount would
* cause the Send Window to exceed 2^31-1 (overflow), otherwise true
*/
boolean increaseStreamWindow(int amount, int streamid) {
+ Stream<?> s = null;
controllerLock.lock();
try {
Integer size = streams.get(streamid);
@@ -174,10 +243,27 @@
if (size < 0)
return false;
streams.put(streamid, size);
- notExhausted.signalAll();
+ DEBUG_LOGGER.log(Level.DEBUG,
+ "Stream %s window size is now %s", streamid, size);
+
+ Map.Entry<Stream<?>,Integer> p = pending.get(streamid);
+ if (p != null) {
+ int minAmount = 1;
+ // only wakes up the pending stream if there is at least
+ // 1 byte of space in both windows
+ if (size >= minAmount
+ && connectionWindowSize >= minAmount) {
+ pending.remove(streamid);
+ s = p.getKey();
+ }
+ }
} finally {
controllerLock.unlock();
}
+
+ if (s != null)
+ s.signalWindowUpdate();
+
return true;
}
@@ -199,6 +285,8 @@
Integer size = entry.getValue();
size += adjustAmount;
streams.put(streamid, size);
+ DEBUG_LOGGER.log(Level.DEBUG,
+ "Stream %s window size is now %s", streamid, size);
}
}
} finally {
@@ -228,4 +316,5 @@
controllerLock.unlock();;
}
}
+
}
--- a/src/jdk.incubator.httpclient/share/classes/jdk/incubator/http/WindowUpdateSender.java Sun Nov 05 17:05:57 2017 +0000
+++ b/src/jdk.incubator.httpclient/share/classes/jdk/incubator/http/WindowUpdateSender.java Sun Nov 05 17:32:13 2017 +0000
@@ -24,13 +24,18 @@
*/
package jdk.incubator.http;
+import java.lang.System.Logger.Level;
import jdk.incubator.http.internal.frame.SettingsFrame;
import jdk.incubator.http.internal.frame.WindowUpdateFrame;
+import jdk.incubator.http.internal.common.Utils;
import java.util.concurrent.atomic.AtomicInteger;
abstract class WindowUpdateSender {
+ final static boolean DEBUG = Utils.DEBUG;
+ final System.Logger debug =
+ Utils.getDebugLogger(this::dbgString, DEBUG);
final int limit;
final Http2Connection connection;
@@ -59,6 +64,7 @@
abstract int getStreamId();
void update(int delta) {
+ debug.log(Level.DEBUG, "update: %d", delta);
if (received.addAndGet(delta) > limit) {
synchronized (this) {
int tosend = received.get();
@@ -71,8 +77,12 @@
}
void sendWindowUpdate(int delta) {
+ debug.log(Level.DEBUG, "sending window update: %d", delta);
connection.sendUnorderedFrame(new WindowUpdateFrame(getStreamId(), delta));
}
+ String dbgString() {
+ return "WindowUpdateSender(stream: " + getStreamId() + ")";
+ }
}
--- a/src/jdk.incubator.httpclient/share/classes/jdk/incubator/http/internal/common/AsyncDataReadQueue.java Sun Nov 05 17:05:57 2017 +0000
+++ b/src/jdk.incubator.httpclient/share/classes/jdk/incubator/http/internal/common/AsyncDataReadQueue.java Sun Nov 05 17:32:13 2017 +0000
@@ -126,7 +126,7 @@
return;
}
- flushAsync(true);
+ flushAsync(false);
}
private static <T> boolean checkCanSet(String name, T oldval, Consumer<Throwable> onError) {
--- a/src/jdk.incubator.httpclient/share/classes/jdk/incubator/http/internal/common/AsyncWriteQueue.java Sun Nov 05 17:05:57 2017 +0000
+++ b/src/jdk.incubator.httpclient/share/classes/jdk/incubator/http/internal/common/AsyncWriteQueue.java Sun Nov 05 17:32:13 2017 +0000
@@ -44,7 +44,7 @@
* to the source queue by calling {@code source.setDelayed(buffers)}
* and return false. If all the data was successfully sent downstream
* then returns true.
- * @param buffers An array of ButeBufferReference containing data
+ * @param buffers An array of ByteBufferReference containing data
* to send downstream.
* @param source This AsyncWriteQueue.
* @return true if all the data could be sent downstream, false otherwise.
@@ -94,7 +94,7 @@
}
/**
- * retruns true if flushing was performed
+ * Returns true if flushing was performed
* @return
* @throws IOException
*/
--- /dev/null Thu Jan 01 00:00:00 1970 +0000
+++ b/src/jdk.incubator.httpclient/share/classes/jdk/incubator/http/internal/common/ConnectionExpiredException.java Sun Nov 05 17:32:13 2017 +0000
@@ -0,0 +1,57 @@
+/*
+ * Copyright (c) 2017, Oracle and/or its affiliates. All rights reserved.
+ * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER.
+ *
+ * This code is free software; you can redistribute it and/or modify it
+ * under the terms of the GNU General Public License version 2 only, as
+ * published by the Free Software Foundation. Oracle designates this
+ * particular file as subject to the "Classpath" exception as provided
+ * by Oracle in the LICENSE file that accompanied this code.
+ *
+ * This code is distributed in the hope that it will be useful, but WITHOUT
+ * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or
+ * FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License
+ * version 2 for more details (a copy is included in the LICENSE file that
+ * accompanied this code).
+ *
+ * You should have received a copy of the GNU General Public License version
+ * 2 along with this work; if not, write to the Free Software Foundation,
+ * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA.
+ *
+ * Please contact Oracle, 500 Oracle Parkway, Redwood Shores, CA 94065 USA
+ * or visit www.oracle.com if you need additional information or have any
+ * questions.
+ */
+
+package jdk.incubator.http.internal.common;
+
+import java.io.IOException;
+
+/**
+ * Signals that an end of file or end of stream has been reached
+ * unexpectedly before any protocol specific data has been received.
+ */
+public final class ConnectionExpiredException extends IOException {
+ private static final long serialVersionUID = 0;
+
+ /**
+ * Constructs a {@code ConnectionExpiredException} with the specified detail
+ * message.
+ *
+ * @param s the detail message
+ */
+ public ConnectionExpiredException(String s) {
+ super(s);
+ }
+
+ /**
+ * Constructs a {@code ConnectionExpiredException} with the specified detail
+ * message and cause.
+ *
+ * @param s the detail message
+ * @param cause the throwable cause
+ */
+ public ConnectionExpiredException(String s, Throwable cause) {
+ super(s, cause);
+ }
+}
--- /dev/null Thu Jan 01 00:00:00 1970 +0000
+++ b/src/jdk.incubator.httpclient/share/classes/jdk/incubator/http/internal/common/Demand.java Sun Nov 05 17:32:13 2017 +0000
@@ -0,0 +1,116 @@
+/*
+ * Copyright (c) 2017, Oracle and/or its affiliates. All rights reserved.
+ * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER.
+ *
+ * This code is free software; you can redistribute it and/or modify it
+ * under the terms of the GNU General Public License version 2 only, as
+ * published by the Free Software Foundation. Oracle designates this
+ * particular file as subject to the "Classpath" exception as provided
+ * by Oracle in the LICENSE file that accompanied this code.
+ *
+ * This code is distributed in the hope that it will be useful, but WITHOUT
+ * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or
+ * FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License
+ * version 2 for more details (a copy is included in the LICENSE file that
+ * accompanied this code).
+ *
+ * You should have received a copy of the GNU General Public License version
+ * 2 along with this work; if not, write to the Free Software Foundation,
+ * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA.
+ *
+ * Please contact Oracle, 500 Oracle Parkway, Redwood Shores, CA 94065 USA
+ * or visit www.oracle.com if you need additional information or have any
+ * questions.
+ */
+
+package jdk.incubator.http.internal.common;
+
+import java.util.concurrent.atomic.AtomicLong;
+
+/**
+ * Encapsulates operations with demand (Reactive Streams).
+ *
+ * <p> Demand is the aggregated number of elements requested by a Subscriber
+ * which is yet to be delivered (fulfilled) by the Publisher.
+ */
+public final class Demand {
+
+ private final AtomicLong val = new AtomicLong();
+
+ /**
+ * Increases this demand by the specified positive value.
+ *
+ * @param n
+ * increment {@code > 0}
+ *
+ * @return {@code true} iff prior to this operation this demand was fulfilled
+ */
+ public boolean increase(long n) {
+ if (n <= 0) {
+ throw new IllegalArgumentException(String.valueOf(n));
+ }
+ long prev = val.getAndAccumulate(n, (p, i) -> p + i < 0 ? Long.MAX_VALUE : p + i);
+ return prev == 0;
+ }
+
+ /**
+ * Tries to decrease this demand by the specified positive value.
+ *
+ * <p> The actual value this demand has been decreased by might be less than
+ * {@code n}, including {@code 0} (no decrease at all).
+ *
+ * @param n
+ * decrement {@code > 0}
+ *
+ * @return a value {@code m} ({@code 0 <= m <= n}) this demand has been
+ * actually decreased by
+ */
+ public long decreaseAndGet(long n) {
+ if (n <= 0) {
+ throw new IllegalArgumentException(String.valueOf(n));
+ }
+ long p, d;
+ do {
+ d = val.get();
+ p = Math.min(d, n);
+ } while (!val.compareAndSet(d, d - p));
+ return p;
+ }
+
+ /**
+ * Tries to decrease this demand by {@code 1}.
+ *
+ * @return {@code true} iff this demand has been decreased by {@code 1}
+ */
+ public boolean tryDecrement() {
+ return decreaseAndGet(1) == 1;
+ }
+
+ /**
+ * @return {@code true} iff there is no unfulfilled demand
+ */
+ public boolean isFulfilled() {
+ return val.get() == 0;
+ }
+
+ /**
+ * Resets this object to its initial state.
+ */
+ public void reset() {
+ val.set(0);
+ }
+
+ /**
+ * Returns the current value of this demand.
+ *
+ * @return the current value of this demand
+ */
+ public long get() {
+ return val.get();
+ }
+
+ @Override
+ public String toString() {
+ return String.valueOf(val.get());
+ }
+}
--- /dev/null Thu Jan 01 00:00:00 1970 +0000
+++ b/src/jdk.incubator.httpclient/share/classes/jdk/incubator/http/internal/common/FlowTube.java Sun Nov 05 17:32:13 2017 +0000
@@ -0,0 +1,202 @@
+/*
+ * Copyright (c) 2017, Oracle and/or its affiliates. All rights reserved.
+ * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER.
+ *
+ * This code is free software; you can redistribute it and/or modify it
+ * under the terms of the GNU General Public License version 2 only, as
+ * published by the Free Software Foundation. Oracle designates this
+ * particular file as subject to the "Classpath" exception as provided
+ * by Oracle in the LICENSE file that accompanied this code.
+ *
+ * This code is distributed in the hope that it will be useful, but WITHOUT
+ * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or
+ * FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License
+ * version 2 for more details (a copy is included in the LICENSE file that
+ * accompanied this code).
+ *
+ * You should have received a copy of the GNU General Public License version
+ * 2 along with this work; if not, write to the Free Software Foundation,
+ * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA.
+ *
+ * Please contact Oracle, 500 Oracle Parkway, Redwood Shores, CA 94065 USA
+ * or visit www.oracle.com if you need additional information or have any
+ * questions.
+ */
+
+package jdk.incubator.http.internal.common;
+
+import java.nio.ByteBuffer;
+import java.util.List;
+import java.util.concurrent.Flow;
+
+/**
+ * FlowTube is an I/O abstraction that allows reading from and writing to a
+ * destination asynchronously.
+ * This is not a {@link Flow.Processor
+ * Flow.Processor<List<ByteBuffer>, List<ByteBuffer>>},
+ * but rather models a publisher source and a subscriber sink in a bidirectional flow.
+ * <p>
+ * The {@code connectFlows} method should be called to connect the bidirectional
+ * flow. A FlowTube supports handing over the same read subscription to different
+ * sequential read subscribers over time. When {@code connectFlows(writePublisher,
+ * readSubscriber} is called, the FlowTube will call {@code dropSubscription} on
+ * its former readSubscriber, and {@code onConnection} on its new readSubscriber.
+ * By default, the implementation of {@code onConnection} is to call
+ * {@code onSubscribe}, but a subscriber that needs to subscribe sequentially
+ * several times to the same FlowTube may override the default implementation
+ * to ensure that {@code onSubscribe} is called only once.
+ *
+ */
+public interface FlowTube extends
+ Flow.Publisher<List<ByteBuffer>>,
+ Flow.Subscriber<List<ByteBuffer>> {
+
+ /**
+ * A subscriber for reading from the bidirectional flow.
+ * A TubeSubscriber is a {@code Flow.Subscriber} that can be canceled
+ * by calling {@code dropSubscription()}.
+ * Once {@code dropSubscription()} is called, the {@code TubeSubscriber}
+ * should stop calling any method on its subscription.
+ */
+ static interface TubeSubscriber extends Flow.Subscriber<List<ByteBuffer>> {
+ /**
+ * Called by {@code FlowTube.connectFlows}.
+ * @param subscription the subscription.
+ * @implSpec By default this method call {@code this.onSubscribe()}.
+ */
+ default void onConnection(Flow.Subscription subscription) {
+ onSubscribe(subscription);
+ }
+
+ /**
+ * Called when the flow is connected again, and the subscription
+ * is handed over to a new subscriber.
+ * Once {@code dropSubscription()} is called, the {@code TubeSubscriber}
+ * should stop calling any method on its subscription.
+ */
+ default void dropSubscription() { }
+
+ }
+
+ /**
+ * A publisher for writing to the bidirectional flow.
+ */
+ static interface TubePublisher extends Flow.Publisher<List<ByteBuffer>> {
+
+ }
+
+ /**
+ * Connects the bidirectional flows to a write {@code Publisher} and a
+ * read {@code Subscriber}. This method can be called sequentially
+ * several times to switch existing publishers and subscribers to a new
+ * pair of write subscriber and read publisher.
+ * @param writePublisher A new publisher for writing to the bidirectional flow.
+ * @param readSubscriber A new subscriber for reading from the bidirectional
+ * flow.
+ */
+ default void connectFlows(TubePublisher writePublisher,
+ TubeSubscriber readSubscriber) {
+
+ this.subscribe(readSubscriber);
+ writePublisher.subscribe(this);
+ }
+
+ /**
+ * Returns true if this flow was completed, either exceptionally
+ * or normally (EOF reached).
+ * @return true if the flow is finished
+ */
+ boolean isFinished();
+
+
+ /**
+ * Returns {@code s} if {@code s} is a {@code TubeSubscriber}, otherwise
+ * wraps it in a {@code TubeSubscriber}.
+ * Using the wrapper is only appropriate in the case where
+ * {@code dropSubscription} doesn't need to be implemented, and the
+ * {@code TubeSubscriber} is subscribed only once.
+ *
+ * @param s a subscriber for reading.
+ * @return a {@code TubeSubscriber}: either {@code s} if {@code s} is a
+ * {@code TubeSubscriber}, otherwise a {@code TubeSubscriber}
+ * wrapper that delegates to {@code s}
+ */
+ static TubeSubscriber asTubeSubscriber(Flow.Subscriber<? super List<ByteBuffer>> s) {
+ if (s instanceof TubeSubscriber) {
+ return (TubeSubscriber) s;
+ }
+ return new AbstractTubeSubscriber.TubeSubscriberWrapper(s);
+ }
+
+ /**
+ * Returns {@code s} if {@code s} is a {@code TubePublisher}, otherwise
+ * wraps it in a {@code TubePublisher}.
+ *
+ * @param p a publisher for writing.
+ * @return a {@code TubePublisher}: either {@code s} if {@code s} is a
+ * {@code TubePublisher}, otherwise a {@code TubePublisher}
+ * wrapper that delegates to {@code s}
+ */
+ static TubePublisher asTubePublisher(Flow.Publisher<List<ByteBuffer>> p) {
+ if (p instanceof TubePublisher) {
+ return (TubePublisher) p;
+ }
+ return new AbstractTubePublisher.TubePublisherWrapper(p);
+ }
+
+ /**
+ * Convenience abstract class for {@code TubePublisher} implementations.
+ * It is not required that a {@code TubePublisher} implementation extends
+ * this class.
+ */
+ static abstract class AbstractTubePublisher implements TubePublisher {
+ static final class TubePublisherWrapper extends AbstractTubePublisher {
+ final Flow.Publisher<List<ByteBuffer>> delegate;
+ public TubePublisherWrapper(Flow.Publisher<List<ByteBuffer>> delegate) {
+ this.delegate = delegate;
+ }
+ @Override
+ public void subscribe(Flow.Subscriber<? super List<ByteBuffer>> subscriber) {
+ delegate.subscribe(subscriber);
+ }
+ }
+ }
+
+ /**
+ * Convenience abstract class for {@code TubeSubscriber} implementations.
+ * It is not required that a {@code TubeSubscriber} implementation extends
+ * this class.
+ */
+ static abstract class AbstractTubeSubscriber implements TubeSubscriber {
+ static final class TubeSubscriberWrapper extends AbstractTubeSubscriber {
+ final Flow.Subscriber<? super List<ByteBuffer>> delegate;
+ TubeSubscriberWrapper(Flow.Subscriber<? super List<ByteBuffer>> delegate) {
+ this.delegate = delegate;
+ }
+ @Override
+ public void dropSubscription() {}
+ @Override
+ public void onConnection(Flow.Subscription subscription) {
+ delegate.onSubscribe(subscription);
+ }
+ @Override
+ public void onSubscribe(Flow.Subscription subscription) {
+ delegate.onSubscribe(subscription);
+ }
+ @Override
+ public void onNext(List<ByteBuffer> item) {
+ delegate.onNext(item);
+ }
+ @Override
+ public void onError(Throwable throwable) {
+ delegate.onError(throwable);
+ }
+ @Override
+ public void onComplete() {
+ delegate.onComplete();
+ }
+ }
+
+ }
+
+}
--- a/src/jdk.incubator.httpclient/share/classes/jdk/incubator/http/internal/common/HttpHeadersImpl.java Sun Nov 05 17:05:57 2017 +0000
+++ b/src/jdk.incubator.httpclient/share/classes/jdk/incubator/http/internal/common/HttpHeadersImpl.java Sun Nov 05 17:32:13 2017 +0000
@@ -38,7 +38,7 @@
/**
* Implementation of HttpHeaders.
*/
-public class HttpHeadersImpl implements HttpHeaders {
+public class HttpHeadersImpl extends HttpHeaders {
private final TreeMap<String,List<String>> headers;
@@ -47,17 +47,6 @@
}
@Override
- public Optional<String> firstValue(String name) {
- List<String> l = headers.get(name);
- return Optional.ofNullable(l == null ? null : l.get(0));
- }
-
- @Override
- public List<String> allValues(String name) {
- return headers.get(name);
- }
-
- @Override
public Map<String, List<String>> map() {
return Collections.unmodifiableMap(headers);
}
@@ -91,17 +80,6 @@
headers.put(name, values);
}
- @Override
- public OptionalLong firstValueAsLong(String name) {
- List<String> l = headers.get(name);
- if (l == null) {
- return OptionalLong.empty();
- } else {
- String v = l.get(0);
- return OptionalLong.of(Long.parseLong(v));
- }
- }
-
public void clear() {
headers.clear();
}
--- a/src/jdk.incubator.httpclient/share/classes/jdk/incubator/http/internal/common/Log.java Sun Nov 05 17:05:57 2017 +0000
+++ b/src/jdk.incubator.httpclient/share/classes/jdk/incubator/http/internal/common/Log.java Sun Nov 05 17:32:13 2017 +0000
@@ -27,6 +27,8 @@
import java.io.IOException;
import jdk.incubator.http.HttpHeaders;
+
+import java.util.ArrayList;
import java.util.List;
import java.util.Locale;
import java.util.Map;
@@ -36,6 +38,9 @@
import jdk.incubator.http.internal.frame.Http2Frame;
import jdk.incubator.http.internal.frame.WindowUpdateFrame;
+import javax.net.ssl.SNIServerName;
+import javax.net.ssl.SSLParameters;
+
/**
* -Djava.net.HttpClient.log=
* errors,requests,headers,
@@ -196,9 +201,9 @@
}
}
- public static void logResponse(String s, Object... s1) {
+ public static void logResponse(Supplier<String> supplier) {
if (requests()) {
- logger.log(Level.INFO, "RESPONSE: " + s, s1);
+ logger.log(Level.INFO, "RESPONSE: " + supplier.get());
}
}
@@ -227,6 +232,55 @@
}
}
+ public static void logParams(SSLParameters p) {
+ if (!Log.ssl()) {
+ return;
+ }
+
+ if (p == null) {
+ Log.logSSL("SSLParameters: Null params");
+ return;
+ }
+
+ final StringBuilder sb = new StringBuilder("SSLParameters:");
+ final List<Object> params = new ArrayList<>();
+ if (p.getCipherSuites() != null) {
+ for (String cipher : p.getCipherSuites()) {
+ sb.append("\n cipher: {")
+ .append(params.size()).append("}");
+ params.add(cipher);
+ }
+ }
+
+ // SSLParameters.getApplicationProtocols() can't return null
+ // JDK 8 EXCL START
+ for (String approto : p.getApplicationProtocols()) {
+ sb.append("\n application protocol: {")
+ .append(params.size()).append("}");
+ params.add(approto);
+ }
+ // JDK 8 EXCL END
+
+ if (p.getProtocols() != null) {
+ for (String protocol : p.getProtocols()) {
+ sb.append("\n protocol: {")
+ .append(params.size()).append("}");
+ params.add(protocol);
+ }
+ }
+
+ if (p.getServerNames() != null) {
+ for (SNIServerName sname : p.getServerNames()) {
+ sb.append("\n server name: {")
+ .append(params.size()).append("}");
+ params.add(sname.toString());
+ }
+ }
+ sb.append('\n');
+
+ Log.logSSL(sb.toString(), params.toArray());
+ }
+
public static void dumpHeaders(StringBuilder sb, String prefix, HttpHeaders headers) {
if (headers != null) {
Map<String,List<String>> h = headers.map();
--- a/src/jdk.incubator.httpclient/share/classes/jdk/incubator/http/internal/common/MinimalFuture.java Sun Nov 05 17:05:57 2017 +0000
+++ b/src/jdk.incubator.httpclient/share/classes/jdk/incubator/http/internal/common/MinimalFuture.java Sun Nov 05 17:32:13 2017 +0000
@@ -139,7 +139,7 @@
}
}
- public <U> MinimalFuture<U> newIncompleteFuture() {
+ public static <U> MinimalFuture<U> newMinimalFuture() {
return new MinimalFuture<>();
}
--- /dev/null Thu Jan 01 00:00:00 1970 +0000
+++ b/src/jdk.incubator.httpclient/share/classes/jdk/incubator/http/internal/common/SSLFlowDelegate.java Sun Nov 05 17:32:13 2017 +0000
@@ -0,0 +1,821 @@
+/*
+ * Copyright (c) 2017, Oracle and/or its affiliates. All rights reserved.
+ * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER.
+ *
+ * This code is free software; you can redistribute it and/or modify it
+ * under the terms of the GNU General Public License version 2 only, as
+ * published by the Free Software Foundation. Oracle designates this
+ * particular file as subject to the "Classpath" exception as provided
+ * by Oracle in the LICENSE file that accompanied this code.
+ *
+ * This code is distributed in the hope that it will be useful, but WITHOUT
+ * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or
+ * FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License
+ * version 2 for more details (a copy is included in the LICENSE file that
+ * accompanied this code).
+ *
+ * You should have received a copy of the GNU General Public License version
+ * 2 along with this work; if not, write to the Free Software Foundation,
+ * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA.
+ *
+ * Please contact Oracle, 500 Oracle Parkway, Redwood Shores, CA 94065 USA
+ * or visit www.oracle.com if you need additional information or have any
+ * questions.
+ */
+
+package jdk.incubator.http.internal.common;
+
+import java.io.IOException;
+import java.lang.System.Logger.Level;
+import java.nio.ByteBuffer;
+import java.util.concurrent.Executor;
+import java.util.concurrent.Flow;
+import java.util.concurrent.Flow.Subscriber;
+import java.util.List;
+import java.util.ArrayList;
+import java.util.Collections;
+import java.util.Iterator;
+import java.util.LinkedList;
+import java.util.concurrent.CompletableFuture;
+import java.util.concurrent.ConcurrentLinkedQueue;
+import java.util.concurrent.atomic.AtomicInteger;
+import javax.net.ssl.SSLEngine;
+import javax.net.ssl.SSLEngineResult;
+import javax.net.ssl.SSLEngineResult.HandshakeStatus;
+import javax.net.ssl.SSLEngineResult.Status;
+import javax.net.ssl.SSLException;
+
+/**
+ * Implements SSL using two SubscriberWrappers.
+ *
+ * <p> Constructor takes two Flow.Subscribers: one that receives the network
+ * data (after it has been encrypted by SSLFlowDelegate) data, and one that
+ * receives the application data (before it has been encrypted by SSLFlowDelegate).
+ *
+ * <p> Methods upstreamReader() and upstreamWriter() return the corresponding
+ * Flow.Subscribers containing Flows for the encrypted/decrypted upstream data.
+ * See diagram below.
+ *
+ * <p> How Flow.Subscribers are used in this class, and where they come from:
+ * <pre>
+ * {@code
+ *
+ *
+ *
+ * ---------> data flow direction
+ *
+ *
+ * +------------------+
+ * upstreamWriter | | downWriter
+ * ---------------> | | ------------>
+ * obtained from this | | supplied to constructor
+ * | SSLFlowDelegate |
+ * downReader | | upstreamReader
+ * <--------------- | | <--------------
+ * supplied to constructor | | obtained from this
+ * +------------------+
+ * }
+ * </pre>
+ */
+public class SSLFlowDelegate {
+
+ static final boolean DEBUG = Utils.DEBUG; // Revisit: temporary dev flag.
+ final System.Logger debug =
+ Utils.getDebugLogger(this::dbgString, DEBUG);
+
+ final Executor exec;
+ final Reader reader;
+ final Writer writer;
+ final SSLEngine engine;
+ final String tubeName; // hack
+ final CompletableFuture<Void> cf;
+ final CompletableFuture<String> alpnCF; // completes on initial handshake
+ final static ByteBuffer SENTINEL = Utils.EMPTY_BYTEBUFFER;
+
+ /**
+ * Creates an SSLFlowDelegate fed from two Flow.Subscribers. Each
+ * Flow.Subscriber requires an associated {@link CompletableFuture}
+ * for errors that need to be signaled from downstream to upstream.
+ */
+ public SSLFlowDelegate(SSLEngine engine,
+ Executor exec,
+ Subscriber<? super List<ByteBuffer>> downReader,
+ Subscriber<? super List<ByteBuffer>> downWriter)
+ {
+ this.tubeName = String.valueOf(downWriter);
+ this.reader = new Reader();
+ this.reader.subscribe(downReader);
+ this.writer = new Writer();
+ this.writer.subscribe(downWriter);
+ this.engine = engine;
+ this.exec = exec;
+ this.handshakeState = new AtomicInteger(NOT_HANDSHAKING);
+ this.cf = CompletableFuture.allOf(reader.completion(), writer.completion())
+ .thenRun(this::normalStop);
+ this.alpnCF = new CompletableFuture<>();
+ //Monitor.add(this::monitor);
+ }
+
+ /**
+ * Returns a CompletableFuture<String> which completes after
+ * the initial handshake completes, and which contains the negotiated
+ * alpn.
+ */
+ public CompletableFuture<String> alpn() {
+ return alpnCF;
+ }
+
+ private void setALPN() {
+ // Handshake is finished. So, can retrieve the ALPN now
+ if (alpnCF.isDone())
+ return;
+ String alpn = engine.getApplicationProtocol();
+ debug.log(Level.DEBUG, "setALPN = %s", alpn);
+ alpnCF.complete(alpn);
+ }
+
+ public String monitor() {
+ StringBuilder sb = new StringBuilder();
+ sb.append("SSL: HS state: " + states(handshakeState));
+ sb.append(" Engine state: " + engine.getHandshakeStatus().toString());
+ sb.append(" LL : ");
+ synchronized(stateList) {
+ for (String s: stateList) {
+ sb.append(s).append(" ");
+ }
+ }
+ sb.append("\r\n");
+ sb.append("Reader:: ").append(reader.toString());
+ sb.append("\r\n");
+ sb.append("Writer:: ").append(writer.toString());
+ sb.append("\r\n===================================");
+ return sb.toString();
+ }
+
+ /**
+ * Processing function for incoming data. Pass it thru SSLEngine.unwrap().
+ * Any decrypted buffers returned to be passed downstream.
+ * Status codes:
+ * NEED_UNWRAP: do nothing. Following incoming data will contain
+ * any required handshake data
+ * NEED_WRAP: call writer.addData() with empty buffer
+ * NEED_TASK: delegate task to executor
+ * BUFFER_OVERFLOW: allocate larger output buffer. Repeat unwrap
+ * BUFFER_UNDERFLOW: keep buffer and wait for more data
+ * OK: return generated buffers.
+ *
+ * Upstream subscription strategy is to try and keep no more than
+ * TARGET_BUFSIZE bytes in readBuf
+ */
+ class Reader extends SubscriberWrapper {
+ final SequentialScheduler scheduler;
+ static final int TARGET_BUFSIZE = 16 * 1024;
+ volatile ByteBuffer readBuf;
+ volatile boolean completing = false;
+ final Object readLock = new Object();
+ final System.Logger debugr =
+ Utils.getDebugLogger(this::dbgString, DEBUG);
+
+ class ReaderDownstreamPusher extends SequentialScheduler.CompleteRestartableTask {
+ @Override public void run() { processData(); }
+ }
+
+ Reader() {
+ super();
+ scheduler = new SequentialScheduler(new ReaderDownstreamPusher());
+ this.readBuf = ByteBuffer.allocate(1024);
+ readBuf.limit(0); // keep in read mode
+ }
+
+ public final String dbgString() {
+ return "SSL Reader(" + tubeName + ")";
+ }
+
+ /**
+ * entry point for buffers delivered from upstream Subscriber
+ */
+ @Override
+ public void incoming(List<ByteBuffer> buffers, boolean complete) {
+ debugr.log(Level.DEBUG, () -> "Adding " + Utils.remaining(buffers)
+ + " bytes to read buffer");
+ addToReadBuf(buffers);
+ if (complete) {
+ this.completing = true;
+ }
+ scheduler.runOrSchedule();
+ }
+
+ @Override
+ public String toString() {
+ return "READER: " + super.toString() + " readBuf: " + readBuf.toString()
+ + " count: " + count.toString();
+ }
+
+ private void reallocReadBuf() {
+ int sz = readBuf.capacity();
+ ByteBuffer newb = ByteBuffer.allocate(sz*2);
+ readBuf.flip();
+ Utils.copy(readBuf, newb);
+ readBuf = newb;
+ }
+
+ @Override
+ protected long upstreamWindowUpdate(long currentWindow, long downstreamQsize) {
+ if (readBuf.remaining() > TARGET_BUFSIZE) {
+ return 0;
+ } else {
+ return super.upstreamWindowUpdate(currentWindow, downstreamQsize);
+ }
+ }
+
+ // readBuf is kept ready for reading outside of this method
+ private void addToReadBuf(List<ByteBuffer> buffers) {
+ synchronized (readLock) {
+ for (ByteBuffer buf : buffers) {
+ readBuf.compact();
+ while (readBuf.remaining() < buf.remaining())
+ reallocReadBuf();
+ readBuf.put(buf);
+ readBuf.flip();
+ }
+ }
+ }
+
+ void schedule() {
+ scheduler.runOrSchedule();
+ }
+
+ void stop() {
+ debugr.log(Level.DEBUG, "stop");
+ scheduler.stop();
+ }
+
+ AtomicInteger count = new AtomicInteger(0);
+
+ // work function where it all happens
+ void processData() {
+ try {
+ debugr.log(Level.DEBUG, () -> "processData: " + readBuf.remaining()
+ + " bytes to unwrap "
+ + states(handshakeState));
+
+ while (readBuf.hasRemaining()) {
+ boolean handshaking = false;
+ try {
+ EngineResult result;
+ synchronized (readLock) {
+ result = unwrapBuffer(readBuf);
+ debugr.log(Level.DEBUG, "Unwrapped: %s", result.result);
+ }
+ if (result.status() == Status.BUFFER_UNDERFLOW) {
+ debugr.log(Level.DEBUG, "BUFFER_UNDERFLOW");
+ return;
+ }
+ if (completing && result.status() == Status.CLOSED) {
+ debugr.log(Level.DEBUG, "Closed: completing");
+ outgoing(Utils.EMPTY_BB_LIST, true);
+ return;
+ }
+ if (result.handshaking() && !completing) {
+ debugr.log(Level.DEBUG, "handshaking");
+ doHandshake(result, READER);
+ resumeActivity();
+ handshaking = true;
+ } else {
+ if ((handshakeState.getAndSet(NOT_HANDSHAKING) & ~DOING_TASKS) == HANDSHAKING) {
+ setALPN();
+ handshaking = false;
+ resumeActivity();
+ }
+ }
+ if (result.bytesProduced() > 0) {
+ debugr.log(Level.DEBUG, "sending %d", result.bytesProduced());
+ count.addAndGet(result.bytesProduced());
+ outgoing(result.destBuffer, false);
+ }
+ } catch (IOException ex) {
+ errorCommon(ex);
+ handleError(ex);
+ }
+ if (handshaking && !completing)
+ return;
+ }
+ if (completing) {
+ debugr.log(Level.DEBUG, "completing");
+ // Complete the alpnCF, if not already complete, regardless of
+ // whether or not the ALPN is available, there will be no more
+ // activity.
+ setALPN();
+ outgoing(Utils.EMPTY_BB_LIST, true);
+ }
+ } catch (Throwable ex) {
+ errorCommon(ex);
+ handleError(ex);
+ }
+ }
+ }
+ /**
+ * Returns a CompletableFuture which completes after all activity
+ * in the delegate is terminated (whether normally or exceptionally).
+ *
+ * @return
+ */
+ public CompletableFuture<Void> completion() {
+ return cf;
+ }
+
+ private String xxx(List<ByteBuffer> i) {
+ StringBuilder sb = new StringBuilder();
+ sb.append("xxx size=" + i.size());
+ int x = 0;
+ for (ByteBuffer b : i)
+ x += b.remaining();
+ sb.append(" total " + x);
+ return sb.toString();
+ }
+
+ public interface Monitorable {
+ public String getInfo();
+ }
+
+ public static class Monitor extends Thread {
+ final List<Monitorable> list;
+ static Monitor themon;
+
+ static {
+ themon = new Monitor();
+ themon.start(); // uncomment to enable Monitor
+ }
+
+ Monitor() {
+ super("Monitor");
+ setDaemon(true);
+ list = Collections.synchronizedList(new LinkedList<>());
+ }
+
+ void addTarget(Monitorable o) {
+ list.add(o);
+ }
+
+ public static void add(Monitorable o) {
+ themon.addTarget(o);
+ }
+
+ @Override
+ public void run() {
+ System.out.println("Monitor starting");
+ while (true) {
+ try {Thread.sleep(20*1000); } catch (Exception e) {}
+ synchronized (list) {
+ for (Monitorable o : list) {
+ System.out.println(o.getInfo());
+ System.out.println("-------------------------");
+ }
+ }
+ System.out.println("--o-o-o-o-o-o-o-o-o-o-o-o-o-o-");
+
+ }
+ }
+ }
+
+ /**
+ * Processing function for outgoing data. Pass it thru SSLEngine.wrap()
+ * Any encrypted buffers generated are passed downstream to be written.
+ * Status codes:
+ * NEED_UNWRAP: call reader.addData() with empty buffer
+ * NEED_WRAP: call addData() with empty buffer
+ * NEED_TASK: delegate task to executor
+ * BUFFER_OVERFLOW: allocate larger output buffer. Repeat wrap
+ * BUFFER_UNDERFLOW: shouldn't happen on writing side
+ * OK: return generated buffers
+ */
+ class Writer extends SubscriberWrapper {
+ final SequentialScheduler scheduler;
+ // queues of buffers received from upstream waiting
+ // to be processed by the SSLEngine
+ final List<ByteBuffer> writeList;
+ final System.Logger debugw =
+ Utils.getDebugLogger(this::dbgString, DEBUG);
+
+ class WriterDownstreamPusher extends SequentialScheduler.CompleteRestartableTask {
+ @Override public void run() { processData(); }
+ }
+
+ Writer() {
+ super();
+ writeList = Collections.synchronizedList(new LinkedList<>());
+ scheduler = new SequentialScheduler(new WriterDownstreamPusher());
+ }
+
+ @Override
+ protected void incoming(List<ByteBuffer> buffers, boolean complete) {
+ assert complete ? buffers == Utils.EMPTY_BB_LIST : true;
+ assert buffers != Utils.EMPTY_BB_LIST ? complete == false : true;
+ if (complete) {
+ writeList.add(SENTINEL);
+ } else {
+ writeList.addAll(buffers);
+ }
+ debugw.log(Level.DEBUG, () -> "added " + buffers.size()
+ + " (" + Utils.remaining(buffers)
+ + " bytes) to the writeList");
+ scheduler.runOrSchedule();
+ }
+
+ public final String dbgString() {
+ return "SSL Writer(" + tubeName + ")";
+ }
+
+ protected void onSubscribe() {
+ doHandshake(EngineResult.INIT, INIT);
+ resumeActivity();
+ }
+
+ void schedule() {
+ scheduler.runOrSchedule();
+ }
+
+ void stop() {
+ debugw.log(Level.DEBUG, "stop");
+ scheduler.stop();
+ }
+
+ private boolean isCompleting() {
+ synchronized(writeList) {
+ int lastIndex = writeList.size() - 1;
+ if (lastIndex < 0)
+ return false;
+ return writeList.get(lastIndex) == SENTINEL;
+ }
+ }
+
+ @Override
+ protected long upstreamWindowUpdate(long currentWindow, long downstreamQsize) {
+ if (writeList.size() > 10)
+ return 0;
+ else
+ return super.upstreamWindowUpdate(currentWindow, downstreamQsize);
+ }
+
+ private boolean hsTriggered() {
+ synchronized(writeList) {
+ for (ByteBuffer b : writeList)
+ if (b == HS_TRIGGER)
+ return true;
+ return false;
+ }
+ }
+
+ private void processData() {
+ boolean completing = isCompleting();
+
+ try {
+ debugw.log(Level.DEBUG, () -> "processData(" + Utils.remaining(writeList) + ")");
+ while (Utils.remaining(writeList) > 0 || hsTriggered()) {
+ ByteBuffer[] outbufs = writeList.toArray(Utils.EMPTY_BB_ARRAY);
+ EngineResult result = wrapBuffers(outbufs);
+ debugw.log(Level.DEBUG, "wrapBuffer returned %s", result.result);
+
+ if (result.status() == Status.CLOSED) {
+ if (result.bytesProduced() <= 0)
+ return;
+
+ completing = true;
+ // There could still be some outgoing data in outbufs.
+ writeList.add(SENTINEL);
+ }
+
+ boolean handshaking = false;
+ if (result.handshaking()) {
+ debugw.log(Level.DEBUG, "handshaking");
+ doHandshake(result, WRITER);
+ handshaking = true;
+ } else {
+ if ((handshakeState.getAndSet(NOT_HANDSHAKING) & ~DOING_TASKS) == HANDSHAKING) {
+ setALPN();
+ resumeActivity();
+ }
+ }
+ cleanList(writeList); // tidy up the source list
+ sendResultBytes(result);
+ if (handshaking && !completing) {
+ if (writeList.isEmpty() && !result.needUnwrap()) {
+ writer.addData(HS_TRIGGER);
+ }
+ return;
+ }
+ }
+ if (completing && Utils.remaining(writeList) == 0) {
+ /*
+ System.out.println("WRITER DOO 3");
+ engine.closeOutbound();
+ EngineResult result = wrapBuffers(Utils.EMPTY_BB_ARRAY);
+ sendResultBytes(result);
+ */
+ outgoing(Utils.EMPTY_BB_LIST, true);
+ return;
+ }
+ } catch (Throwable ex) {
+ handleError(ex);
+ }
+ }
+
+ private void sendResultBytes(EngineResult result) {
+ if (result.bytesProduced() > 0) {
+ debugw.log(Level.DEBUG, "Sending %d bytes downstream",
+ result.bytesProduced());
+ outgoing(result.destBuffer, false);
+ }
+ }
+
+ @Override
+ public String toString() {
+ return "WRITER: " + super.toString() +
+ " writeList size " + Integer.toString(writeList.size());
+ //" writeList: " + writeList.toString();
+ }
+ }
+
+ private void handleError(Throwable t) {
+ debug.log(Level.DEBUG, "handleError", t);
+ cf.completeExceptionally(t);
+ // no-op if already completed
+ alpnCF.completeExceptionally(t);
+ reader.stop();
+ writer.stop();
+ }
+
+ private void normalStop() {
+ reader.stop();
+ writer.stop();
+ }
+
+ private void cleanList(List<ByteBuffer> l) {
+ synchronized (l) {
+ Iterator<ByteBuffer> iter = l.iterator();
+ while (iter.hasNext()) {
+ ByteBuffer b = iter.next();
+ if (!b.hasRemaining()) {
+ iter.remove();
+ }
+ }
+ }
+ }
+
+ /**
+ * States for handshake. We avoid races when accessing/updating the AtomicInt
+ * because updates always schedule an additional call to both the read()
+ * and write() functions.
+ */
+ private static final int NOT_HANDSHAKING = 0;
+ private static final int HANDSHAKING = 1;
+ private static final int INIT = 2;
+ private static final int DOING_TASKS = 4; // bit added to above state
+ private static final ByteBuffer HS_TRIGGER = ByteBuffer.allocate(0);
+
+ private static final int READER = 1;
+ private static final int WRITER = 2;
+
+ private static String states(AtomicInteger state) {
+ int s = state.get();
+ StringBuilder sb = new StringBuilder();
+ int x = s & ~DOING_TASKS;
+ switch (x) {
+ case NOT_HANDSHAKING:
+ sb.append(" NOT_HANDSHAKING ");
+ break;
+ case HANDSHAKING:
+ sb.append(" HANDSHAKING ");
+ break;
+ case INIT:
+ sb.append(" INIT ");
+ break;
+ default:
+ throw new InternalError();
+ }
+ if ((s & DOING_TASKS) > 0)
+ sb.append("|DOING_TASKS");
+ return sb.toString();
+ }
+
+ private void resumeActivity() {
+ reader.schedule();
+ writer.schedule();
+ }
+
+ final AtomicInteger handshakeState;
+ final ConcurrentLinkedQueue<String> stateList = new ConcurrentLinkedQueue<>();
+
+ private void doHandshake(EngineResult r, int caller) {
+ int s = handshakeState.getAndAccumulate(HANDSHAKING, (current, update) -> update | (current & DOING_TASKS));
+ stateList.add(r.handshakeStatus().toString());
+ stateList.add(Integer.toString(caller));
+ switch (r.handshakeStatus()) {
+ case NEED_TASK:
+ if ((s & DOING_TASKS) > 0) // someone else was doing tasks
+ return;
+ List<Runnable> tasks = obtainTasks();
+ executeTasks(tasks);
+ break;
+ case NEED_WRAP:
+ writer.addData(HS_TRIGGER);
+ break;
+ case NEED_UNWRAP:
+ case NEED_UNWRAP_AGAIN:
+ // do nothing else
+ break;
+ default:
+ throw new InternalError("Unexpected handshake status:"
+ + r.handshakeStatus());
+ }
+ }
+
+ private List<Runnable> obtainTasks() {
+ List<Runnable> l = new ArrayList<>();
+ Runnable r;
+ while ((r = engine.getDelegatedTask()) != null) {
+ l.add(r);
+ }
+ return l;
+ }
+
+ private void executeTasks(List<Runnable> tasks) {
+ exec.execute(() -> {
+ handshakeState.getAndUpdate((current) -> current | DOING_TASKS);
+ try {
+ tasks.forEach((r) -> {
+ r.run();
+ });
+ } catch (Throwable t) {
+ handleError(t);
+ }
+ handshakeState.getAndUpdate((current) -> current & ~DOING_TASKS);
+ writer.addData(HS_TRIGGER);
+ resumeActivity();
+ });
+ }
+
+
+ EngineResult unwrapBuffer(ByteBuffer src) throws IOException {
+ ByteBuffer dst = getAppBuffer();
+ while (true) {
+ SSLEngineResult sslResult = engine.unwrap(src, dst);
+ switch (sslResult.getStatus()) {
+ case BUFFER_OVERFLOW:
+ // may happen only if app size buffer was changed.
+ // get it again if app buffer size changed
+ int appSize = engine.getSession().getApplicationBufferSize();
+ ByteBuffer b = ByteBuffer.allocate(appSize + dst.position());
+ dst.flip();
+ b.put(dst);
+ dst = b;
+ break;
+ case CLOSED:
+ doClosure();
+ return new EngineResult(sslResult);
+ case BUFFER_UNDERFLOW:
+ // handled implicitly by compaction/reallocation of readBuf
+ return new EngineResult(sslResult);
+ case OK:
+ dst.flip();
+ return new EngineResult(sslResult, dst);
+ }
+ }
+ }
+
+ // TODO: acknowledge a received CLOSE request from peer
+ void doClosure() throws IOException {
+ //while (!wrapAndSend(emptyArray))
+ //;
+ }
+
+ /**
+ * Returns the upstream Flow.Subscriber of the reading (incoming) side.
+ * This flow must be given the encrypted data read from upstream (eg socket)
+ * before it is decrypted.
+ */
+ public Flow.Subscriber<List<ByteBuffer>> upstreamReader() {
+ return reader;
+ }
+
+ /**
+ * Returns the upstream Flow.Subscriber of the writing (outgoing) side.
+ * This flow contains the plaintext data before it is encrypted.
+ */
+ public Flow.Subscriber<List<ByteBuffer>> upstreamWriter() {
+ return writer;
+ }
+
+ public void resumeReader() {
+ reader.schedule();
+ }
+
+ public void resetReaderDemand() {
+ reader.resetDownstreamDemand();
+ }
+
+ static class EngineResult {
+ final SSLEngineResult result;
+ final ByteBuffer destBuffer;
+
+ // normal result
+ EngineResult(SSLEngineResult result) {
+ this(result, null);
+ }
+
+ EngineResult(SSLEngineResult result, ByteBuffer destBuffer) {
+ this.result = result;
+ this.destBuffer = destBuffer;
+ }
+
+ // Special result used to trigger handshaking in constructor
+ static EngineResult INIT =
+ new EngineResult(
+ new SSLEngineResult(SSLEngineResult.Status.OK, HandshakeStatus.NEED_WRAP, 0, 0));
+
+ boolean handshaking() {
+ HandshakeStatus s = result.getHandshakeStatus();
+ return s != HandshakeStatus.FINISHED
+ && s != HandshakeStatus.NOT_HANDSHAKING
+ && result.getStatus() != Status.CLOSED;
+ }
+
+ boolean needUnwrap() {
+ HandshakeStatus s = result.getHandshakeStatus();
+ return s == HandshakeStatus.NEED_UNWRAP;
+ }
+
+
+ int bytesConsumed() {
+ return result.bytesConsumed();
+ }
+
+ int bytesProduced() {
+ return result.bytesProduced();
+ }
+
+ SSLEngineResult.HandshakeStatus handshakeStatus() {
+ return result.getHandshakeStatus();
+ }
+
+ SSLEngineResult.Status status() {
+ return result.getStatus();
+ }
+ }
+
+ public ByteBuffer getNetBuffer() {
+ return ByteBuffer.allocate(engine.getSession().getPacketBufferSize());
+ }
+
+ private ByteBuffer getAppBuffer() {
+ return ByteBuffer.allocate(engine.getSession().getApplicationBufferSize());
+ }
+
+ final String dbgString() {
+ return "SSLFlowDelegate(" + tubeName + ")";
+ }
+
+ @SuppressWarnings("fallthrough")
+ EngineResult wrapBuffers(ByteBuffer[] src) throws SSLException {
+ debug.log(Level.DEBUG, () -> "wrapping "
+ + Utils.remaining(src) + " bytes");
+ ByteBuffer dst = getNetBuffer();
+ while (true) {
+ SSLEngineResult sslResult = engine.wrap(src, dst);
+ debug.log(Level.DEBUG, () -> "SSLResult: " + sslResult);
+ switch (sslResult.getStatus()) {
+ case BUFFER_OVERFLOW:
+ // Shouldn't happen. We allocated buffer with packet size
+ // get it again if net buffer size was changed
+ debug.log(Level.DEBUG, "BUFFER_OVERFLOW");
+ int appSize = engine.getSession().getApplicationBufferSize();
+ ByteBuffer b = ByteBuffer.allocate(appSize + dst.position());
+ dst.flip();
+ b.put(dst);
+ dst = b;
+ break; // try again
+ case CLOSED:
+ debug.log(Level.DEBUG, "CLOSED");
+ // fallthrough. There could be some remaining data in dst.
+ // CLOSED will be handled by the caller.
+ case OK:
+ dst.flip();
+ final ByteBuffer dest = dst;
+ debug.log(Level.DEBUG, () -> "OK => produced: "
+ + dest.remaining()
+ + " not wrapped: "
+ + Utils.remaining(src));
+ return new EngineResult(sslResult, dest);
+ case BUFFER_UNDERFLOW:
+ // Shouldn't happen. Doesn't returns when wrap()
+ // underflow handled externally
+ // assert false : "Buffer Underflow";
+ debug.log(Level.DEBUG, "BUFFER_UNDERFLOW");
+ return new EngineResult(sslResult);
+ default:
+ debug.log(Level.DEBUG, "ASSERT");
+ assert false;
+ }
+ }
+ }
+}
--- /dev/null Thu Jan 01 00:00:00 1970 +0000
+++ b/src/jdk.incubator.httpclient/share/classes/jdk/incubator/http/internal/common/SSLTube.java Sun Nov 05 17:32:13 2017 +0000
@@ -0,0 +1,437 @@
+/*
+ * Copyright (c) 2017, Oracle and/or its affiliates. All rights reserved.
+ * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER.
+ *
+ * This code is free software; you can redistribute it and/or modify it
+ * under the terms of the GNU General Public License version 2 only, as
+ * published by the Free Software Foundation. Oracle designates this
+ * particular file as subject to the "Classpath" exception as provided
+ * by Oracle in the LICENSE file that accompanied this code.
+ *
+ * This code is distributed in the hope that it will be useful, but WITHOUT
+ * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or
+ * FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License
+ * version 2 for more details (a copy is included in the LICENSE file that
+ * accompanied this code).
+ *
+ * You should have received a copy of the GNU General Public License version
+ * 2 along with this work; if not, write to the Free Software Foundation,
+ * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA.
+ *
+ * Please contact Oracle, 500 Oracle Parkway, Redwood Shores, CA 94065 USA
+ * or visit www.oracle.com if you need additional information or have any
+ * questions.
+ */
+
+package jdk.incubator.http.internal.common;
+
+import java.lang.System.Logger.Level;
+import java.nio.ByteBuffer;
+import java.util.List;
+import java.util.Objects;
+import java.util.concurrent.CompletableFuture;
+import java.util.concurrent.Executor;
+import java.util.concurrent.Flow;
+import java.util.concurrent.atomic.AtomicReference;
+import java.util.function.Consumer;
+import javax.net.ssl.SSLEngine;
+import javax.net.ssl.SSLHandshakeException;
+import javax.net.ssl.SSLEngineResult.HandshakeStatus;
+import static javax.net.ssl.SSLEngineResult.HandshakeStatus.NOT_HANDSHAKING;
+import static javax.net.ssl.SSLEngineResult.HandshakeStatus.FINISHED;
+
+public class SSLTube implements FlowTube {
+
+ static final boolean DEBUG = Utils.DEBUG; // revisit: temporary developer's flag.
+ final System.Logger debug =
+ Utils.getDebugLogger(this::dbgString, DEBUG);
+
+ private final FlowTube tube;
+ private final SSLSubscriberWrapper readSubscriber;
+ private final SSLSubscriptionWrapper writeSubscription;
+ private final SSLFlowDelegate sslDelegate;
+ private final SSLEngine engine;
+ private volatile boolean finished;
+
+ public SSLTube(SSLEngine engine, Executor executor, FlowTube tube) {
+ Objects.requireNonNull(engine);
+ Objects.requireNonNull(executor);
+ this.tube = Objects.requireNonNull(tube);
+ writeSubscription = new SSLSubscriptionWrapper();
+ readSubscriber = new SSLSubscriberWrapper();
+ this.engine = engine;
+ sslDelegate = new SSLFlowDelegate(engine,
+ executor,
+ readSubscriber,
+ tube); // FIXME
+ tube.subscribe(sslDelegate.upstreamReader());
+ sslDelegate.upstreamWriter().onSubscribe(writeSubscription);
+ }
+
+ public CompletableFuture<String> getALPN() {
+ return sslDelegate.alpn();
+ }
+
+ @Override
+ public void subscribe(Flow.Subscriber<? super List<ByteBuffer>> s) {
+ readSubscriber.dropSubscription();
+ readSubscriber.setDelegate(s);
+ s.onSubscribe(readSubscription);
+ }
+
+ /**
+ * Tells whether, or not, this FlowTube has finished receiving data.
+ *
+ * @return true when one of this FlowTube Subscriber's OnError or onComplete
+ * methods have been invoked
+ */
+ @Override
+ public boolean isFinished() {
+ return finished;
+ }
+
+ private volatile Flow.Subscription readSubscription;
+
+ // The DelegateWrapper wraps a subscribed {@code Flow.Subscriber} and
+ // tracks the subscriber's state. In particular it makes sure that
+ // onComplete/onError are not called before onSubscribed.
+ final static class DelegateWrapper implements FlowTube.TubeSubscriber {
+ private final FlowTube.TubeSubscriber delegate;
+ volatile boolean subscribedCalled;
+ volatile boolean subscribedDone;
+ volatile boolean completed;
+ volatile Throwable error;
+ DelegateWrapper(Flow.Subscriber<? super List<ByteBuffer>> delegate) {
+ this.delegate = FlowTube.asTubeSubscriber(delegate);
+ }
+
+ @Override
+ public void dropSubscription() {
+ if (subscribedCalled) {
+ delegate.dropSubscription();
+ }
+ }
+
+ @Override
+ public void onNext(List<ByteBuffer> item) {
+ assert subscribedCalled;
+ delegate.onNext(item);
+ }
+
+ @Override
+ public void onSubscribe(Flow.Subscription subscription) {
+ onSubscribe(delegate::onSubscribe, subscription);
+ }
+
+ @Override
+ public void onConnection(Flow.Subscription subscription) {
+ onSubscribe(delegate::onConnection, subscription);
+ }
+
+ private void onSubscribe(Consumer<Flow.Subscription> method,
+ Flow.Subscription subscription) {
+ subscribedCalled = true;
+ method.accept(subscription);
+ Throwable x;
+ boolean finished;
+ synchronized (this) {
+ subscribedDone = true;
+ x = error;
+ finished = completed;
+ }
+ if (x != null) {
+ delegate.onError(x);
+ } else if (finished) {
+ delegate.onComplete();
+ }
+ }
+
+ @Override
+ public void onError(Throwable t) {
+ if (completed) return;
+ boolean subscribed;
+ synchronized (this) {
+ if (completed) return;
+ error = t;
+ completed = true;
+ subscribed = subscribedDone;
+ }
+ if (subscribed) {
+ delegate.onError(t);
+ }
+ }
+
+ @Override
+ public void onComplete() {
+ if (completed) return;
+ boolean subscribed;
+ synchronized (this) {
+ if (completed) return;
+ completed = true;
+ subscribed = subscribedDone;
+ }
+ if (subscribed) {
+ delegate.onComplete();
+ }
+ }
+
+ @Override
+ public String toString() {
+ return "DelegateWrapper:" + delegate.toString();
+ }
+
+ }
+
+ // Used to read data from the SSLTube.
+ final class SSLSubscriberWrapper implements FlowTube.TubeSubscriber {
+ private volatile DelegateWrapper delegate;
+ private volatile DelegateWrapper subscribed;
+ private volatile boolean onCompleteReceived;
+ private final AtomicReference<Throwable> errorRef
+ = new AtomicReference<>();
+
+ void setDelegate(Flow.Subscriber<? super List<ByteBuffer>> delegate) {
+ debug.log(Level.DEBUG, "SSLSubscriberWrapper (reader) got delegate: %s",
+ delegate);
+ assert delegate != null;
+ DelegateWrapper delegateWrapper = new DelegateWrapper(delegate);
+ Flow.Subscription subscription;
+ synchronized (this) {
+ this.delegate = delegateWrapper;
+ subscription = readSubscription;
+ }
+ if (subscription == null) {
+ debug.log(Level.DEBUG, "SSLSubscriberWrapper (reader) no subscription yet");
+ return;
+ }
+
+ onNewSubscription(delegateWrapper,
+ delegateWrapper::onSubscribe,
+ subscription);
+ }
+
+ @Override
+ public void dropSubscription() {
+ DelegateWrapper subscriberImpl = delegate;
+ if (subscriberImpl != null) {
+ subscriberImpl.dropSubscription();
+ }
+ }
+
+ @Override
+ public void onConnection(Flow.Subscription subscription) {
+ debug.log(Level.DEBUG,
+ "SSLSubscriberWrapper (reader) onConnection(%s)",
+ subscription);
+ assert subscription != null;
+ DelegateWrapper subscriberImpl;
+ synchronized (this) {
+ subscriberImpl = delegate;
+ readSubscription = subscription;
+ }
+ if (subscriberImpl == null) {
+ debug.log(Level.DEBUG,
+ "SSLSubscriberWrapper (reader) onConnection: no delegate yet");
+ return;
+ }
+ onNewSubscription(subscriberImpl,
+ subscriberImpl::onConnection,
+ subscription);
+ }
+
+ @Override
+ public void onSubscribe(Flow.Subscription subscription) {
+ debug.log(Level.DEBUG,
+ "SSLSubscriberWrapper (reader) onSubscribe(%s)",
+ subscription);
+ readSubscription = subscription;
+ assert subscription != null;
+ DelegateWrapper subscriberImpl;
+ synchronized (this) {
+ subscriberImpl = delegate;
+ readSubscription = subscription;
+ }
+ if (subscriberImpl == null) {
+ debug.log(Level.DEBUG,
+ "SSLSubscriberWrapper (reader) onSubscribe: no delegate yet");
+ return;
+ }
+ onNewSubscription(subscriberImpl,
+ subscriberImpl::onSubscribe,
+ subscription);
+ }
+
+ private void onNewSubscription(DelegateWrapper subscriberImpl,
+ Consumer<Flow.Subscription> method,
+ Flow.Subscription subscription) {
+ assert subscriberImpl != null;
+ assert method != null;
+ assert subscription != null;
+
+ Throwable failed;
+ boolean completed;
+ // reset any demand that may have been made by the previous
+ // subscriber
+ sslDelegate.resetReaderDemand();
+ // send the subscription to the subscriber.
+ method.accept(subscription);
+ // reschedule after calling onSubscribe (this should not be
+ // strictly needed as the first call to subscription.request()
+ // coming after resetting the demand should trigger it).
+ // However, it should not do any harm.
+ sslDelegate.resumeReader();
+
+ // The following twisted logic is just here that we don't invoke
+ // onError before onSubscribe. It also prevents race conditions
+ // if onError is invoked concurrently with setDelegate.
+ synchronized (this) {
+ failed = this.errorRef.get();
+ completed = finished;
+ if (delegate == subscriberImpl) {
+ subscribed = subscriberImpl;
+ }
+ }
+ if (failed != null) {
+ subscriberImpl.onError(failed);
+ } else if (completed) {
+ subscriberImpl.onComplete();
+ }
+ }
+
+ @Override
+ public void onNext(List<ByteBuffer> item) {
+ delegate.onNext(item);
+ }
+
+ public void onErrorImpl(Throwable throwable) {
+ // The following twisted logic is just here that we don't invoke
+ // onError before onSubscribe. It also prevents race conditions
+ // if onError is invoked concurrently with setDelegate.
+ // See setDelegate.
+
+ errorRef.compareAndSet(null, throwable);
+ Throwable failed = errorRef.get();
+ finished = true;
+ debug.log(Level.DEBUG, "%s: onErrorImpl: %s", this, throwable);
+ DelegateWrapper subscriberImpl;
+ synchronized (this) {
+ subscriberImpl = subscribed;
+ }
+ if (subscriberImpl != null) {
+ subscriberImpl.onError(failed);
+ } else {
+ debug.log(Level.DEBUG, "%s: delegate null, stored %s", this, failed);
+ }
+ }
+
+ @Override
+ public void onError(Throwable throwable) {
+ assert !finished && !onCompleteReceived;
+ onErrorImpl(throwable);
+ }
+
+ private boolean handshaking() {
+ HandshakeStatus hs = engine.getHandshakeStatus();
+ return !(hs == NOT_HANDSHAKING || hs == FINISHED);
+ }
+
+ @Override
+ public void onComplete() {
+ assert !finished && !onCompleteReceived;
+ onCompleteReceived = true;
+ DelegateWrapper subscriberImpl;
+ synchronized(this) {
+ subscriberImpl = subscribed;
+ }
+
+ if (handshaking()) {
+ onErrorImpl(new SSLHandshakeException(
+ "Remote host terminated the handshake"));
+ } else if (subscriberImpl != null) {
+ finished = true;
+ subscriberImpl.onComplete();
+ }
+ }
+ }
+
+ @Override
+ public void connectFlows(TubePublisher writePub,
+ TubeSubscriber readSub) {
+ debug.log(Level.DEBUG, "connecting flows");
+ readSubscriber.setDelegate(readSub);
+ writePub.subscribe(this);
+ }
+
+ /** Outstanding write demand from the SSL Flow Delegate. */
+ private final Demand writeDemand = new Demand();
+
+ final class SSLSubscriptionWrapper implements Flow.Subscription {
+
+ volatile Flow.Subscription delegate;
+
+ void setSubscription(Flow.Subscription sub) {
+ long demand = writeDemand.get(); // FIXME: isn't it a racy way of passing the demand?
+ delegate = sub;
+ debug.log(Level.DEBUG, "setSubscription: demand=%d", demand);
+ if (demand > 0)
+ sub.request(demand);
+ }
+
+ @Override
+ public void request(long n) {
+ writeDemand.increase(n);
+ debug.log(Level.DEBUG, "request: n=%d", n);
+ Flow.Subscription sub = delegate;
+ if (sub != null && n > 0) {
+ sub.request(n);
+ }
+ }
+
+ @Override
+ public void cancel() {
+ // TODO: no-op or error?
+ }
+ }
+
+ /* Subscriber - writing side */
+ @Override
+ public void onSubscribe(Flow.Subscription subscription) {
+ Objects.requireNonNull(subscription);
+ Flow.Subscription x = writeSubscription.delegate;
+ if (x != null)
+ x.cancel();
+
+ writeSubscription.setSubscription(subscription);
+ }
+
+ @Override
+ public void onNext(List<ByteBuffer> item) {
+ Objects.requireNonNull(item);
+ boolean decremented = writeDemand.tryDecrement();
+ assert decremented : "Unexpected writeDemand: ";
+ debug.log(Level.DEBUG,
+ "sending %d buffers to SSL flow delegate", item.size());
+ sslDelegate.upstreamWriter().onNext(item);
+ }
+
+ @Override
+ public void onError(Throwable throwable) {
+ Objects.requireNonNull(throwable);
+ sslDelegate.upstreamWriter().onError(throwable);
+ }
+
+ @Override
+ public void onComplete() {
+ sslDelegate.upstreamWriter().onComplete();
+ }
+
+ @Override
+ public String toString() {
+ return dbgString();
+ }
+
+ final String dbgString() {
+ return "SSLTube(" + tube + ")";
+ }
+
+}
--- /dev/null Thu Jan 01 00:00:00 1970 +0000
+++ b/src/jdk.incubator.httpclient/share/classes/jdk/incubator/http/internal/common/SequentialScheduler.java Sun Nov 05 17:32:13 2017 +0000
@@ -0,0 +1,346 @@
+/*
+ * Copyright (c) 2016, 2017, Oracle and/or its affiliates. All rights reserved.
+ * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER.
+ *
+ * This code is free software; you can redistribute it and/or modify it
+ * under the terms of the GNU General Public License version 2 only, as
+ * published by the Free Software Foundation. Oracle designates this
+ * particular file as subject to the "Classpath" exception as provided
+ * by Oracle in the LICENSE file that accompanied this code.
+ *
+ * This code is distributed in the hope that it will be useful, but WITHOUT
+ * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or
+ * FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License
+ * version 2 for more details (a copy is included in the LICENSE file that
+ * accompanied this code).
+ *
+ * You should have received a copy of the GNU General Public License version
+ * 2 along with this work; if not, write to the Free Software Foundation,
+ * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA.
+ *
+ * Please contact Oracle, 500 Oracle Parkway, Redwood Shores, CA 94065 USA
+ * or visit www.oracle.com if you need additional information or have any
+ * questions.
+ */
+
+package jdk.incubator.http.internal.common;
+
+import java.util.concurrent.Executor;
+import java.util.concurrent.atomic.AtomicInteger;
+
+import static java.util.Objects.requireNonNull;
+
+/**
+ * A scheduler of ( repeatable ) tasks that MUST be run sequentially.
+ *
+ * <p> This class can be used as a synchronization aid that assists a number of
+ * parties in running a task in a mutually exclusive fashion.
+ *
+ * <p> To run the task, a party invokes {@code runOrSchedule}. To permanently
+ * prevent the task from subsequent runs, the party invokes {@code stop}.
+ *
+ * <p> The parties can, but do not have to, operate in different threads.
+ *
+ * <p> The task can be either synchronous ( completes when its {@code run}
+ * method returns ), or asynchronous ( completed when its
+ * {@code DeferredCompleter} is explicitly completed ).
+ *
+ * <p> The next run of the task will not begin until the previous run has
+ * finished.
+ *
+ * <p> The task may invoke {@code runOrSchedule} itself, which may be a normal
+ * situation.
+ */
+public final class SequentialScheduler {
+
+ /*
+ Since the task is fixed and known beforehand, no blocking synchronization
+ (locks, queues, etc.) is required. The job can be done solely using
+ nonblocking primitives.
+
+ The machinery below addresses two problems:
+
+ 1. Running the task in a sequential order (no concurrent runs):
+
+ begin, end, begin, end...
+
+ 2. Avoiding indefinite recursion:
+
+ begin
+ end
+ begin
+ end
+ ...
+
+ Problem #1 is solved with a finite state machine with 4 states:
+
+ BEGIN, AGAIN, END, and STOP.
+
+ Problem #2 is solved with a "state modifier" OFFLOAD.
+
+ Parties invoke `runOrSchedule()` to signal the task must run. A party
+ that has invoked `runOrSchedule()` either begins the task or exploits the
+ party that is either beginning the task or ending it.
+
+ The party that is trying to end the task either ends it or begins it
+ again.
+
+ To avoid indefinite recursion, before re-running the task the
+ TryEndDeferredCompleter sets the OFFLOAD bit, signalling to its "child"
+ TryEndDeferredCompleter that this ("parent") TryEndDeferredCompleter is
+ available and the "child" must offload the task on to the "parent". Then
+ a race begins. Whichever invocation of TryEndDeferredCompleter.complete
+ manages to unset OFFLOAD bit first does not do the work.
+
+ There is at most 1 thread that is beginning the task and at most 2
+ threads that are trying to end it: "parent" and "child". In case of a
+ synchronous task "parent" and "child" are the same thread.
+ */
+
+ /**
+ * An interface to signal the completion of a {@link RestartableTask}.
+ *
+ * <p> The invocation of {@code complete} completes the task. The invocation
+ * of {@code complete} may restart the task, if an attempt has previously
+ * been made to run the task while it was already running.
+ *
+ * @apiNote {@code DeferredCompleter} is useful when a task is not necessary
+ * complete when its {@code run} method returns, but will complete at a
+ * later time, and maybe in different thread. This type exists for
+ * readability purposes at use-sites only.
+ */
+ public static abstract class DeferredCompleter {
+
+ /** Extensible from this (outer) class ONLY. */
+ private DeferredCompleter() { }
+
+ /** Completes the task. Must be called once, and once only. */
+ public abstract void complete();
+ }
+
+ /**
+ * A restartable task.
+ */
+ @FunctionalInterface
+ public interface RestartableTask {
+
+ /**
+ * The body of the task.
+ *
+ * @param taskCompleter
+ * A completer that must be invoked once, and only once,
+ * when this task is logically finished
+ */
+ void run(DeferredCompleter taskCompleter);
+ }
+
+ /**
+ * A complete restartable task is one which is simple and self-contained.
+ * It completes once its {@code run} method returns.
+ */
+ public static abstract class CompleteRestartableTask
+ implements RestartableTask
+ {
+ @Override
+ public final void run(DeferredCompleter taskCompleter) {
+ try {
+ run();
+ } finally {
+ taskCompleter.complete();
+ }
+ }
+
+ /** The body of the task. */
+ protected abstract void run();
+ }
+
+ /**
+ * A RestartableTask that runs its main loop within a
+ * synchronized block to place a memory barrier around it.
+ * Because the main loop can't run concurrently in two treads,
+ * then the lock shouldn't be contended and no deadlock should
+ * ever be possible.
+ */
+ public static final class SynchronizedRestartableTask
+ extends CompleteRestartableTask {
+ private final Runnable mainLoop;
+ private final Object lock = new Object();
+ public SynchronizedRestartableTask(Runnable mainLoop) {
+ this.mainLoop = mainLoop;
+ }
+
+ @Override
+ protected void run() {
+ synchronized(lock) {
+ mainLoop.run();
+ }
+ }
+ }
+
+ private static final int OFFLOAD = 1;
+ private static final int AGAIN = 2;
+ private static final int BEGIN = 4;
+ private static final int STOP = 8;
+ private static final int END = 16;
+
+ private final AtomicInteger state = new AtomicInteger(END);
+ private final RestartableTask restartableTask;
+ private final DeferredCompleter completer;
+ private final SchedulableTask schedulableTask;
+
+ /**
+ * A simple task that can be pushed on an executor to execute
+ * {@code restartableTask.run(completer)}.
+ */
+ private final class SchedulableTask implements Runnable {
+ @Override
+ public void run() {
+ restartableTask.run(completer);
+ }
+ }
+
+ public SequentialScheduler(RestartableTask restartableTask) {
+ this.restartableTask = requireNonNull(restartableTask);
+ this.completer = new TryEndDeferredCompleter();
+ this.schedulableTask = new SchedulableTask();
+ }
+
+ /**
+ * Runs or schedules the task to be run.
+ *
+ * @implSpec The recursion which is possible here must be bounded:
+ *
+ * <pre>{@code
+ * this.runOrSchedule()
+ * completer.complete()
+ * this.runOrSchedule()
+ * ...
+ * }</pre>
+ *
+ * @implNote The recursion in this implementation has the maximum
+ * depth of 1.
+ */
+ public void runOrSchedule() {
+ runOrSchedule(schedulableTask, null);
+ }
+
+ /**
+ * Runs or schedules the task to be run in the provided executor.
+ *
+ * <p> This method can be used when potential executing from a calling
+ * thread is not desirable.
+ *
+ * @param executor
+ * An executor in which to execute the task, if the task needs
+ * to be executed.
+ *
+ * @apiNote The given executor can be {@code null} in which case calling
+ * {@code deferOrSchedule(null)} is strictly equivalent to calling
+ * {@code runOrSchedule()}.
+ */
+ public void deferOrSchedule(Executor executor) { // TODO: why this name? why not runOrSchedule?
+ runOrSchedule(schedulableTask, executor);
+ }
+
+ private void runOrSchedule(SchedulableTask task, Executor executor) {
+ while (true) {
+ int s = state.get();
+ if (s == END) {
+ if (state.compareAndSet(END, BEGIN)) {
+ break;
+ }
+ } else if ((s & BEGIN) != 0) {
+ // Tries to change the state to AGAIN, preserving OFFLOAD bit
+ if (state.compareAndSet(s, AGAIN | (s & OFFLOAD))) {
+ return;
+ }
+ } else if ((s & AGAIN) != 0 || s == STOP) {
+ /* In the case of AGAIN the scheduler does not provide
+ happens-before relationship between actions prior to
+ runOrSchedule() and actions that happen in task.run().
+ The reason is that no volatile write is done in this case,
+ and the call piggybacks on the call that has actually set
+ AGAIN state. */
+ return;
+ } else {
+ // Non-existent state, or the one that cannot be offloaded
+ throw new InternalError(String.valueOf(s));
+ }
+ }
+ if (executor == null) {
+ task.run();
+ } else {
+ executor.execute(task);
+ }
+ }
+
+ /** The only concrete {@code DeferredCompleter} implementation. */
+ private class TryEndDeferredCompleter extends DeferredCompleter {
+
+ @Override
+ public void complete() {
+ while (true) {
+ int s;
+ while (((s = state.get()) & OFFLOAD) != 0) {
+ // Tries to offload ending of the task to the parent
+ if (state.compareAndSet(s, s & ~OFFLOAD)) {
+ return;
+ }
+ }
+ while (true) {
+ if ((s & OFFLOAD) != 0) {
+ /* OFFLOAD bit can never be observed here. Otherwise
+ it would mean there is another invocation of
+ "complete" that can run the task. */
+ throw new InternalError(String.valueOf(s));
+ }
+ if (s == BEGIN) {
+ if (state.compareAndSet(BEGIN, END)) {
+ return;
+ }
+ } else if (s == AGAIN) {
+ if (state.compareAndSet(AGAIN, BEGIN | OFFLOAD)) {
+ break;
+ }
+ } else if (s == STOP) {
+ return;
+ } else if (s == END) {
+ throw new IllegalStateException("Duplicate completion");
+ } else {
+ // Non-existent state
+ throw new InternalError(String.valueOf(s));
+ }
+ s = state.get();
+ }
+ restartableTask.run(completer);
+ }
+ }
+ }
+
+ /**
+ * Tells whether, or not, this scheduler has been permanently stopped.
+ *
+ * <p> Should be used from inside the task to poll the status of the
+ * scheduler, pretty much the same way as it is done for threads:
+ * <pre>{@code
+ * if (!Thread.currentThread().isInterrupted()) {
+ * ...
+ * }
+ * }</pre>
+ */
+ public boolean isStopped() {
+ return state.get() == STOP;
+ }
+
+ /**
+ * Stops this scheduler. Subsequent invocations of {@code runOrSchedule}
+ * are effectively no-ops.
+ *
+ * <p> If the task has already begun, this invocation will not affect it,
+ * unless the task itself uses {@code isStopped()} method to check the state
+ * of the handler.
+ */
+ public void stop() {
+ state.set(STOP);
+ }
+}
--- /dev/null Thu Jan 01 00:00:00 1970 +0000
+++ b/src/jdk.incubator.httpclient/share/classes/jdk/incubator/http/internal/common/SubscriberWrapper.java Sun Nov 05 17:32:13 2017 +0000
@@ -0,0 +1,412 @@
+/*
+ * Copyright (c) 2017, Oracle and/or its affiliates. All rights reserved.
+ * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER.
+ *
+ * This code is free software; you can redistribute it and/or modify it
+ * under the terms of the GNU General Public License version 2 only, as
+ * published by the Free Software Foundation. Oracle designates this
+ * particular file as subject to the "Classpath" exception as provided
+ * by Oracle in the LICENSE file that accompanied this code.
+ *
+ * This code is distributed in the hope that it will be useful, but WITHOUT
+ * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or
+ * FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License
+ * version 2 for more details (a copy is included in the LICENSE file that
+ * accompanied this code).
+ *
+ * You should have received a copy of the GNU General Public License version
+ * 2 along with this work; if not, write to the Free Software Foundation,
+ * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA.
+ *
+ * Please contact Oracle, 500 Oracle Parkway, Redwood Shores, CA 94065 USA
+ * or visit www.oracle.com if you need additional information or have any
+ * questions.
+ */
+
+package jdk.incubator.http.internal.common;
+
+import java.io.Closeable;
+import java.lang.System.Logger.Level;
+import java.nio.ByteBuffer;
+import java.util.List;
+import java.util.Objects;
+import java.util.concurrent.CompletableFuture;
+import java.util.concurrent.ConcurrentLinkedQueue;
+import java.util.concurrent.Flow;
+import java.util.concurrent.Flow.Subscriber;
+import java.util.concurrent.atomic.AtomicLong;
+import java.util.concurrent.atomic.AtomicReference;
+
+/**
+ * A wrapper for a Flow.Subscriber. This wrapper delivers data to the wrapped
+ * Subscriber which is supplied to the constructor. This class takes care of
+ * downstream flow control automatically and upstream flow control automatically
+ * by default.
+ * <p>
+ * Processing is done by implementing the {@link #incoming(List, boolean)} method
+ * which supplies buffers from upstream. This method (or any other method)
+ * can then call the outgoing() method to deliver processed buffers downstream.
+ * <p>
+ * Upstream error signals are delivered downstream directly. Cancellation from
+ * downstream is also propagated upstream immediately.
+ * <p>
+ * Each SubscriberWrapper has a {@link java.util.concurrent.CompletableFuture}{@code <Void>}
+ * which propagates completion/errors from downstream to upstream. Normal completion
+ * can only occur after onComplete() is called, but errors can be propagated upwards
+ * at any time.
+ */
+public abstract class SubscriberWrapper
+ implements FlowTube.TubeSubscriber, Closeable, Flow.Processor<List<ByteBuffer>,List<ByteBuffer>>
+ // TODO: SSLTube Subscriber will never change? Does this really need to be a TS?
+{
+ static final boolean DEBUG = Utils.DEBUG;; // Revisit: temporary dev flag.
+ final System.Logger logger =
+ Utils.getDebugLogger(this::dbgString, DEBUG);
+
+ volatile Flow.Subscription upstreamSubscription;
+ final SubscriptionBase downstreamSubscription;
+ volatile boolean upstreamCompleted;
+ volatile boolean downstreamCompleted;
+ volatile boolean completionAcknowledged;
+ private volatile Subscriber<? super List<ByteBuffer>> downstreamSubscriber;
+ // Input Q and lo and hi pri output Qs.
+ private final ConcurrentLinkedQueue<List<ByteBuffer>> inputQ;
+ private final CompletableFuture<Void> cf;
+ private final SequentialScheduler pushScheduler;
+ private final AtomicReference<Throwable> errorRef = new AtomicReference<>();
+
+ /**
+ * Wraps the given downstream subscriber. For each call to {@link
+ * #onNext(List<ByteBuffer>) } the given filter function is invoked
+ * and the list (if not empty) returned is passed downstream.
+ *
+ * A {@code CompletableFuture} is supplied which can be used to signal an
+ * error from downstream and which terminates the wrapper or which signals
+ * completion of downstream activity which can be propagated upstream. Error
+ * completion can be signaled at any time, but normal completion must not be
+ * signaled before onComplete() is called.
+ */
+ public SubscriberWrapper()
+ {
+ this.inputQ = new ConcurrentLinkedQueue<>();
+ this.cf = new CompletableFuture<>();
+ this.pushScheduler = new SequentialScheduler(new DownstreamPusher());
+ this.downstreamSubscription = new SubscriptionBase(pushScheduler,
+ this::downstreamCompletion);
+ }
+
+ @Override
+ public final void subscribe(Subscriber<? super List<ByteBuffer>> downstreamSubscriber) {
+ Objects.requireNonNull(downstreamSubscriber);
+ this.downstreamSubscriber = downstreamSubscriber;
+ }
+
+ /**
+ * Wraps the given downstream wrapper in this. For each call to
+ * {@link #onNext(List<ByteBuffer>) } the incoming() method is called.
+ *
+ * The {@code downstreamCF} from the downstream wrapper is linked to this
+ * wrappers notifier.
+ *
+ * @param downstreamWrapper downstream destination
+ */
+ public SubscriberWrapper(Subscriber<? super List<ByteBuffer>> downstreamWrapper)
+ {
+ this();
+ subscribe(downstreamWrapper);
+ }
+
+ /**
+ * Delivers data to be processed by this wrapper. Generated data to be sent
+ * downstream, must be provided to the {@link #outgoing(List, boolean)}}
+ * method.
+ *
+ * @param buffers a List of ByteBuffers.
+ * @param complete if true then no more data will be added to the list
+ */
+ protected abstract void incoming(List<ByteBuffer> buffers, boolean complete);
+
+ /**
+ * This method is called to determine the window size to use at any time. The
+ * current window is supplied together with the current downstream queue size.
+ * {@code 0} should be returned if no change is
+ * required or a positive integer which will be added to the current window.
+ * The default implementation maintains a downstream queue size of no greater
+ * than 5. The method can be overridden if required.
+ *
+ * @param currentWindow the current upstream subscription window
+ * @param downstreamQsize the current number of buffers waiting to be sent
+ * downstream
+ *
+ * @return value to add to currentWindow
+ */
+ protected long upstreamWindowUpdate(long currentWindow, long downstreamQsize) {
+ if (downstreamQsize > 5) {
+ return 0;
+ }
+
+ if (currentWindow == 0) {
+ return 1;
+ } else {
+ return 0;
+ }
+ }
+
+ /**
+ * Override this if anything needs to be done after the upstream subscriber
+ * has subscribed
+ */
+ protected void onSubscribe() {
+ }
+
+ /**
+ * Delivers buffers of data downstream. After incoming()
+ * has been called complete == true signifying completion of the upstream
+ * subscription, data may continue to be delivered, up to when outgoing() is
+ * called complete == true, after which, the downstream subscription is
+ * completed.
+ *
+ * It's an error to call outgoing() with complete = true if incoming() has
+ * not previously been called with it.
+ */
+ public void outgoing(ByteBuffer buffer, boolean complete) {
+ Objects.requireNonNull(buffer);
+ assert !complete || !buffer.hasRemaining();
+ outgoing(List.of(buffer), complete);
+ }
+
+ public void outgoing(List<ByteBuffer> buffers, boolean complete) {
+ Objects.requireNonNull(buffers);
+ if (complete) {
+ assert Utils.remaining(buffers) == 0;
+ logger.log(Level.DEBUG, "completionAcknowledged");
+ if (!upstreamCompleted)
+ throw new IllegalStateException("upstream not completed");
+ completionAcknowledged = true;
+ } else {
+ logger.log(Level.DEBUG, () -> "Adding "
+ + Utils.remaining(buffers)
+ + " to inputQ queue");
+ inputQ.add(buffers);
+ }
+ logger.log(Level.DEBUG, () -> "pushScheduler "
+ + (pushScheduler.isStopped() ? " is stopped!" : " is alive"));
+ pushScheduler.runOrSchedule();
+ }
+
+ /**
+ * Returns a CompletableFuture which completes when this wrapper completes.
+ * Normal completion happens with the following steps (in order):
+ * 1. onComplete() is called
+ * 2. incoming() called with complete = true
+ * 3. outgoing() may continue to be called normally
+ * 4. outgoing called with complete = true
+ * 5. downstream subscriber is called onComplete()
+ *
+ * If the subscription is canceled or onComplete() is invoked the
+ * CompletableFuture completes exceptionally. Exceptional completion
+ * also occurs if downstreamCF completes exceptionally.
+ */
+ public CompletableFuture<Void> completion() {
+ return cf;
+ }
+
+ /**
+ * Invoked whenever it 'may' be possible to push buffers downstream.
+ */
+ class DownstreamPusher extends SequentialScheduler.CompleteRestartableTask {
+ @Override
+ public void run() {
+ try {
+ run1();
+ } catch (Throwable t) {
+ errorCommon(t);
+ }
+ }
+
+ private void run1() {
+ if (downstreamCompleted) {
+ logger.log(Level.DEBUG, "DownstreamPusher: downstream is already completed");
+ return;
+ }
+
+ // If there was an error, send it downstream.
+ Throwable error = errorRef.get();
+ if (error != null) {
+ synchronized(this) {
+ if (downstreamCompleted) return;
+ downstreamCompleted = true;
+ }
+ logger.log(Level.DEBUG,
+ () -> "DownstreamPusher: forwarding error downstream: " + error);
+ pushScheduler.stop();
+ inputQ.clear();
+ downstreamSubscriber.onError(error);
+ return;
+ }
+
+ // OK - no error, let's proceed
+ if (!inputQ.isEmpty()) {
+ logger.log(Level.DEBUG,
+ "DownstreamPusher: queue not empty, downstreamSubscription: %s",
+ downstreamSubscription);
+ } else {
+ logger.log(Level.DEBUG,
+ "DownstreamPusher: queue empty, downstreamSubscription: %s",
+ downstreamSubscription);
+ }
+
+ final boolean dbgOn = logger.isLoggable(Level.DEBUG);
+ while (!inputQ.isEmpty() && downstreamSubscription.tryDecrement()) {
+ List<ByteBuffer> b = inputQ.poll();
+ if (dbgOn) logger.log(Level.DEBUG,
+ "DownstreamPusher: Pushing "
+ + Utils.remaining(b)
+ + " bytes downstream");
+ downstreamSubscriber.onNext(b);
+ }
+ upstreamWindowUpdate();
+ checkCompletion();
+ }
+ }
+
+ AtomicLong upstreamWindow = new AtomicLong(0);
+
+ void upstreamWindowUpdate() {
+ long downstreamQueueSize = inputQ.size();
+ long n = upstreamWindowUpdate(upstreamWindow.get(), downstreamQueueSize);
+ if (n > 0)
+ upstreamRequest(n);
+ }
+
+ @Override
+ public void onSubscribe(Flow.Subscription subscription) {
+ if (upstreamSubscription != null) {
+ throw new IllegalStateException("Single shot publisher");
+ }
+ this.upstreamSubscription = subscription;
+ upstreamRequest(upstreamWindowUpdate(0, 0));
+ logger.log(Level.DEBUG,
+ "calling downstreamSubscriber::onSubscribe on %s",
+ downstreamSubscriber);
+ downstreamSubscriber.onSubscribe(downstreamSubscription);
+ onSubscribe();
+ }
+
+ @Override
+ public void onNext(List<ByteBuffer> item) {
+ logger.log(Level.DEBUG, "onNext");
+ long prev = upstreamWindow.getAndDecrement();
+ if (prev <= 0)
+ throw new IllegalStateException("invalid onNext call");
+ incomingCaller(item, false);
+ upstreamWindowUpdate();
+ }
+
+ private void upstreamRequest(long n) {
+ logger.log(Level.DEBUG, "requesting %d", n);
+ upstreamWindow.getAndAdd(n);
+ upstreamSubscription.request(n);
+ }
+
+ public long upstreamWindow() {
+ return upstreamWindow.get();
+ }
+
+ @Override
+ public void onError(Throwable throwable) {
+ logger.log(Level.DEBUG, () -> "onError: " + throwable);
+ errorCommon(Objects.requireNonNull(throwable));
+ }
+
+ protected boolean errorCommon(Throwable throwable) {
+ assert throwable != null;
+ if (errorRef.compareAndSet(null, throwable)) {
+ logger.log(Level.DEBUG, "error", throwable);
+ pushScheduler.runOrSchedule();
+ upstreamCompleted = true;
+ cf.completeExceptionally(throwable);
+ return true;
+ }
+ return false;
+ }
+
+ @Override
+ public void close() {
+ errorCommon(new RuntimeException("wrapper closed"));
+ }
+
+ private void incomingCaller(List<ByteBuffer> l, boolean complete) {
+ try {
+ incoming(l, complete);
+ } catch(Throwable t) {
+ errorCommon(t);
+ }
+ }
+
+ @Override
+ public void onComplete() {
+ logger.log(Level.DEBUG, () -> "upstream completed: " + toString());
+ upstreamCompleted = true;
+ incomingCaller(Utils.EMPTY_BB_LIST, true);
+ checkCompletion();
+ pushScheduler.runOrSchedule();
+ }
+
+ /** Adds the given data to the input queue. */
+ public void addData(ByteBuffer l) {
+ if (upstreamSubscription == null) {
+ throw new IllegalStateException("can't add data before upstream subscriber subscribes");
+ }
+ incomingCaller(List.of(l), false);
+ }
+
+ void checkCompletion() {
+ if (downstreamCompleted || !upstreamCompleted) {
+ return;
+ }
+ if (!inputQ.isEmpty()) {
+ return;
+ }
+ if (errorRef.get() != null) {
+ pushScheduler.runOrSchedule();
+ return;
+ }
+ if (completionAcknowledged) {
+ downstreamSubscriber.onComplete();
+ // Fix me subscriber.onComplete.run();
+ downstreamCompleted = true;
+ cf.complete(null);
+ }
+ }
+
+ // called from the downstream Subscription.cancel()
+ void downstreamCompletion() {
+ upstreamSubscription.cancel();
+ cf.complete(null);
+ }
+
+ public void resetDownstreamDemand() {
+ downstreamSubscription.demand.reset();
+ }
+
+ @Override
+ public String toString() {
+ StringBuilder sb = new StringBuilder();
+ sb.append("SubscriberWrapper:")
+ .append(" upstreamCompleted: ").append(Boolean.toString(upstreamCompleted))
+ .append(" upstreamWindow: ").append(upstreamWindow.toString())
+ .append(" downstreamCompleted: ").append(Boolean.toString(downstreamCompleted))
+ .append(" completionAcknowledged: ").append(Boolean.toString(completionAcknowledged))
+ .append(" inputQ size: ").append(Integer.toString(inputQ.size()))
+ //.append(" inputQ: ").append(inputQ.toString())
+ .append(" cf: ").append(cf.toString())
+ .append(" downstreamSubscription: ").append(downstreamSubscription.toString());
+
+ return sb.toString();
+ }
+
+ public String dbgString() {
+ return "SubscriberWrapper";
+ }
+}
--- /dev/null Thu Jan 01 00:00:00 1970 +0000
+++ b/src/jdk.incubator.httpclient/share/classes/jdk/incubator/http/internal/common/SubscriptionBase.java Sun Nov 05 17:32:13 2017 +0000
@@ -0,0 +1,88 @@
+/*
+ * Copyright (c) 2017, Oracle and/or its affiliates. All rights reserved.
+ * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER.
+ *
+ * This code is free software; you can redistribute it and/or modify it
+ * under the terms of the GNU General Public License version 2 only, as
+ * published by the Free Software Foundation. Oracle designates this
+ * particular file as subject to the "Classpath" exception as provided
+ * by Oracle in the LICENSE file that accompanied this code.
+ *
+ * This code is distributed in the hope that it will be useful, but WITHOUT
+ * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or
+ * FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License
+ * version 2 for more details (a copy is included in the LICENSE file that
+ * accompanied this code).
+ *
+ * You should have received a copy of the GNU General Public License version
+ * 2 along with this work; if not, write to the Free Software Foundation,
+ * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA.
+ *
+ * Please contact Oracle, 500 Oracle Parkway, Redwood Shores, CA 94065 USA
+ * or visit www.oracle.com if you need additional information or have any
+ * questions.
+ */
+
+package jdk.incubator.http.internal.common;
+
+import java.util.concurrent.Flow;
+import java.util.concurrent.atomic.AtomicBoolean;
+
+/**
+ * Maintains subscription counter and provides primitives for
+ * - accessing window
+ * - reducing window when delivering items externally
+ * - resume delivery when window was zero previously
+ *
+ * @author mimcmah
+ */
+public class SubscriptionBase implements Flow.Subscription {
+
+ final Demand demand = new Demand();
+
+ final SequentialScheduler scheduler; // when window was zero and is opened, run this
+ final Runnable cancelAction; // when subscription cancelled, run this
+ final AtomicBoolean cancelled;
+
+ public SubscriptionBase(SequentialScheduler scheduler, Runnable cancelAction) {
+ this.scheduler = scheduler;
+ this.cancelAction = cancelAction;
+ this.cancelled = new AtomicBoolean(false);
+ }
+
+ @Override
+ public void request(long n) {
+ if (demand.increase(n))
+ scheduler.runOrSchedule();
+ }
+
+
+
+ @Override
+ public synchronized String toString() {
+ return "SubscriptionBase: window = " + demand.get() +
+ " cancelled = " + cancelled.toString();
+ }
+
+ /**
+ * Returns true if the window was reduced by 1. In that case
+ * items must be supplied to subscribers and the scheduler run
+ * externally. If the window could not be reduced by 1, then false
+ * is returned and the scheduler will run later when the window is updated.
+ */
+ public boolean tryDecrement() {
+ return demand.tryDecrement();
+ }
+
+ public long window() {
+ return demand.get();
+ }
+
+ @Override
+ public void cancel() {
+ if (cancelled.getAndSet(true))
+ return;
+ scheduler.stop();
+ cancelAction.run();
+ }
+}
--- /dev/null Thu Jan 01 00:00:00 1970 +0000
+++ b/src/jdk.incubator.httpclient/share/classes/jdk/incubator/http/internal/common/SynchronousPublisher.java Sun Nov 05 17:32:13 2017 +0000
@@ -0,0 +1,506 @@
+/*
+ * Copyright (c) 2017, Oracle and/or its affiliates. All rights reserved.
+ * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER.
+ *
+ * This code is free software; you can redistribute it and/or modify it
+ * under the terms of the GNU General Public License version 2 only, as
+ * published by the Free Software Foundation. Oracle designates this
+ * particular file as subject to the "Classpath" exception as provided
+ * by Oracle in the LICENSE file that accompanied this code.
+ *
+ * This code is distributed in the hope that it will be useful, but WITHOUT
+ * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or
+ * FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License
+ * version 2 for more details (a copy is included in the LICENSE file that
+ * accompanied this code).
+ *
+ * You should have received a copy of the GNU General Public License version
+ * 2 along with this work; if not, write to the Free Software Foundation,
+ * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA.
+ *
+ * Please contact Oracle, 500 Oracle Parkway, Redwood Shores, CA 94065 USA
+ * or visit www.oracle.com if you need additional information or have any
+ * questions.
+ */
+
+package jdk.incubator.http.internal.common;
+
+import java.util.Map;
+import java.util.Objects;
+import java.util.Optional;
+import java.util.WeakHashMap;
+import java.util.concurrent.Flow.Publisher;
+import java.util.concurrent.Flow.Subscriber;
+import java.util.concurrent.Flow.Subscription;
+import java.util.concurrent.atomic.AtomicInteger;
+import java.util.concurrent.atomic.AtomicReference;
+import java.util.concurrent.locks.ReentrantLock;
+
+/**
+ * This publisher signals {@code onNext} synchronously and
+ * {@code onComplete}/{@code onError} asynchronously to its only subscriber.
+ *
+ * <p> This publisher supports a single subscriber over this publisher's
+ * lifetime. {@code signalComplete} and {@code signalError} may be called before
+ * the subscriber has subscribed.
+ *
+ * <p> The subscriber's requests are signalled to the subscription supplied to
+ * the {@code feedback} method.
+ *
+ * <p> {@code subscribe} and {@code feedback} methods can be called in any
+ * order.
+ *
+ * <p> {@code signalNext} may be called recursively, the implementation will
+ * bound the depth of the recursion.
+ *
+ * <p> It is always an error to call {@code signalNext} without a sufficient
+ * demand.
+ *
+ * <p> If subscriber throws an exception from any of its methods, the
+ * subscription will be cancelled.
+ */
+public final class SynchronousPublisher<T> implements Publisher<T> {
+ /*
+ * PENDING, ACTIVE and CANCELLED are states. TERMINATE and DELIVERING are
+ * state modifiers, they cannot appear in the state on their own.
+ *
+ * PENDING, ACTIVE and CANCELLED are mutually exclusive states. Any two of
+ * those bits cannot be set at the same time in state.
+ *
+ * PENDING -----------------> ACTIVE <------> DELIVERING
+ * | |
+ * +------> TERMINATE <------+
+ * | | |
+ * | v |
+ * +------> CANCELLED <------+
+ *
+ * The following states are allowed:
+ *
+ * PENDING
+ * PENDING | TERMINATE,
+ * ACTIVE,
+ * ACTIVE | DELIVERING,
+ * ACTIVE | TERMINATE,
+ * ACTIVE | DELIVERING | TERMINATE
+ * CANCELLED
+ */
+ /**
+ * A state modifier meaning {@code onSubscribe} has not been called yet.
+ *
+ * <p> After {@code onSubscribe} has been called the machine can transition
+ * into {@code ACTIVE}, {@code PENDING | TERMINATE} or {@code CANCELLED}.
+ */
+ private static final int PENDING = 1;
+ /**
+ * A state modifier meaning {@code onSubscribe} has been called, no error
+ * and no completion has been signalled and {@code onNext} may be called.
+ */
+ private static final int ACTIVE = 2;
+ /**
+ * A state modifier meaning no calls to subscriber may be made.
+ *
+ * <p> Once this modifier is set, it will not be unset. It's a final state.
+ */
+ private static final int CANCELLED = 4;
+ /**
+ * A state modifier meaning {@code onNext} is being called and no other
+ * signal may be made.
+ *
+ * <p> This bit can be set at any time. signalNext uses it to ensure the
+ * method is called sequentially.
+ */
+ private static final int DELIVERING = 8;
+ /**
+ * A state modifier meaning the next call must be either {@code onComplete}
+ * or {@code onError}.
+ *
+ * <p> The concrete method depends on the value of {@code terminationType}).
+ * {@code TERMINATE} bit cannot appear on its own, it can be set only with
+ * {@code PENDING} or {@code ACTIVE}.
+ */
+ private static final int TERMINATE = 16;
+ /**
+ * Current demand. If fulfilled, no {@code onNext} signals may be made.
+ */
+ private final Demand demand = new Demand();
+ /**
+ * The current state of the subscription. Contains disjunctions of the above
+ * state modifiers.
+ */
+ private final AtomicInteger state = new AtomicInteger(PENDING);
+ /**
+ * A convenient way to represent 3 values: not set, completion and error.
+ */
+ private final AtomicReference<Optional<Throwable>> terminationType
+ = new AtomicReference<>();
+ /**
+ * {@code signalNext} uses this lock to ensure the method is called in a
+ * thread-safe manner.
+ */
+ private final ReentrantLock nextLock = new ReentrantLock();
+ private T next;
+
+ private final Object lock = new Object();
+ /**
+ * This map stores the subscribers attempted to subscribe to this publisher.
+ * It is needed so this publisher does not call {@code onSubscribe} on a
+ * subscriber more than once (Rule 2.12).
+ *
+ * <p> It will most likely have a single entry for the only subscriber.
+ * Because this publisher is one-off, subscribing to it more than once is an
+ * error.
+ */
+ private final Map<Subscriber<?>, Object> knownSubscribers
+ = new WeakHashMap<>(1, 1);
+ /**
+ * The active subscriber. This reference will be reset to {@code null} once
+ * the subscription becomes cancelled (Rule 3.13).
+ */
+ private volatile Subscriber<? super T> subscriber;
+ /**
+ * A temporary subscription that receives all calls to
+ * {@code request}/{@code cancel} until two things happen: (1) the feedback
+ * becomes set and (2) {@code onSubscribe} method is called on the
+ * subscriber.
+ *
+ * <p> The first condition is obvious. The second one is about not
+ * propagating requests to {@code feedback} until {@code onSubscribe} call
+ * has been finished. The reason is that Rule 1.3 requires the subscriber
+ * methods to be called in a thread-safe manner. This, in particular,
+ * implies that if called from multiple threads, the calls must not be
+ * concurrent. If, for instance, {@code subscription.request(long)) (and
+ * this is a usual state of affairs) is called from within
+ * {@code onSubscribe} call, the publisher will have to resort to some sort
+ * of queueing (locks, queues, etc.) of possibly arriving {@code onNext}
+ * signals while in {@code onSubscribe}. This publisher doesn't queue
+ * signals, instead it "queues" requests. Because requests are just numbers
+ * and requests are additive, the effective queue is a single number of
+ * total requests made so far.
+ */
+ private final TemporarySubscription temporarySubscription
+ = new TemporarySubscription();
+ private volatile Subscription feedback;
+ /**
+ * Keeping track of whether a subscription may be made. (The {@code
+ * subscriber} field may later become {@code null}, but this flag is
+ * permanent. Once {@code true} forever {@code true}.
+ */
+ private boolean subscribed;
+
+ @Override
+ public void subscribe(Subscriber<? super T> sub) {
+ Objects.requireNonNull(sub);
+ boolean success = false;
+ boolean duplicate = false;
+ synchronized (lock) {
+ if (!subscribed) {
+ subscribed = true;
+ subscriber = sub;
+ assert !knownSubscribers.containsKey(subscriber);
+ knownSubscribers.put(subscriber, null);
+ success = true;
+ } else if (sub.equals(subscriber)) {
+ duplicate = true;
+ } else if (!knownSubscribers.containsKey(sub)) {
+ knownSubscribers.put(sub, null);
+ } else {
+ return;
+ }
+ }
+ if (success) {
+ signalSubscribe();
+ } else if (duplicate) {
+ signalError(new IllegalStateException("Duplicate subscribe"));
+ } else {
+ // This is a best-effort attempt for an isolated publisher to call
+ // a foreign subscriber's methods in a sequential order. However it
+ // cannot be guaranteed unless all publishers share information on
+ // all subscribers in the system. This publisher does its job right.
+ sub.onSubscribe(new NopSubscription());
+ sub.onError(new IllegalStateException("Already subscribed"));
+ }
+ }
+
+ /**
+ * Accepts a subscription that is signalled with the subscriber's requests.
+ *
+ * @throws NullPointerException
+ * if {@code subscription} is {@code null}
+ * @throws IllegalStateException
+ * if there is a feedback subscription already
+ */
+ public void feedback(Subscription subscription) {
+ Objects.requireNonNull(subscription);
+ synchronized (lock) {
+ if (feedback != null) {
+ throw new IllegalStateException(
+ "Already has a feedback subscription");
+ }
+ feedback = subscription;
+ if ((state.get() & PENDING) == 0) {
+ temporarySubscription.replaceWith(new PermanentSubscription());
+ }
+ }
+ }
+
+ /**
+ * Tries to deliver the specified item to the subscriber.
+ *
+ * <p> The item may not be delivered even if there is a demand. This can
+ * happen as a result of subscriber cancelling the subscription by
+ * signalling {@code cancel} or this publisher cancelling the subscription
+ * by signaling {@code onError} or {@code onComplete}.
+ *
+ * <p> Given no exception is thrown, a call to this method decremented the
+ * demand.
+ *
+ * @param item
+ * the item to deliver to the subscriber
+ *
+ * @return {@code true} iff the subscriber has received {@code item}
+ * @throws NullPointerException
+ * if {@code item} is {@code null}
+ * @throws IllegalStateException
+ * if there is no demand
+ * @throws IllegalStateException
+ * the method is called concurrently
+ */
+ public boolean signalNext(T item) {
+ Objects.requireNonNull(item);
+ if (!nextLock.tryLock()) {
+ throw new IllegalStateException("Concurrent signalling");
+ }
+ boolean recursion = false;
+ try {
+ next = item;
+ while (true) {
+ int s = state.get();
+ if ((s & DELIVERING) == DELIVERING) {
+ recursion = true;
+ break;
+ } else if (state.compareAndSet(s, s | DELIVERING)) {
+ break;
+ }
+ }
+ if (!demand.tryDecrement()) {
+ // Hopefully this will help to find bugs in this publisher's
+ // clients. Because signalNext should never be issues without
+ // having a sufficient demand. Even if the thing is cancelled!
+// next = null;
+ throw new IllegalStateException("No demand");
+ }
+ if (recursion) {
+ return true;
+ }
+ while (next != null) {
+ int s = state.get();
+ if ((s & (ACTIVE | TERMINATE)) == (ACTIVE | TERMINATE)) {
+ if (state.compareAndSet(
+ s, CANCELLED | (s & ~(ACTIVE | TERMINATE)))) {
+ // terminationType must be read only after the
+ // termination condition has been observed
+ // (those have been stored in the opposite order)
+ Optional<Throwable> t = terminationType.get();
+ dispatchTerminationAndUnsubscribe(t);
+ return false;
+ }
+ } else if ((s & ACTIVE) == ACTIVE) {
+ try {
+ T t = next;
+ next = null;
+ subscriber.onNext(t);
+ } catch (Throwable t) {
+ cancelNow();
+ throw t;
+ }
+ } else if ((s & CANCELLED) == CANCELLED) {
+ return false;
+ } else if ((s & PENDING) == PENDING) {
+ // Actually someone called signalNext even before
+ // onSubscribe has been called, but from this publisher's
+ // API point of view it's still "No demand"
+ throw new IllegalStateException("No demand");
+ } else {
+ throw new InternalError(String.valueOf(s));
+ }
+ }
+ return true;
+ } finally {
+ while (!recursion) { // If the call was not recursive unset the bit
+ int s = state.get();
+ if ((s & DELIVERING) != DELIVERING) {
+ throw new InternalError(String.valueOf(s));
+ } else if (state.compareAndSet(s, s & ~DELIVERING)) {
+ break;
+ }
+ }
+ nextLock.unlock();
+ }
+ }
+
+ /**
+ * Cancels the subscription by signalling {@code onError} to the subscriber.
+ *
+ * <p> Will not signal {@code onError} if the subscription has been
+ * cancelled already.
+ *
+ * <p> This method may be called at any time.
+ *
+ * @param error
+ * the error to signal
+ *
+ * @throws NullPointerException
+ * if {@code error} is {@code null}
+ */
+ public void signalError(Throwable error) {
+ terminateNow(Optional.of(error));
+ }
+
+ /**
+ * Cancels the subscription by signalling {@code onComplete} to the
+ * subscriber.
+ *
+ * <p> Will not signal {@code onComplete} if the subscription has been
+ * cancelled already.
+ *
+ * <p> This method may be called at any time.
+ */
+ public void signalComplete() {
+ terminateNow(Optional.empty());
+ }
+
+ /**
+ * Must be called first and at most once.
+ */
+ private void signalSubscribe() {
+ assert subscribed;
+ try {
+ subscriber.onSubscribe(temporarySubscription);
+ } catch (Throwable t) {
+ cancelNow();
+ throw t;
+ }
+ while (true) {
+ int s = state.get();
+ if ((s & (PENDING | TERMINATE)) == (PENDING | TERMINATE)) {
+ if (state.compareAndSet(
+ s, CANCELLED | (s & ~(PENDING | TERMINATE)))) {
+ Optional<Throwable> t = terminationType.get();
+ dispatchTerminationAndUnsubscribe(t);
+ return;
+ }
+ } else if ((s & PENDING) == PENDING) {
+ if (state.compareAndSet(s, ACTIVE | (s & ~PENDING))) {
+ synchronized (lock) {
+ if (feedback != null) {
+ temporarySubscription
+ .replaceWith(new PermanentSubscription());
+ }
+ }
+ return;
+ }
+ } else { // It should not be in any other state
+ throw new InternalError(String.valueOf(s));
+ }
+ }
+ }
+
+ private void unsubscribe() {
+ subscriber = null;
+ }
+
+ private final static class NopSubscription implements Subscription {
+
+ @Override
+ public void request(long n) { }
+ @Override
+ public void cancel() { }
+ }
+
+ private final class PermanentSubscription implements Subscription {
+
+ @Override
+ public void request(long n) {
+ if (n <= 0) {
+ signalError(new IllegalArgumentException(
+ "non-positive subscription request"));
+ } else {
+ demand.increase(n);
+ feedback.request(n);
+ }
+ }
+
+ @Override
+ public void cancel() {
+ if (cancelNow()) {
+ unsubscribe();
+ // feedback.cancel() is called at most once
+ // (let's not assume idempotency)
+ feedback.cancel();
+ }
+ }
+ }
+
+ /**
+ * Cancels the subscription unless it has been cancelled already.
+ *
+ * @return {@code true} iff the subscription has been cancelled as a result
+ * of this call
+ */
+ private boolean cancelNow() {
+ while (true) {
+ int s = state.get();
+ if ((s & CANCELLED) == CANCELLED) {
+ return false;
+ } else if ((s & (ACTIVE | PENDING)) != 0) {
+ // ACTIVE or PENDING
+ if (state.compareAndSet(
+ s, CANCELLED | (s & ~(ACTIVE | PENDING)))) {
+ unsubscribe();
+ return true;
+ }
+ } else {
+ throw new InternalError(String.valueOf(s));
+ }
+ }
+ }
+
+ /**
+ * Terminates this subscription unless is has been cancelled already.
+ *
+ * @param t the type of termination
+ */
+ private void terminateNow(Optional<Throwable> t) {
+ // Termination condition must be set only after the termination
+ // type has been set (those will be read in the opposite order)
+ if (!terminationType.compareAndSet(null, t)) {
+ return;
+ }
+ while (true) {
+ int s = state.get();
+ if ((s & CANCELLED) == CANCELLED) {
+ return;
+ } else if ((s & (PENDING | DELIVERING)) != 0) {
+ // PENDING or DELIVERING (which implies ACTIVE)
+ if (state.compareAndSet(s, s | TERMINATE)) {
+ return;
+ }
+ } else if ((s & ACTIVE) == ACTIVE) {
+ if (state.compareAndSet(s, CANCELLED | (s & ~ACTIVE))) {
+ dispatchTerminationAndUnsubscribe(t);
+ return;
+ }
+ } else {
+ throw new InternalError(String.valueOf(s));
+ }
+ }
+ }
+
+ private void dispatchTerminationAndUnsubscribe(Optional<Throwable> t) {
+ try {
+ t.ifPresentOrElse(subscriber::onError, subscriber::onComplete);
+ } finally {
+ unsubscribe();
+ }
+ }
+}
--- /dev/null Thu Jan 01 00:00:00 1970 +0000
+++ b/src/jdk.incubator.httpclient/share/classes/jdk/incubator/http/internal/common/TemporarySubscription.java Sun Nov 05 17:32:13 2017 +0000
@@ -0,0 +1,112 @@
+/*
+ * Copyright (c) 2017, Oracle and/or its affiliates. All rights reserved.
+ * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER.
+ *
+ * This code is free software; you can redistribute it and/or modify it
+ * under the terms of the GNU General Public License version 2 only, as
+ * published by the Free Software Foundation. Oracle designates this
+ * particular file as subject to the "Classpath" exception as provided
+ * by Oracle in the LICENSE file that accompanied this code.
+ *
+ * This code is distributed in the hope that it will be useful, but WITHOUT
+ * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or
+ * FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License
+ * version 2 for more details (a copy is included in the LICENSE file that
+ * accompanied this code).
+ *
+ * You should have received a copy of the GNU General Public License version
+ * 2 along with this work; if not, write to the Free Software Foundation,
+ * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA.
+ *
+ * Please contact Oracle, 500 Oracle Parkway, Redwood Shores, CA 94065 USA
+ * or visit www.oracle.com if you need additional information or have any
+ * questions.
+ */
+
+package jdk.incubator.http.internal.common;
+
+import jdk.incubator.http.internal.common.SequentialScheduler.CompleteRestartableTask;
+
+import java.util.Objects;
+import java.util.concurrent.Flow.Subscription;
+import java.util.concurrent.atomic.AtomicReference;
+
+/**
+ * Acts as a subscription receiving calls to {@code request} and {@code cancel}
+ * methods until the replacing subscription is set.
+ *
+ * <p> After the replacing subscription is set, it gets updated with the result
+ * of calls happened before that and starts receiving calls to its
+ * {@code request} and {@code cancel} methods.
+ *
+ * <p> This subscription ensures that {@code request} and {@code cancel} methods
+ * of the replacing subscription are called sequentially.
+ */
+public final class TemporarySubscription implements Subscription {
+
+ private final AtomicReference<Subscription> subscription = new AtomicReference<>();
+ private final Demand demand = new Demand();
+ private volatile boolean cancelled;
+ private volatile long illegalValue = 1;
+
+ private final SequentialScheduler scheduler = new SequentialScheduler(new UpdateTask());
+
+ @Override
+ public void request(long n) {
+ if (n <= 0) {
+ // Any non-positive request would do, no need to remember them
+ // all or any one in particular.
+ // tl;dr racy, but don't care
+ illegalValue = n;
+ } else {
+ demand.increase(n);
+ }
+ scheduler.runOrSchedule();
+ }
+
+ @Override
+ public void cancel() {
+ cancelled = true;
+ scheduler.runOrSchedule();
+ }
+
+ public void replaceWith(Subscription permanentSubscription) {
+ Objects.requireNonNull(permanentSubscription);
+ if (permanentSubscription == this) {
+ // Otherwise it would be an unpleasant bug to chase
+ throw new IllegalStateException("Self replacement");
+ }
+ if (!subscription.compareAndSet(null, permanentSubscription)) {
+ throw new IllegalStateException("Already replaced");
+ }
+ scheduler.runOrSchedule();
+ }
+
+ private final class UpdateTask extends CompleteRestartableTask {
+
+ @Override
+ public void run() {
+ Subscription dst = TemporarySubscription.this.subscription.get();
+ if (dst == null) {
+ return;
+ }
+ /* As long as the result is effectively the same, it does not matter
+ how requests are accumulated and what goes first: request or
+ cancel. See rules 3.5, 3.6, 3.7 and 3.9 from the reactive-streams
+ specification. */
+ long illegalValue = TemporarySubscription.this.illegalValue;
+ if (illegalValue <= 0) {
+ dst.request(illegalValue);
+ scheduler.stop();
+ } else if (cancelled) {
+ dst.cancel();
+ scheduler.stop();
+ } else {
+ long accumulatedValue = demand.decreaseAndGet(Long.MAX_VALUE);
+ if (accumulatedValue > 0) {
+ dst.request(accumulatedValue);
+ }
+ }
+ }
+ }
+}
--- a/src/jdk.incubator.httpclient/share/classes/jdk/incubator/http/internal/common/Utils.java Sun Nov 05 17:05:57 2017 +0000
+++ b/src/jdk.incubator.httpclient/share/classes/jdk/incubator/http/internal/common/Utils.java Sun Nov 05 17:32:13 2017 +0000
@@ -36,6 +36,8 @@
import java.io.UncheckedIOException;
import java.io.PrintStream;
import java.io.UnsupportedEncodingException;
+import java.lang.System.Logger;
+import java.lang.System.Logger.Level;
import java.net.InetSocketAddress;
import java.net.NetPermission;
import java.net.URI;
@@ -47,17 +49,22 @@
import java.security.PrivilegedAction;
import java.util.Arrays;
import java.util.Collection;
+import java.util.Collections;
import java.util.List;
+import java.util.LinkedList;
import java.util.Map;
import java.util.Optional;
+import java.util.ResourceBundle;
import java.util.Set;
import java.util.concurrent.BlockingQueue;
import java.util.concurrent.CompletableFuture;
+import java.util.concurrent.CompletionException;
import java.util.concurrent.Executor;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
import java.util.concurrent.LinkedBlockingQueue;
import java.util.function.Predicate;
+import java.util.function.Supplier;
import jdk.incubator.http.HttpHeaders;
/**
@@ -65,6 +72,17 @@
*/
public final class Utils {
+ public static final boolean ASSERTIONSENABLED;
+ static {
+ boolean enabled = false;
+ assert enabled = true;
+ ASSERTIONSENABLED = enabled;
+ }
+ public static final boolean DEBUG = true;// Revisit: temporary dev flag.
+ //getBooleanProperty(DebugLogger.HTTP_NAME, false);
+ public static final boolean DEBUG_HPACK = // Revisit: temporary dev flag.
+ getBooleanProperty(DebugLogger.HPACK_NAME, false);
+
/**
* Allocated buffer size. Must never be higher than 16K. But can be lower
* if smaller allocation units preferred. HTTP/2 mandates that all
@@ -91,6 +109,19 @@
return ByteBuffer.allocate(BUFSIZE);
}
+ // Used when we know the max amount we want to put in the buffer
+ // In that case there's no reason to allocate a greater amount.
+ // Still not allow to allocate more than BUFSIZE.
+ public static ByteBuffer getBufferWithAtMost(int maxAmount) {
+ return ByteBuffer.allocate(Math.min(BUFSIZE, maxAmount));
+ }
+
+ public static Throwable getCompletionCause(Throwable x) {
+ if (!(x instanceof CompletionException)) return x;
+ final Throwable cause = x.getCause();
+ return cause == null ? x : cause;
+ }
+
public static IOException getIOException(Throwable t) {
if (t instanceof IOException) {
return (IOException) t;
@@ -103,16 +134,6 @@
}
/**
- * We use the same buffer for reading all headers and dummy bodies in an Exchange.
- */
- public static ByteBuffer getExchangeBuffer() {
- ByteBuffer buf = getBuffer();
- // Force a read the first time it is used
- buf.limit(0);
- return buf;
- }
-
- /**
* Puts position to limit and limit to capacity so we can resume reading
* into this buffer, but if required > 0 then limit may be reduced so that
* no more than required bytes are read next time.
@@ -130,11 +151,6 @@
private Utils() { }
- public static ExecutorService innocuousThreadPool() {
- return Executors.newCachedThreadPool(
- (r) -> InnocuousThread.newThread("DefaultHttpClient", r));
- }
-
// ABNF primitives defined in RFC 7230
private static final boolean[] tchar = new boolean[256];
private static final boolean[] fieldvchar = new boolean[256];
@@ -217,46 +233,6 @@
return accepted;
}
- /**
- * Returns the security permission required for the given details.
- * If method is CONNECT, then uri must be of form "scheme://host:port"
- */
- public static URLPermission getPermission(URI uri,
- String method,
- Map<String, List<String>> headers) {
- StringBuilder sb = new StringBuilder();
-
- String urlstring, actionstring;
-
- if (method.equals("CONNECT")) {
- urlstring = uri.toString();
- actionstring = "CONNECT";
- } else {
- sb.append(uri.getScheme())
- .append("://")
- .append(uri.getAuthority())
- .append(uri.getPath());
- urlstring = sb.toString();
-
- sb = new StringBuilder();
- sb.append(method);
- if (headers != null && !headers.isEmpty()) {
- sb.append(':');
- Set<String> keys = headers.keySet();
- boolean first = true;
- for (String key : keys) {
- if (!first) {
- sb.append(',');
- }
- sb.append(key);
- first = false;
- }
- }
- actionstring = sb.toString();
- }
- return new URLPermission(urlstring, actionstring);
- }
-
public static void checkNetPermission(String target) {
SecurityManager sm = System.getSecurityManager();
if (sm == null) {
@@ -266,6 +242,14 @@
sm.checkPermission(np);
}
+ public static void sleep(int millis) {
+ try {
+ Thread.sleep(millis);
+ } catch (Exception e) {
+ e.printStackTrace();
+ }
+ }
+
public static int getIntegerNetProperty(String name, int defaultValue) {
return AccessController.doPrivileged((PrivilegedAction<Integer>) () ->
NetProperties.getInteger(name, defaultValue));
@@ -276,6 +260,11 @@
NetProperties.get(name));
}
+ static boolean getBooleanProperty(String name, boolean def) {
+ return AccessController.doPrivileged((PrivilegedAction<Boolean>) () ->
+ Boolean.parseBoolean(System.getProperty(name, String.valueOf(def))));
+ }
+
public static SSLParameters copySSLParameters(SSLParameters p) {
SSLParameters p1 = new SSLParameters();
p1.setAlgorithmConstraints(p.getAlgorithmConstraints());
@@ -337,6 +326,49 @@
return srcLen - src.remaining();
}
+ /** Threshold beyond which data is no longer copied into the current
+ * buffer, if that buffer has enough unused space. */
+ private static final int COPY_THRESHOLD = 8192;
+
+ /**
+ * Adds the data from buffersToAdd to currentList. Either 1) appends the
+ * data from a particular buffer to the last buffer in the list ( if
+ * there is enough unused space ), or 2) adds it to the list.
+ *
+ * @returns the number of bytes added
+ */
+ public static long accumulateBuffers(List<ByteBuffer> currentList,
+ List<ByteBuffer> buffersToAdd) {
+ long accumulatedBytes = 0;
+ for (ByteBuffer bufferToAdd : buffersToAdd) {
+ int remaining = bufferToAdd.remaining();
+ if (remaining <= 0)
+ continue;
+ int listSize = currentList.size();
+ if (listSize == 0) {
+ currentList.add(bufferToAdd);
+ accumulatedBytes = remaining;
+ continue;
+ }
+
+ ByteBuffer lastBuffer = currentList.get(currentList.size() - 1);
+ int freeSpace = lastBuffer.capacity() - lastBuffer.limit();
+ if (remaining <= COPY_THRESHOLD && freeSpace >= remaining) {
+ // append the new data to the unused space in the last buffer
+ int position = lastBuffer.position();
+ int limit = lastBuffer.limit();
+ lastBuffer.position(limit);
+ lastBuffer.limit(limit + bufferToAdd.limit());
+ lastBuffer.put(bufferToAdd);
+ lastBuffer.position(position);
+ } else {
+ currentList.add(bufferToAdd);
+ }
+ accumulatedBytes += remaining;
+ }
+ return accumulatedBytes;
+ }
+
// copy up to amount from src to dst, but no more
public static int copyUpTo(ByteBuffer src, ByteBuffer dst, int amount) {
int toCopy = Math.min(src.remaining(), Math.min(dst.remaining(), amount));
@@ -378,30 +410,81 @@
return Arrays.toString(source.toArray());
}
- public static int remaining(ByteBuffer[] bufs) {
- int remain = 0;
+ public static int remaining(ByteBuffer buf) {
+ return buf.remaining();
+ }
+
+ public static long remaining(ByteBuffer[] bufs) {
+ long remain = 0;
for (ByteBuffer buf : bufs) {
remain += buf.remaining();
}
return remain;
}
- public static int remaining(List<ByteBuffer> bufs) {
- int remain = 0;
- for (ByteBuffer buf : bufs) {
- remain += buf.remaining();
+ public static boolean hasRemaining(List<ByteBuffer> bufs) {
+ synchronized (bufs) {
+ for (ByteBuffer buf : bufs) {
+ if (buf.hasRemaining())
+ return true;
+ }
+ }
+ return false;
+ }
+
+ public static long remaining(List<ByteBuffer> bufs) {
+ long remain = 0;
+ synchronized (bufs) {
+ for (ByteBuffer buf : bufs) {
+ remain += buf.remaining();
+ }
}
return remain;
}
- public static int remaining(ByteBufferReference[] refs) {
- int remain = 0;
+ public static int remaining(List<ByteBuffer> bufs, int max) {
+ long remain = 0;
+ synchronized (bufs) {
+ for (ByteBuffer buf : bufs) {
+ remain += buf.remaining();
+ if (remain > max) {
+ throw new IllegalArgumentException("too many bytes");
+ }
+ }
+ }
+ return (int) remain;
+ }
+
+ public static long remaining(ByteBufferReference[] refs) {
+ long remain = 0;
for (ByteBufferReference ref : refs) {
remain += ref.get().remaining();
}
return remain;
}
+ public static int remaining(ByteBufferReference[] refs, int max) {
+ long remain = 0;
+ for (ByteBufferReference ref : refs) {
+ remain += ref.get().remaining();
+ if (remain > max) {
+ throw new IllegalArgumentException("too many bytes");
+ }
+ }
+ return (int) remain;
+ }
+
+ public static int remaining(ByteBuffer[] refs, int max) {
+ long remain = 0;
+ for (ByteBuffer b : refs) {
+ remain += b.remaining();
+ if (remain > max) {
+ throw new IllegalArgumentException("too many bytes");
+ }
+ }
+ return (int) remain;
+ }
+
// assumes buffer was written into starting at position zero
static void unflip(ByteBuffer buf) {
buf.position(buf.limit());
@@ -446,50 +529,17 @@
return new String(b, StandardCharsets.US_ASCII);
}
- /**
- * Returns a single threaded executor which uses one invocation
- * of the parent executor to execute tasks (in sequence).
- *
- * Use a null valued Runnable to terminate.
- */
- // TODO: this is a blocking way of doing this;
- public static Executor singleThreadExecutor(Executor parent) {
- BlockingQueue<Optional<Runnable>> queue = new LinkedBlockingQueue<>();
- parent.execute(() -> {
- while (true) {
- try {
- Optional<Runnable> o = queue.take();
- if (!o.isPresent()) {
- return;
- }
- o.get().run();
- } catch (InterruptedException ex) {
- return;
- }
- }
- });
- return new Executor() {
- @Override
- public void execute(Runnable command) {
- queue.offer(Optional.ofNullable(command));
- }
- };
- }
-
- private static void executeInline(Runnable r) {
- r.run();
- }
-
- static Executor callingThreadExecutor() {
- return Utils::executeInline;
- }
-
// Put all these static 'empty' singletons here
@SuppressWarnings("rawtypes")
public static final CompletableFuture[] EMPTY_CFARRAY = new CompletableFuture[0];
public static final ByteBuffer EMPTY_BYTEBUFFER = ByteBuffer.allocate(0);
public static final ByteBuffer[] EMPTY_BB_ARRAY = new ByteBuffer[0];
+ public static final List<ByteBuffer> EMPTY_BB_LIST;
+
+ static {
+ EMPTY_BB_LIST = Collections.unmodifiableList(new LinkedList<>());
+ }
public static ByteBuffer slice(ByteBuffer buffer, int amount) {
ByteBuffer newb = buffer.slice();
@@ -515,4 +565,348 @@
public static UncheckedIOException unchecked(IOException e) {
return new UncheckedIOException(e);
}
+
+ /**
+ * Get a logger for debug HTTP traces.
+ *
+ * The logger should only be used with levels whose severity is
+ * {@code <= DEBUG}. By default, this logger will forward all messages
+ * logged to an internal logger named "jdk.internal.httpclient.debug".
+ * In addition, if the property -Djdk.internal.httpclient.debug=true is set,
+ * it will print the messages on stderr.
+ * The logger will add some decoration to the printed message, in the form of
+ * {@code <Level>:[<thread-name>] [<elapsed-time>] <dbgTag>: <formatted message>}
+ *
+ * @param dbgTag A lambda that returns a string that identifies the caller
+ * (e.g: "SocketTube(3)", or "Http2Connection(SocketTube(3))")
+ *
+ * @return A logger for HTTP internal debug traces
+ */
+ public static Logger getDebugLogger(Supplier<String> dbgTag) {
+ return getDebugLogger(dbgTag, DEBUG);
+ }
+
+ /**
+ * Get a logger for debug HTTP traces.The logger should only be used
+ * with levels whose severity is {@code <= DEBUG}.
+ *
+ * By default, this logger will forward all messages logged to an internal
+ * logger named "jdk.internal.httpclient.debug".
+ * In addition, if the message severity level is >= to
+ * the provided {@code errLevel} it will print the messages on stderr.
+ * The logger will add some decoration to the printed message, in the form of
+ * {@code <Level>:[<thread-name>] [<elapsed-time>] <dbgTag>: <formatted message>}
+ *
+ * @apiNote To obtain a logger that will always print things on stderr in
+ * addition to forwarding to the internal logger, use
+ * {@code getDebugLogger(this::dbgTag, Level.ALL);}.
+ * This is also equivalent to calling
+ * {@code getDebugLogger(this::dbgTag, true);}.
+ * To obtain a logger that will only forward to the internal logger,
+ * use {@code getDebugLogger(this::dbgTag, Level.OFF);}.
+ * This is also equivalent to calling
+ * {@code getDebugLogger(this::dbgTag, false);}.
+ *
+ * @param dbgTag A lambda that returns a string that identifies the caller
+ * (e.g: "SocketTube(3)", or "Http2Connection(SocketTube(3))")
+ * @param errLevel The level above which messages will be also printed on
+ * stderr (in addition to be forwarded to the internal logger).
+ *
+ * @return A logger for HTTP internal debug traces
+ */
+ static Logger getDebugLogger(Supplier<String> dbgTag, Level errLevel) {
+ return new DebugLogger(DebugLogger.HTTP, dbgTag, Level.OFF, errLevel);
+ }
+
+ /**
+ * Get a logger for debug HTTP traces.The logger should only be used
+ * with levels whose severity is {@code <= DEBUG}.
+ *
+ * By default, this logger will forward all messages logged to an internal
+ * logger named "jdk.internal.httpclient.debug".
+ * In addition, the provided boolean {@code on==true}, it will print the
+ * messages on stderr.
+ * The logger will add some decoration to the printed message, in the form of
+ * {@code <Level>:[<thread-name>] [<elapsed-time>] <dbgTag>: <formatted message>}
+ *
+ * @apiNote To obtain a logger that will always print things on stderr in
+ * addition to forwarding to the internal logger, use
+ * {@code getDebugLogger(this::dbgTag, true);}.
+ * This is also equivalent to calling
+ * {@code getDebugLogger(this::dbgTag, Level.ALL);}.
+ * To obtain a logger that will only forward to the internal logger,
+ * use {@code getDebugLogger(this::dbgTag, false);}.
+ * This is also equivalent to calling
+ * {@code getDebugLogger(this::dbgTag, Level.OFF);}.
+ *
+ * @param dbgTag A lambda that returns a string that identifies the caller
+ * (e.g: "SocketTube(3)", or "Http2Connection(SocketTube(3))")
+ * @param on Whether messages should also be printed on
+ * stderr (in addition to be forwarded to the internal logger).
+ *
+ * @return A logger for HTTP internal debug traces
+ */
+ public static Logger getDebugLogger(Supplier<String> dbgTag, boolean on) {
+ Level errLevel = on ? Level.ALL : Level.OFF;
+ return getDebugLogger(dbgTag, errLevel);
+ }
+
+ /**
+ * Get a logger for debug HPACK traces.
+ *
+ * The logger should only be used with levels whose severity is
+ * {@code <= DEBUG}. By default, this logger will forward all messages
+ * logged to an internal logger named "jdk.internal.httpclient.hpack.debug".
+ * In addition, if the property -Djdk.internal.httpclient.hpack.debug=true
+ * is set, it will print the messages on stdout.
+ * The logger will add some decoration to the printed message, in the form of
+ * {@code <Level>:[<thread-name>] [<elapsed-time>] <dbgTag>: <formatted message>}
+ *
+ * @param dbgTag A lambda that returns a string that identifies the caller
+ * (e.g: "Http2Connection(SocketTube(3))/hpack.Decoder(3)")
+ *
+ * @return A logger for HPACK internal debug traces
+ */
+ public static Logger getHpackLogger(Supplier<String> dbgTag) {
+ Level errLevel = Level.OFF;
+ Level outLevel = DEBUG_HPACK ? Level.ALL : Level.OFF;
+ return new DebugLogger(DebugLogger.HPACK, dbgTag, outLevel, errLevel);
+ }
+
+ /**
+ * Get a logger for debug HPACK traces.The logger should only be used
+ * with levels whose severity is {@code <= DEBUG}.
+ *
+ * By default, this logger will forward all messages logged to an internal
+ * logger named "jdk.internal.httpclient.hpack.debug".
+ * In addition, if the message severity level is >= to
+ * the provided {@code outLevel} it will print the messages on stdout.
+ * The logger will add some decoration to the printed message, in the form of
+ * {@code <Level>:[<thread-name>] [<elapsed-time>] <dbgTag>: <formatted message>}
+ *
+ * @apiNote To obtain a logger that will always print things on stdout in
+ * addition to forwarding to the internal logger, use
+ * {@code getHpackLogger(this::dbgTag, Level.ALL);}.
+ * This is also equivalent to calling
+ * {@code getHpackLogger(this::dbgTag, true);}.
+ * To obtain a logger that will only forward to the internal logger,
+ * use {@code getHpackLogger(this::dbgTag, Level.OFF);}.
+ * This is also equivalent to calling
+ * {@code getHpackLogger(this::dbgTag, false);}.
+ *
+ * @param dbgTag A lambda that returns a string that identifies the caller
+ * (e.g: "Http2Connection(SocketTube(3))/hpack.Decoder(3)")
+ * @param outLevel The level above which messages will be also printed on
+ * stdout (in addition to be forwarded to the internal logger).
+ *
+ * @return A logger for HPACK internal debug traces
+ */
+ static Logger getHpackLogger(Supplier<String> dbgTag, Level outLevel) {
+ Level errLevel = Level.OFF;
+ return new DebugLogger(DebugLogger.HPACK, dbgTag, outLevel, errLevel);
+ }
+
+ /**
+ * Get a logger for debug HPACK traces.The logger should only be used
+ * with levels whose severity is {@code <= DEBUG}.
+ *
+ * By default, this logger will forward all messages logged to an internal
+ * logger named "jdk.internal.httpclient.hpack.debug".
+ * In addition, the provided boolean {@code on==true}, it will print the
+ * messages on stdout.
+ * The logger will add some decoration to the printed message, in the form of
+ * {@code <Level>:[<thread-name>] [<elapsed-time>] <dbgTag>: <formatted message>}
+ *
+ * @apiNote To obtain a logger that will always print things on stdout in
+ * addition to forwarding to the internal logger, use
+ * {@code getHpackLogger(this::dbgTag, true);}.
+ * This is also equivalent to calling
+ * {@code getHpackLogger(this::dbgTag, Level.ALL);}.
+ * To obtain a logger that will only forward to the internal logger,
+ * use {@code getHpackLogger(this::dbgTag, false);}.
+ * This is also equivalent to calling
+ * {@code getHpackLogger(this::dbgTag, Level.OFF);}.
+ *
+ * @param dbgTag A lambda that returns a string that identifies the caller
+ * (e.g: "Http2Connection(SocketTube(3))/hpack.Decoder(3)")
+ * @param on Whether messages should also be printed on
+ * stdout (in addition to be forwarded to the internal logger).
+ *
+ * @return A logger for HPACK internal debug traces
+ */
+ public static Logger getHpackLogger(Supplier<String> dbgTag, boolean on) {
+ Level outLevel = on ? Level.ALL : Level.OFF;
+ return getHpackLogger(dbgTag, outLevel);
+ }
+
+
+
+ private static final class DebugLogger implements System.Logger {
+
+ // deliberately not in the same subtree than standard loggers.
+ final static String HTTP_NAME = "jdk.internal.httpclient.debug";
+ final static String HPACK_NAME = "jdk.internal.httpclient.hpack.debug";
+ final static Logger HTTP = System.getLogger(HTTP_NAME);
+ final static Logger HPACK = System.getLogger(HPACK_NAME);
+ final static long START_NANOS = System.nanoTime();
+
+ final Supplier<String> dbgTag;
+ final Level errLevel;
+ final Level outLevel;
+ final Logger logger;
+ final boolean debugOn;
+ final boolean traceOn;
+
+ DebugLogger(Logger logger,
+ Supplier<String> dbgTag,
+ Level outLevel,
+ Level errLevel) {
+ this.dbgTag = dbgTag;
+ this.errLevel = errLevel;
+ this.outLevel = outLevel;
+ this.logger = logger;
+ // support only static configuration.
+ this.debugOn = isEnabled(Level.DEBUG);
+ this.traceOn = isEnabled(Level.TRACE);
+ }
+
+ @Override
+ public String getName() {
+ return logger.getName();
+ }
+
+ private boolean isEnabled(Level level) {
+ if (level == Level.OFF) return false;
+ int severity = level.getSeverity();
+ return severity >= errLevel.getSeverity()
+ || severity >= outLevel.getSeverity()
+ || logger.isLoggable(level);
+ }
+
+ @Override
+ public boolean isLoggable(Level level) {
+ // fast path, we assume these guys never change.
+ // support only static configuration.
+ if (level == Level.DEBUG) return debugOn;
+ if (level == Level.TRACE) return traceOn;
+ return isEnabled(level);
+ }
+
+ @Override
+ public void log(Level level, ResourceBundle unused,
+ String format, Object... params) {
+ // fast path, we assume these guys never change.
+ // support only static configuration.
+ if (level == Level.DEBUG && !debugOn) return;
+ if (level == Level.TRACE && !traceOn) return;
+
+ int severity = level.getSeverity();
+ if (errLevel != Level.OFF
+ && errLevel.getSeverity() <= severity) {
+ print(System.err, level, format, params, null);
+ }
+ if (outLevel != Level.OFF
+ && outLevel.getSeverity() <= severity) {
+ print(System.out, level, format, params, null);
+ }
+ if (logger.isLoggable(level)) {
+ logger.log(level, unused,
+ getFormat(new StringBuilder(), format, params).toString(),
+ params);
+ }
+ }
+
+ @Override
+ public void log(Level level, ResourceBundle unused, String msg,
+ Throwable thrown) {
+ // fast path, we assume these guys never change.
+ if (level == Level.DEBUG && !debugOn) return;
+ if (level == Level.TRACE && !traceOn) return;
+
+ if (errLevel != Level.OFF
+ && errLevel.getSeverity() <= level.getSeverity()) {
+ print(System.err, level, msg, null, thrown);
+ }
+ if (outLevel != Level.OFF
+ && outLevel.getSeverity() <= level.getSeverity()) {
+ print(System.out, level, msg, null, thrown);
+ }
+ if (logger.isLoggable(level)) {
+ logger.log(level, unused,
+ getFormat(new StringBuilder(), msg, null).toString(),
+ thrown);
+ }
+ }
+
+ private void print(PrintStream out, Level level, String msg,
+ Object[] params, Throwable t) {
+ StringBuilder sb = new StringBuilder();
+ sb.append(level.name()).append(':').append(' ');
+ sb = format(sb, msg, params);
+ if (t != null) sb.append(' ').append(t.toString());
+ out.println(sb.toString());
+ if (t != null) {
+ t.printStackTrace(out);
+ }
+ }
+
+ private StringBuilder decorate(StringBuilder sb, String msg) {
+ String tag = dbgTag == null ? null : dbgTag.get();
+ String res = msg == null ? "" : msg;
+ long elapsed = System.nanoTime() - START_NANOS;
+ long nanos = elapsed % 1000_000;
+ long millis = elapsed / 1000_000;
+ long secs = millis / 1000;
+ sb.append('[').append(Thread.currentThread().getName()).append(']')
+ .append(' ').append('[');
+ if (secs > 0) {
+ sb.append(secs).append('s');
+ }
+ millis = millis % 1000;
+ if (millis > 0) {
+ if (secs > 0) sb.append(' ');
+ sb.append(millis).append("ms");
+ }
+ sb.append(']').append(' ');
+ if (tag != null) {
+ sb.append(tag).append(' ');
+ }
+ sb.append(res);
+ return sb;
+ }
+
+
+ private StringBuilder getFormat(StringBuilder sb, String format, Object[] params) {
+ if (format == null || params == null || params.length == 0) {
+ return decorate(sb, format);
+ } else if (format.contains("{0}") || format.contains("{1}")) {
+ return decorate(sb, format);
+ } else if (format.contains("%s") || format.contains("%d")) {
+ try {
+ return decorate(sb, String.format(format, params));
+ } catch (Throwable t) {
+ return decorate(sb, format);
+ }
+ } else {
+ return decorate(sb, format);
+ }
+ }
+
+ private StringBuilder format(StringBuilder sb, String format, Object[] params) {
+ if (format == null || params == null || params.length == 0) {
+ return decorate(sb, format);
+ } else if (format.contains("{0}") || format.contains("{1}")) {
+ return decorate(sb, java.text.MessageFormat.format(format, params));
+ } else if (format.contains("%s") || format.contains("%d")) {
+ try {
+ return decorate(sb, String.format(format, params));
+ } catch (Throwable t) {
+ return decorate(sb, format);
+ }
+ } else {
+ return decorate(sb, format);
+ }
+ }
+
+ }
}
--- a/src/jdk.incubator.httpclient/share/classes/jdk/incubator/http/internal/frame/DataFrame.java Sun Nov 05 17:05:57 2017 +0000
+++ b/src/jdk.incubator.httpclient/share/classes/jdk/incubator/http/internal/frame/DataFrame.java Sun Nov 05 17:32:13 2017 +0000
@@ -47,7 +47,7 @@
public DataFrame(int streamid, int flags, ByteBufferReference[] data) {
super(streamid, flags);
this.data = data;
- this.dataLength = Utils.remaining(data);
+ this.dataLength = Utils.remaining(data, Integer.MAX_VALUE);
}
public DataFrame(int streamid, int flags, ByteBufferReference[] data, int padLength) {
--- a/src/jdk.incubator.httpclient/share/classes/jdk/incubator/http/internal/frame/FramesDecoder.java Sun Nov 05 17:05:57 2017 +0000
+++ b/src/jdk.incubator.httpclient/share/classes/jdk/incubator/http/internal/frame/FramesDecoder.java Sun Nov 05 17:32:13 2017 +0000
@@ -30,6 +30,7 @@
import jdk.incubator.http.internal.common.Utils;
import java.io.IOException;
+import java.lang.System.Logger.Level;
import java.nio.ByteBuffer;
import java.util.ArrayDeque;
import java.util.ArrayList;
@@ -46,7 +47,9 @@
*/
public class FramesDecoder {
-
+ static final boolean DEBUG = Utils.DEBUG; // Revisit: temporary dev flag.
+ static final System.Logger DEBUG_LOGGER =
+ Utils.getDebugLogger("FramesDecoder"::toString, DEBUG);
@FunctionalInterface
public interface FrameProcessor {
@@ -92,25 +95,54 @@
this.maxFrameSize = Math.min(Math.max(16 * 1024, maxFrameSize), 16 * 1024 * 1024 - 1);
}
+ /** Threshold beyond which data is no longer copied into the current buffer,
+ * if that buffer has enough unused space. */
+ private static final int COPY_THRESHOLD = 8192;
+
/**
- * put next buffer into queue,
- * if frame decoding is possible - decode all buffers and invoke FrameProcessor
+ * Adds the data from the given buffer, and performs frame decoding if
+ * possible. Either 1) appends the data from the given buffer to the
+ * current buffer ( if there is enough unused space ), or 2) adds it to the
+ * next buffer in the queue.
*
- * @param buffer
- * @throws IOException
+ * If there is enough data to perform frame decoding then, all buffers are
+ * decoded and the FrameProcessor is invoked.
*/
public void decode(ByteBufferReference buffer) throws IOException {
int remaining = buffer.get().remaining();
+ DEBUG_LOGGER.log(Level.DEBUG, "decodes: %d", remaining);
if (remaining > 0) {
if (currentBuffer == null) {
currentBuffer = buffer;
} else {
- tailBuffers.add(buffer);
- tailSize += remaining;
+ ByteBuffer cb = currentBuffer.get();
+ int freeSpace = cb.capacity() - cb.limit();
+ if (remaining <= COPY_THRESHOLD && freeSpace >= remaining) {
+ // append the new data to the unused space in the current buffer
+ ByteBuffer b = buffer.get();
+ int position = cb.position();
+ int limit = cb.limit();
+ cb.position(limit);
+ cb.limit(limit + b.limit());
+ cb.put(buffer.get());
+ cb.position(position);
+ buffer.clear(); // release the buffer, if it is a member of a pool
+ DEBUG_LOGGER.log(Level.DEBUG, "copied: %d", remaining);
+ } else {
+ DEBUG_LOGGER.log(Level.DEBUG, "added: %d", remaining);
+ tailBuffers.add(buffer);
+ tailSize += remaining;
+ }
}
}
+ DEBUG_LOGGER.log(Level.DEBUG, "Tail size is now: %d, current=",
+ tailSize,
+ (currentBuffer == null ? 0 :
+ (currentBuffer.get() == null ? 0 :
+ currentBuffer.get().remaining())));
Http2Frame frame;
while ((frame = nextFrame()) != null) {
+ DEBUG_LOGGER.log(Level.DEBUG, "Got frame: %s", frame);
frameProcessor.processFrame(frame);
frameProcessed();
}
@@ -121,21 +153,28 @@
if (currentBuffer == null) {
return null; // no data at all
}
+ long available = currentBuffer.get().remaining() + tailSize;
if (!frameHeaderParsed) {
- if (currentBuffer.get().remaining() + tailSize >= Http2Frame.FRAME_HEADER_SIZE) {
+ if (available >= Http2Frame.FRAME_HEADER_SIZE) {
parseFrameHeader();
if (frameLength > maxFrameSize) {
// connection error
return new MalformedFrame(ErrorFrame.FRAME_SIZE_ERROR,
- "Frame type("+frameType+") " +"length("+frameLength+") exceeds MAX_FRAME_SIZE("+ maxFrameSize+")");
+ "Frame type("+frameType+") "
+ +"length("+frameLength
+ +") exceeds MAX_FRAME_SIZE("
+ + maxFrameSize+")");
}
frameHeaderParsed = true;
} else {
- return null; // no data for frame header
+ DEBUG_LOGGER.log(Level.DEBUG,
+ "Not enough data to parse header, needs: %d, has: %d",
+ Http2Frame.FRAME_HEADER_SIZE, available);
}
}
+ available = currentBuffer == null ? 0 : currentBuffer.get().remaining() + tailSize;
if ((frameLength == 0) ||
- (currentBuffer != null && currentBuffer.get().remaining() + tailSize >= frameLength)) {
+ (currentBuffer != null && available >= frameLength)) {
Http2Frame frame = parseFrameBody();
frameHeaderParsed = false;
// frame == null means we have to skip this frame and try parse next
@@ -143,6 +182,9 @@
return frame;
}
} else {
+ DEBUG_LOGGER.log(Level.DEBUG,
+ "Not enough data to parse frame body, needs: %d, has: %d",
+ frameLength, available);
return null; // no data for the whole frame header
}
}
@@ -296,12 +338,13 @@
private Http2Frame parseDataFrame(int frameLength, int streamid, int flags) {
// non-zero stream
if (streamid == 0) {
- return new MalformedFrame(ErrorFrame.PROTOCOL_ERROR, "zero streamId for DataFrame");
+ return new MalformedFrame(ErrorFrame.PROTOCOL_ERROR,
+ "zero streamId for DataFrame");
}
int padLength = 0;
if ((flags & DataFrame.PADDED) != 0) {
padLength = getByte();
- if(padLength >= frameLength) {
+ if (padLength >= frameLength) {
return new MalformedFrame(ErrorFrame.PROTOCOL_ERROR,
"the length of the padding is the length of the frame payload or greater");
}
@@ -317,7 +360,8 @@
private Http2Frame parseHeadersFrame(int frameLength, int streamid, int flags) {
// non-zero stream
if (streamid == 0) {
- return new MalformedFrame(ErrorFrame.PROTOCOL_ERROR, "zero streamId for HeadersFrame");
+ return new MalformedFrame(ErrorFrame.PROTOCOL_ERROR,
+ "zero streamId for HeadersFrame");
}
int padLength = 0;
if ((flags & HeadersFrame.PADDED) != 0) {
--- a/src/jdk.incubator.httpclient/share/classes/jdk/incubator/http/internal/frame/HeaderFrame.java Sun Nov 05 17:05:57 2017 +0000
+++ b/src/jdk.incubator.httpclient/share/classes/jdk/incubator/http/internal/frame/HeaderFrame.java Sun Nov 05 17:32:13 2017 +0000
@@ -48,7 +48,7 @@
public HeaderFrame(int streamid, int flags, ByteBufferReference[] headerBlocks) {
super(streamid, flags);
this.headerBlocks = headerBlocks;
- this.headerLength = Utils.remaining(headerBlocks);
+ this.headerLength = Utils.remaining(headerBlocks, Integer.MAX_VALUE);
}
@Override
--- a/src/jdk.incubator.httpclient/share/classes/jdk/incubator/http/internal/hpack/Decoder.java Sun Nov 05 17:05:57 2017 +0000
+++ b/src/jdk.incubator.httpclient/share/classes/jdk/incubator/http/internal/hpack/Decoder.java Sun Nov 05 17:32:13 2017 +0000
@@ -24,20 +24,22 @@
*/
package jdk.incubator.http.internal.hpack;
+import jdk.incubator.http.internal.hpack.HPACK.Logger;
import jdk.internal.vm.annotation.Stable;
import java.io.IOException;
-import java.io.UncheckedIOException;
-import java.net.ProtocolException;
import java.nio.ByteBuffer;
+import java.util.concurrent.atomic.AtomicLong;
+import static jdk.incubator.http.internal.hpack.HPACK.Logger.Level.EXTRA;
+import static jdk.incubator.http.internal.hpack.HPACK.Logger.Level.NORMAL;
import static java.lang.String.format;
import static java.util.Objects.requireNonNull;
/**
* Decodes headers from their binary representation.
*
- * <p>Typical lifecycle looks like this:
+ * <p> Typical lifecycle looks like this:
*
* <p> {@link #Decoder(int) new Decoder}
* ({@link #setMaxCapacity(int) setMaxCapacity}?
@@ -62,6 +64,9 @@
*/
public final class Decoder {
+ private final Logger logger;
+ private static final AtomicLong DECODERS_IDS = new AtomicLong();
+
@Stable
private static final State[] states = new State[256];
@@ -92,6 +97,7 @@
}
}
+ private final long id;
private final HeaderTable table;
private State state = State.READY;
@@ -111,9 +117,8 @@
* header table.
*
* <p> The value has to be agreed between decoder and encoder out-of-band,
- * e.g. by a protocol that uses HPACK (see <a
- * href="https://tools.ietf.org/html/rfc7541#section-4.2">4.2. Maximum Table
- * Size</a>).
+ * e.g. by a protocol that uses HPACK
+ * (see <a href="https://tools.ietf.org/html/rfc7541#section-4.2">4.2. Maximum Table Size</a>).
*
* @param capacity
* a non-negative integer
@@ -122,8 +127,24 @@
* if capacity is negative
*/
public Decoder(int capacity) {
- setMaxCapacity(capacity);
- table = new HeaderTable(capacity);
+ id = DECODERS_IDS.incrementAndGet();
+ logger = HPACK.getLogger().subLogger("Decoder#" + id);
+ if (logger.isLoggable(NORMAL)) {
+ logger.log(NORMAL, () -> format("new decoder with maximum table size %s",
+ capacity));
+ }
+ if (logger.isLoggable(NORMAL)) {
+ /* To correlate with logging outside HPACK, knowing
+ hashCode/toString is important */
+ logger.log(NORMAL, () -> {
+ String hashCode = Integer.toHexString(
+ System.identityHashCode(this));
+ return format("toString='%s', identityHashCode=%s",
+ toString(), hashCode);
+ });
+ }
+ setMaxCapacity0(capacity);
+ table = new HeaderTable(capacity, logger.subLogger("HeaderTable"));
integerReader = new IntegerReader();
stringReader = new StringReader();
name = new StringBuilder(512);
@@ -134,9 +155,8 @@
* Sets a maximum capacity of the header table.
*
* <p> The value has to be agreed between decoder and encoder out-of-band,
- * e.g. by a protocol that uses HPACK (see <a
- * href="https://tools.ietf.org/html/rfc7541#section-4.2">4.2. Maximum Table
- * Size</a>).
+ * e.g. by a protocol that uses HPACK
+ * (see <a href="https://tools.ietf.org/html/rfc7541#section-4.2">4.2. Maximum Table Size</a>).
*
* @param capacity
* a non-negative integer
@@ -145,6 +165,14 @@
* if capacity is negative
*/
public void setMaxCapacity(int capacity) {
+ if (logger.isLoggable(NORMAL)) {
+ logger.log(NORMAL, () -> format("setting maximum table size to %s",
+ capacity));
+ }
+ setMaxCapacity0(capacity);
+ }
+
+ private void setMaxCapacity0(int capacity) {
if (capacity < 0) {
throw new IllegalArgumentException("capacity >= 0: " + capacity);
}
@@ -155,8 +183,8 @@
/**
* Decodes a header block from the given buffer to the given callback.
*
- * <p> Suppose a header block is represented by a sequence of {@code
- * ByteBuffer}s in the form of {@code Iterator<ByteBuffer>}. And the
+ * <p> Suppose a header block is represented by a sequence of
+ * {@code ByteBuffer}s in the form of {@code Iterator<ByteBuffer>}. And the
* consumer of decoded headers is represented by the callback. Then to
* decode the header block, the following approach might be used:
*
@@ -174,7 +202,7 @@
*
* <p> Once the method is invoked with {@code endOfHeaderBlock == true}, the
* current header block is deemed ended, and inconsistencies, if any, are
- * reported immediately by throwing an {@code UncheckedIOException}.
+ * reported immediately by throwing an {@code IOException}.
*
* <p> Each callback method is called only after the implementation has
* processed the corresponding bytes. If the bytes revealed a decoding
@@ -200,25 +228,32 @@
*
* @param consumer
* the callback
- * @throws UncheckedIOException
+ * @throws IOException
* in case of a decoding error
* @throws NullPointerException
* if either headerBlock or consumer are null
*/
- public void decode(ByteBuffer headerBlock, boolean endOfHeaderBlock,
- DecodingCallback consumer) {
+ public void decode(ByteBuffer headerBlock,
+ boolean endOfHeaderBlock,
+ DecodingCallback consumer) throws IOException {
requireNonNull(headerBlock, "headerBlock");
requireNonNull(consumer, "consumer");
+ if (logger.isLoggable(NORMAL)) {
+ logger.log(NORMAL, () -> format("reading %s, end of header block? %s",
+ headerBlock, endOfHeaderBlock));
+ }
while (headerBlock.hasRemaining()) {
proceed(headerBlock, consumer);
}
if (endOfHeaderBlock && state != State.READY) {
- throw new UncheckedIOException(
- new ProtocolException("Unexpected end of header block"));
+ logger.log(NORMAL, () -> format("unexpected end of %s representation",
+ state));
+ throw new IOException("Unexpected end of header block");
}
}
- private void proceed(ByteBuffer input, DecodingCallback action) {
+ private void proceed(ByteBuffer input, DecodingCallback action)
+ throws IOException {
switch (state) {
case READY:
resumeReady(input);
@@ -239,14 +274,17 @@
resumeSizeUpdate(input, action);
break;
default:
- throw new InternalError(
- "Unexpected decoder state: " + String.valueOf(state));
+ throw new InternalError("Unexpected decoder state: " + state);
}
}
private void resumeReady(ByteBuffer input) {
int b = input.get(input.position()) & 0xff; // absolute read
State s = states[b];
+ if (logger.isLoggable(EXTRA)) {
+ logger.log(EXTRA, () -> format("next binary representation %s (first byte 0x%02x)",
+ s, b));
+ }
switch (s) {
case INDEXED:
integerReader.configure(7);
@@ -292,20 +330,36 @@
// | 1 | Index (7+) |
// +---+---------------------------+
//
- private void resumeIndexed(ByteBuffer input, DecodingCallback action) {
+ private void resumeIndexed(ByteBuffer input, DecodingCallback action)
+ throws IOException {
if (!integerReader.read(input)) {
return;
}
intValue = integerReader.get();
integerReader.reset();
+ if (logger.isLoggable(NORMAL)) {
+ logger.log(NORMAL, () -> format("indexed %s", intValue));
+ }
try {
- HeaderTable.HeaderField f = table.get(intValue);
+ HeaderTable.HeaderField f = getHeaderFieldAt(intValue);
action.onIndexed(intValue, f.name, f.value);
} finally {
state = State.READY;
}
}
+ private HeaderTable.HeaderField getHeaderFieldAt(int index)
+ throws IOException
+ {
+ HeaderTable.HeaderField f;
+ try {
+ f = table.get(index);
+ } catch (IndexOutOfBoundsException e) {
+ throw new IOException("header fields table index", e);
+ }
+ return f;
+ }
+
// 0 1 2 3 4 5 6 7
// +---+---+---+---+---+---+---+---+
// | 0 | 0 | 0 | 0 | Index (4+) |
@@ -328,15 +382,24 @@
// | Value String (Length octets) |
// +-------------------------------+
//
- private void resumeLiteral(ByteBuffer input, DecodingCallback action) {
+ private void resumeLiteral(ByteBuffer input, DecodingCallback action)
+ throws IOException {
if (!completeReading(input)) {
return;
}
try {
if (firstValueIndex) {
- HeaderTable.HeaderField f = table.get(intValue);
+ if (logger.isLoggable(NORMAL)) {
+ logger.log(NORMAL, () -> format("literal without indexing ('%s', '%s')",
+ intValue, value));
+ }
+ HeaderTable.HeaderField f = getHeaderFieldAt(intValue);
action.onLiteral(intValue, f.name, value, valueHuffmanEncoded);
} else {
+ if (logger.isLoggable(NORMAL)) {
+ logger.log(NORMAL, () -> format("literal without indexing ('%s', '%s')",
+ name, value));
+ }
action.onLiteral(name, nameHuffmanEncoded, value, valueHuffmanEncoded);
}
} finally {
@@ -367,7 +430,9 @@
// | Value String (Length octets) |
// +-------------------------------+
//
- private void resumeLiteralWithIndexing(ByteBuffer input, DecodingCallback action) {
+ private void resumeLiteralWithIndexing(ByteBuffer input,
+ DecodingCallback action)
+ throws IOException {
if (!completeReading(input)) {
return;
}
@@ -381,17 +446,22 @@
String n;
String v = value.toString();
if (firstValueIndex) {
- HeaderTable.HeaderField f = table.get(intValue);
+ if (logger.isLoggable(NORMAL)) {
+ logger.log(NORMAL, () -> format("literal with incremental indexing ('%s', '%s')",
+ intValue, value));
+ }
+ HeaderTable.HeaderField f = getHeaderFieldAt(intValue);
n = f.name;
action.onLiteralWithIndexing(intValue, n, v, valueHuffmanEncoded);
} else {
n = name.toString();
+ if (logger.isLoggable(NORMAL)) {
+ logger.log(NORMAL, () -> format("literal with incremental indexing ('%s', '%s')",
+ n, value));
+ }
action.onLiteralWithIndexing(n, nameHuffmanEncoded, v, valueHuffmanEncoded);
}
table.put(n, v);
- } catch (IllegalArgumentException | IllegalStateException e) {
- throw new UncheckedIOException(
- (IOException) new ProtocolException().initCause(e));
} finally {
cleanUpAfterReading();
}
@@ -419,15 +489,25 @@
// | Value String (Length octets) |
// +-------------------------------+
//
- private void resumeLiteralNeverIndexed(ByteBuffer input, DecodingCallback action) {
+ private void resumeLiteralNeverIndexed(ByteBuffer input,
+ DecodingCallback action)
+ throws IOException {
if (!completeReading(input)) {
return;
}
try {
if (firstValueIndex) {
- HeaderTable.HeaderField f = table.get(intValue);
+ if (logger.isLoggable(NORMAL)) {
+ logger.log(NORMAL, () -> format("literal never indexed ('%s', '%s')",
+ intValue, value));
+ }
+ HeaderTable.HeaderField f = getHeaderFieldAt(intValue);
action.onLiteralNeverIndexed(intValue, f.name, value, valueHuffmanEncoded);
} else {
+ if (logger.isLoggable(NORMAL)) {
+ logger.log(NORMAL, () -> format("literal never indexed ('%s', '%s')",
+ name, value));
+ }
action.onLiteralNeverIndexed(name, nameHuffmanEncoded, value, valueHuffmanEncoded);
}
} finally {
@@ -440,16 +520,21 @@
// | 0 | 0 | 1 | Max size (5+) |
// +---+---------------------------+
//
- private void resumeSizeUpdate(ByteBuffer input, DecodingCallback action) {
+ private void resumeSizeUpdate(ByteBuffer input,
+ DecodingCallback action) throws IOException {
if (!integerReader.read(input)) {
return;
}
intValue = integerReader.get();
+ if (logger.isLoggable(NORMAL)) {
+ logger.log(NORMAL, () -> format("dynamic table size update %s",
+ intValue));
+ }
assert intValue >= 0;
if (intValue > capacity) {
- throw new UncheckedIOException(new ProtocolException(
- format("Received capacity exceeds expected: " +
- "capacity=%s, expected=%s", intValue, capacity)));
+ throw new IOException(
+ format("Received capacity exceeds expected: capacity=%s, expected=%s",
+ intValue, capacity));
}
integerReader.reset();
try {
@@ -460,7 +545,7 @@
}
}
- private boolean completeReading(ByteBuffer input) {
+ private boolean completeReading(ByteBuffer input) throws IOException {
if (!firstValueRead) {
if (firstValueIndex) {
if (!integerReader.read(input)) {
--- a/src/jdk.incubator.httpclient/share/classes/jdk/incubator/http/internal/hpack/DecodingCallback.java Sun Nov 05 17:05:57 2017 +0000
+++ b/src/jdk.incubator.httpclient/share/classes/jdk/incubator/http/internal/hpack/DecodingCallback.java Sun Nov 05 17:32:13 2017 +0000
@@ -1,5 +1,5 @@
/*
- * Copyright (c) 2015, 2016, Oracle and/or its affiliates. All rights reserved.
+ * Copyright (c) 2015, 2017, Oracle and/or its affiliates. All rights reserved.
* DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER.
*
* This code is free software; you can redistribute it and/or modify it
@@ -35,10 +35,10 @@
*
* @apiNote
*
- * <p> The callback provides methods for all possible <a
- * href="https://tools.ietf.org/html/rfc7541#section-6">binary
- * representations</a>. This could be useful for implementing an intermediary,
- * logging, debugging, etc.
+ * <p> The callback provides methods for all possible
+ * <a href="https://tools.ietf.org/html/rfc7541#section-6">binary representations</a>.
+ * This could be useful for implementing an intermediary, logging, debugging,
+ * etc.
*
* <p> The callback is an interface in order to interoperate with lambdas (in
* the most common use case):
@@ -98,7 +98,8 @@
* @see #onLiteralNeverIndexed(int, CharSequence, CharSequence, boolean)
* @see #onLiteralNeverIndexed(CharSequence, boolean, CharSequence, boolean)
*/
- default void onDecoded(CharSequence name, CharSequence value,
+ default void onDecoded(CharSequence name,
+ CharSequence value,
boolean sensitive) {
onDecoded(name, value);
}
@@ -142,8 +143,10 @@
* @param valueHuffman
* if the {@code value} was Huffman encoded
*/
- default void onLiteral(int index, CharSequence name,
- CharSequence value, boolean valueHuffman) {
+ default void onLiteral(int index,
+ CharSequence name,
+ CharSequence value,
+ boolean valueHuffman) {
onDecoded(name, value, false);
}
@@ -166,8 +169,10 @@
* @param valueHuffman
* if the {@code value} was Huffman encoded
*/
- default void onLiteral(CharSequence name, boolean nameHuffman,
- CharSequence value, boolean valueHuffman) {
+ default void onLiteral(CharSequence name,
+ boolean nameHuffman,
+ CharSequence value,
+ boolean valueHuffman) {
onDecoded(name, value, false);
}
@@ -190,7 +195,8 @@
* @param valueHuffman
* if the {@code value} was Huffman encoded
*/
- default void onLiteralNeverIndexed(int index, CharSequence name,
+ default void onLiteralNeverIndexed(int index,
+ CharSequence name,
CharSequence value,
boolean valueHuffman) {
onDecoded(name, value, true);
@@ -215,8 +221,10 @@
* @param valueHuffman
* if the {@code value} was Huffman encoded
*/
- default void onLiteralNeverIndexed(CharSequence name, boolean nameHuffman,
- CharSequence value, boolean valueHuffman) {
+ default void onLiteralNeverIndexed(CharSequence name,
+ boolean nameHuffman,
+ CharSequence value,
+ boolean valueHuffman) {
onDecoded(name, value, true);
}
@@ -241,7 +249,8 @@
*/
default void onLiteralWithIndexing(int index,
CharSequence name,
- CharSequence value, boolean valueHuffman) {
+ CharSequence value,
+ boolean valueHuffman) {
onDecoded(name, value, false);
}
@@ -264,8 +273,10 @@
* @param valueHuffman
* if the {@code value} was Huffman encoded
*/
- default void onLiteralWithIndexing(CharSequence name, boolean nameHuffman,
- CharSequence value, boolean valueHuffman) {
+ default void onLiteralWithIndexing(CharSequence name,
+ boolean nameHuffman,
+ CharSequence value,
+ boolean valueHuffman) {
onDecoded(name, value, false);
}
--- a/src/jdk.incubator.httpclient/share/classes/jdk/incubator/http/internal/hpack/Encoder.java Sun Nov 05 17:05:57 2017 +0000
+++ b/src/jdk.incubator.httpclient/share/classes/jdk/incubator/http/internal/hpack/Encoder.java Sun Nov 05 17:32:13 2017 +0000
@@ -1,5 +1,5 @@
/*
- * Copyright (c) 2014, 2016, Oracle and/or its affiliates. All rights reserved.
+ * Copyright (c) 2014, 2017, Oracle and/or its affiliates. All rights reserved.
* DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER.
*
* This code is free software; you can redistribute it and/or modify it
@@ -24,27 +24,32 @@
*/
package jdk.incubator.http.internal.hpack;
+import jdk.incubator.http.internal.hpack.HPACK.Logger;
+
import java.nio.ByteBuffer;
import java.nio.ReadOnlyBufferException;
import java.util.LinkedList;
import java.util.List;
+import java.util.concurrent.atomic.AtomicLong;
import static java.lang.String.format;
import static java.util.Objects.requireNonNull;
+import static jdk.incubator.http.internal.hpack.HPACK.Logger.Level.EXTRA;
+import static jdk.incubator.http.internal.hpack.HPACK.Logger.Level.NORMAL;
/**
* Encodes headers to their binary representation.
*
- * <p>Typical lifecycle looks like this:
+ * <p> Typical lifecycle looks like this:
*
* <p> {@link #Encoder(int) new Encoder}
* ({@link #setMaxCapacity(int) setMaxCapacity}?
* {@link #encode(ByteBuffer) encode})*
*
- * <p> Suppose headers are represented by {@code Map<String, List<String>>}. A
- * supplier and a consumer of {@link ByteBuffer}s in forms of {@code
- * Supplier<ByteBuffer>} and {@code Consumer<ByteBuffer>} respectively. Then to
- * encode headers, the following approach might be used:
+ * <p> Suppose headers are represented by {@code Map<String, List<String>>}.
+ * A supplier and a consumer of {@link ByteBuffer}s in forms of
+ * {@code Supplier<ByteBuffer>} and {@code Consumer<ByteBuffer>} respectively.
+ * Then to encode headers, the following approach might be used:
*
* <pre>{@code
* for (Map.Entry<String, List<String>> h : headers.entrySet()) {
@@ -61,10 +66,9 @@
* }
* }</pre>
*
- * <p> Though the specification <a
- * href="https://tools.ietf.org/html/rfc7541#section-2"> does not define</a> how
- * an encoder is to be implemented, a default implementation is provided by the
- * method {@link #header(CharSequence, CharSequence, boolean)}.
+ * <p> Though the specification <a href="https://tools.ietf.org/html/rfc7541#section-2">does not define</a>
+ * how an encoder is to be implemented, a default implementation is provided by
+ * the method {@link #header(CharSequence, CharSequence, boolean)}.
*
* <p> To provide a custom encoding implementation, {@code Encoder} has to be
* extended. A subclass then can access methods for encoding using specific
@@ -85,8 +89,8 @@
* the resulting header block afterwards.
*
* <p> Splitting the encoding operation into header set up and header encoding,
- * separates long lived arguments ({@code name}, {@code value}, {@code
- * sensitivity}, etc.) from the short lived ones (e.g. {@code buffer}),
+ * separates long lived arguments ({@code name}, {@code value},
+ * {@code sensitivity}, etc.) from the short lived ones (e.g. {@code buffer}),
* simplifying each operation itself.
*
* @implNote
@@ -99,9 +103,13 @@
*/
public class Encoder {
+ private static final AtomicLong ENCODERS_IDS = new AtomicLong();
+
// TODO: enum: no huffman/smart huffman/always huffman
private static final boolean DEFAULT_HUFFMAN = true;
+ private final Logger logger;
+ private final long id;
private final IndexedWriter indexedWriter = new IndexedWriter();
private final LiteralWriter literalWriter = new LiteralWriter();
private final LiteralNeverIndexedWriter literalNeverIndexedWriter
@@ -129,9 +137,8 @@
* header table.
*
* <p> The value has to be agreed between decoder and encoder out-of-band,
- * e.g. by a protocol that uses HPACK (see <a
- * href="https://tools.ietf.org/html/rfc7541#section-4.2">4.2. Maximum Table
- * Size</a>).
+ * e.g. by a protocol that uses HPACK
+ * (see <a href="https://tools.ietf.org/html/rfc7541#section-4.2">4.2. Maximum Table Size</a>).
*
* @param maxCapacity
* a non-negative integer
@@ -140,14 +147,33 @@
* if maxCapacity is negative
*/
public Encoder(int maxCapacity) {
+ id = ENCODERS_IDS.incrementAndGet();
+ this.logger = HPACK.getLogger().subLogger("Encoder#" + id);
+ if (logger.isLoggable(NORMAL)) {
+ logger.log(NORMAL, () -> format("new encoder with maximum table size %s",
+ maxCapacity));
+ }
+ if (logger.isLoggable(EXTRA)) {
+ /* To correlate with logging outside HPACK, knowing
+ hashCode/toString is important */
+ logger.log(EXTRA, () -> {
+ String hashCode = Integer.toHexString(
+ System.identityHashCode(this));
+ /* Since Encoder can be subclassed hashCode AND identity
+ hashCode might be different. So let's print both. */
+ return format("toString='%s', hashCode=%s, identityHashCode=%s",
+ toString(), hashCode(), hashCode);
+ });
+ }
if (maxCapacity < 0) {
- throw new IllegalArgumentException("maxCapacity >= 0: " + maxCapacity);
+ throw new IllegalArgumentException(
+ "maxCapacity >= 0: " + maxCapacity);
}
// Initial maximum capacity update mechanics
minCapacity = Long.MAX_VALUE;
currCapacity = -1;
- setMaxCapacity(maxCapacity);
- headerTable = new HeaderTable(lastCapacity);
+ setMaxCapacity0(maxCapacity);
+ headerTable = new HeaderTable(lastCapacity, logger.subLogger("HeaderTable"));
}
/**
@@ -176,6 +202,10 @@
* Sets up the given header {@code (name, value)} with possibly sensitive
* value.
*
+ * <p> If the {@code value} is sensitive (think security, secrecy, etc.)
+ * this encoder will compress it using a special representation
+ * (see <a href="https://tools.ietf.org/html/rfc7541#section-6.2.3">6.2.3. Literal Header Field Never Indexed</a>).
+ *
* <p> Fixates {@code name} and {@code value} for the duration of encoding.
*
* @param name
@@ -193,8 +223,13 @@
* @see #header(CharSequence, CharSequence)
* @see DecodingCallback#onDecoded(CharSequence, CharSequence, boolean)
*/
- public void header(CharSequence name, CharSequence value,
+ public void header(CharSequence name,
+ CharSequence value,
boolean sensitive) throws IllegalStateException {
+ if (logger.isLoggable(NORMAL)) {
+ logger.log(NORMAL, () -> format("encoding ('%s', '%s'), sensitive: %s",
+ name, value, sensitive));
+ }
// Arguably a good balance between complexity of implementation and
// efficiency of encoding
requireNonNull(name, "name");
@@ -222,9 +257,8 @@
* Sets a maximum capacity of the header table.
*
* <p> The value has to be agreed between decoder and encoder out-of-band,
- * e.g. by a protocol that uses HPACK (see <a
- * href="https://tools.ietf.org/html/rfc7541#section-4.2">4.2. Maximum Table
- * Size</a>).
+ * e.g. by a protocol that uses HPACK
+ * (see <a href="https://tools.ietf.org/html/rfc7541#section-4.2">4.2. Maximum Table Size</a>).
*
* <p> May be called any number of times after or before a complete header
* has been encoded.
@@ -242,11 +276,23 @@
* hasn't yet started to encode it
*/
public void setMaxCapacity(int capacity) {
+ if (logger.isLoggable(NORMAL)) {
+ logger.log(NORMAL, () -> format("setting maximum table size to %s",
+ capacity));
+ }
+ setMaxCapacity0(capacity);
+ }
+
+ private void setMaxCapacity0(int capacity) {
checkEncoding();
if (capacity < 0) {
throw new IllegalArgumentException("capacity >= 0: " + capacity);
}
int calculated = calculateCapacity(capacity);
+ if (logger.isLoggable(NORMAL)) {
+ logger.log(NORMAL, () -> format("actual maximum table size will be %s",
+ calculated));
+ }
if (calculated < 0 || calculated > capacity) {
throw new IllegalArgumentException(
format("0 <= calculated <= capacity: calculated=%s, capacity=%s",
@@ -263,9 +309,22 @@
minCapacity = Math.min(minCapacity, lastCapacity);
}
+ /**
+ * Calculates actual capacity to be used by this encoder in response to
+ * a request to update maximum table size.
+ *
+ * <p> Default implementation does not add anything to the headers table,
+ * hence this method returns {@code 0}.
+ *
+ * <p> It is an error to return a value {@code c}, where {@code c < 0} or
+ * {@code c > maxCapacity}.
+ *
+ * @param maxCapacity
+ * upper bound
+ *
+ * @return actual capacity
+ */
protected int calculateCapacity(int maxCapacity) {
- // Default implementation of the Encoder won't add anything to the
- // table, therefore no need for a table space
return 0;
}
@@ -298,7 +357,10 @@
if (!encoding) {
throw new IllegalStateException("A header hasn't been set up");
}
- if (!prependWithCapacityUpdate(headerBlock)) {
+ if (logger.isLoggable(EXTRA)) {
+ logger.log(EXTRA, () -> format("writing to %s", headerBlock));
+ }
+ if (!prependWithCapacityUpdate(headerBlock)) { // TODO: log
return false;
}
boolean done = writer.write(headerTable, headerBlock);
@@ -339,21 +401,35 @@
protected final void indexed(int index) throws IndexOutOfBoundsException {
checkEncoding();
+ if (logger.isLoggable(EXTRA)) {
+ logger.log(EXTRA, () -> format("indexed %s", index));
+ }
encoding = true;
writer = indexedWriter.index(index);
}
- protected final void literal(int index, CharSequence value,
+ protected final void literal(int index,
+ CharSequence value,
boolean useHuffman)
throws IndexOutOfBoundsException {
+ if (logger.isLoggable(EXTRA)) {
+ logger.log(EXTRA, () -> format("literal without indexing ('%s', '%s')",
+ index, value));
+ }
checkEncoding();
encoding = true;
writer = literalWriter
.index(index).value(value, useHuffman);
}
- protected final void literal(CharSequence name, boolean nameHuffman,
- CharSequence value, boolean valueHuffman) {
+ protected final void literal(CharSequence name,
+ boolean nameHuffman,
+ CharSequence value,
+ boolean valueHuffman) {
+ if (logger.isLoggable(EXTRA)) {
+ logger.log(EXTRA, () -> format("literal without indexing ('%s', '%s')",
+ name, value));
+ }
checkEncoding();
encoding = true;
writer = literalWriter
@@ -364,6 +440,10 @@
CharSequence value,
boolean valueHuffman)
throws IndexOutOfBoundsException {
+ if (logger.isLoggable(EXTRA)) {
+ logger.log(EXTRA, () -> format("literal never indexed ('%s', '%s')",
+ index, value));
+ }
checkEncoding();
encoding = true;
writer = literalNeverIndexedWriter
@@ -374,6 +454,10 @@
boolean nameHuffman,
CharSequence value,
boolean valueHuffman) {
+ if (logger.isLoggable(EXTRA)) {
+ logger.log(EXTRA, () -> format("literal never indexed ('%s', '%s')",
+ name, value));
+ }
checkEncoding();
encoding = true;
writer = literalNeverIndexedWriter
@@ -384,6 +468,10 @@
CharSequence value,
boolean valueHuffman)
throws IndexOutOfBoundsException {
+ if (logger.isLoggable(EXTRA)) {
+ logger.log(EXTRA, () -> format("literal with incremental indexing ('%s', '%s')",
+ index, value));
+ }
checkEncoding();
encoding = true;
writer = literalWithIndexingWriter
@@ -394,6 +482,10 @@
boolean nameHuffman,
CharSequence value,
boolean valueHuffman) {
+ if (logger.isLoggable(EXTRA)) { // TODO: include huffman info?
+ logger.log(EXTRA, () -> format("literal with incremental indexing ('%s', '%s')",
+ name, value));
+ }
checkEncoding();
encoding = true;
writer = literalWithIndexingWriter
@@ -402,6 +494,10 @@
protected final void sizeUpdate(int capacity)
throws IllegalArgumentException {
+ if (logger.isLoggable(EXTRA)) {
+ logger.log(EXTRA, () -> format("dynamic table size update %s",
+ capacity));
+ }
checkEncoding();
// Ensure subclass follows the contract
if (capacity > this.maxCapacity) {
@@ -420,7 +516,7 @@
return headerTable;
}
- protected final void checkEncoding() {
+ protected final void checkEncoding() { // TODO: better name e.g. checkIfEncodingInProgress()
if (encoding) {
throw new IllegalStateException(
"Previous encoding operation hasn't finished yet");
--- /dev/null Thu Jan 01 00:00:00 1970 +0000
+++ b/src/jdk.incubator.httpclient/share/classes/jdk/incubator/http/internal/hpack/HPACK.java Sun Nov 05 17:32:13 2017 +0000
@@ -0,0 +1,182 @@
+/*
+ * Copyright (c) 2017, Oracle and/or its affiliates. All rights reserved.
+ * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER.
+ *
+ * This code is free software; you can redistribute it and/or modify it
+ * under the terms of the GNU General Public License version 2 only, as
+ * published by the Free Software Foundation. Oracle designates this
+ * particular file as subject to the "Classpath" exception as provided
+ * by Oracle in the LICENSE file that accompanied this code.
+ *
+ * This code is distributed in the hope that it will be useful, but WITHOUT
+ * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or
+ * FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License
+ * version 2 for more details (a copy is included in the LICENSE file that
+ * accompanied this code).
+ *
+ * You should have received a copy of the GNU General Public License version
+ * 2 along with this work; if not, write to the Free Software Foundation,
+ * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA.
+ *
+ * Please contact Oracle, 500 Oracle Parkway, Redwood Shores, CA 94065 USA
+ * or visit www.oracle.com if you need additional information or have any
+ * questions.
+ */
+package jdk.incubator.http.internal.hpack;
+
+import jdk.incubator.http.internal.hpack.HPACK.Logger.Level;
+import jdk.internal.vm.annotation.Stable;
+
+import java.security.AccessController;
+import java.security.PrivilegedAction;
+import java.util.Arrays;
+import java.util.Map;
+import java.util.function.Supplier;
+
+import static java.lang.String.format;
+import static java.util.stream.Collectors.joining;
+import static jdk.incubator.http.internal.hpack.HPACK.Logger.Level.EXTRA;
+import static jdk.incubator.http.internal.hpack.HPACK.Logger.Level.NONE;
+import static jdk.incubator.http.internal.hpack.HPACK.Logger.Level.NORMAL;
+
+/**
+ * Internal utilities and stuff.
+ */
+public final class HPACK {
+
+ private static final RootLogger LOGGER;
+ private static final Map<String, Level> logLevels =
+ Map.of("NORMAL", NORMAL, "EXTRA", EXTRA);
+
+ static {
+ String PROPERTY = "jdk.internal.httpclient.hpack.log.level";
+
+ String value = AccessController.doPrivileged(
+ (PrivilegedAction<String>) () -> System.getProperty(PROPERTY));
+
+ if (value == null) {
+ LOGGER = new RootLogger(NONE);
+ } else {
+ String upperCasedValue = value.toUpperCase();
+ Level l = logLevels.get(upperCasedValue);
+ if (l == null) {
+ LOGGER = new RootLogger(NONE);
+ LOGGER.log(() -> format("%s value '%s' is not recognized (use %s), logging is disabled",
+ PROPERTY, value, logLevels.keySet().stream().collect(joining(", "))));
+ } else {
+ LOGGER = new RootLogger(l);
+ LOGGER.log(() -> format("logging level is %s", l));
+ }
+ }
+ }
+
+ public static Logger getLogger() {
+ return LOGGER;
+ }
+
+ private HPACK() { }
+
+ /**
+ * The purpose of this logger is to provide means of diagnosing issues _in
+ * the HPACK implementation_. It's not a general purpose logger.
+ */
+ public static class Logger {
+
+ /**
+ * Log detail level.
+ */
+ public enum Level {
+
+ NONE(0),
+ NORMAL(1),
+ EXTRA(2);
+
+ private final int level;
+
+ Level(int i) {
+ level = i;
+ }
+
+ public final boolean implies(Level other) {
+ return this.level >= other.level;
+ }
+ }
+
+ private final String name;
+ @Stable
+ private final Logger[] path; /* A path to parent: [root, ..., parent, this] */
+
+ private Logger(Logger[] path, String name) {
+ Logger[] p = Arrays.copyOfRange(path, 0, path.length + 1);
+ p[path.length] = this;
+ this.path = p;
+ this.name = name;
+ }
+
+ protected final String getName() {
+ return name;
+ }
+ /*
+ * Usual performance trick for logging, reducing performance overhead in
+ * the case where logging with the specified level is a NOP.
+ */
+
+ public boolean isLoggable(Level level) {
+ return isLoggable(path, level);
+ }
+
+ public void log(Level level, Supplier<? extends CharSequence> s) {
+ log(path, level, s);
+ }
+
+ public Logger subLogger(String name) {
+ return new Logger(path, name);
+ }
+
+ protected boolean isLoggable(Logger[] path, Level level) {
+ return parent().isLoggable(path, level);
+ }
+
+ protected void log(Logger[] path,
+ Level level,
+ Supplier<? extends CharSequence> s) {
+ parent().log(path, level, s);
+ }
+
+ protected final Logger parent() {
+ return path[path.length - 2];
+ }
+ }
+
+ private static final class RootLogger extends Logger {
+
+ private final Level level;
+
+ protected RootLogger(Level level) {
+ super(new Logger[]{ }, "hpack");
+ this.level = level;
+ }
+
+ @Override
+ protected boolean isLoggable(Logger[] path, Level level) {
+ return this.level.implies(level);
+ }
+
+ @Override
+ protected void log(Logger[] path,
+ Level level,
+ Supplier<? extends CharSequence> s) {
+ if (this.level.implies(level)) {
+ StringBuilder b = new StringBuilder();
+ for (Logger p : path) {
+ b.append('/').append(p.getName());
+ }
+ System.out.println(b.toString() + ' ' + s.get());
+ }
+ }
+
+ public void log(Supplier<? extends CharSequence> s) {
+ System.out.println(getName() + ' ' + s.get());
+ }
+ }
+}
--- a/src/jdk.incubator.httpclient/share/classes/jdk/incubator/http/internal/hpack/HeaderTable.java Sun Nov 05 17:05:57 2017 +0000
+++ b/src/jdk.incubator.httpclient/share/classes/jdk/incubator/http/internal/hpack/HeaderTable.java Sun Nov 05 17:32:13 2017 +0000
@@ -24,6 +24,7 @@
*/
package jdk.incubator.http.internal.hpack;
+import jdk.incubator.http.internal.hpack.HPACK.Logger;
import jdk.internal.vm.annotation.Stable;
import java.util.HashMap;
@@ -32,6 +33,8 @@
import java.util.NoSuchElementException;
import static java.lang.String.format;
+import static jdk.incubator.http.internal.hpack.HPACK.Logger.Level.EXTRA;
+import static jdk.incubator.http.internal.hpack.HPACK.Logger.Level.NORMAL;
//
// Header Table combined from two tables: static and dynamic.
@@ -122,11 +125,13 @@
}
}
+ private final Logger logger;
private final Table dynamicTable = new Table(0);
private int maxSize;
private int size;
- public HeaderTable(int maxSize) {
+ public HeaderTable(int maxSize, Logger logger) {
+ this.logger = logger;
setMaxSize(maxSize);
}
@@ -211,21 +216,41 @@
}
private void put(HeaderField h) {
+ if (logger.isLoggable(NORMAL)) {
+ logger.log(NORMAL, () -> format("adding ('%s', '%s')",
+ h.name, h.value));
+ }
int entrySize = sizeOf(h);
+ if (logger.isLoggable(EXTRA)) {
+ logger.log(EXTRA, () -> format("size of ('%s', '%s') is %s",
+ h.name, h.value, entrySize));
+ }
while (entrySize > maxSize - size && size != 0) {
+ if (logger.isLoggable(EXTRA)) {
+ logger.log(EXTRA, () -> format("insufficient space %s, must evict entry",
+ (maxSize - size)));
+ }
evictEntry();
}
if (entrySize > maxSize - size) {
+ if (logger.isLoggable(EXTRA)) {
+ logger.log(EXTRA, () -> format("not adding ('%s, '%s'), too big",
+ h.name, h.value));
+ }
return;
}
size += entrySize;
dynamicTable.add(h);
+ if (logger.isLoggable(EXTRA)) {
+ logger.log(EXTRA, () -> format("('%s, '%s') added", h.name, h.value));
+ logger.log(EXTRA, this::toString);
+ }
}
void setMaxSize(int maxSize) {
if (maxSize < 0) {
- throw new IllegalArgumentException
- ("maxSize >= 0: maxSize=" + maxSize);
+ throw new IllegalArgumentException(
+ "maxSize >= 0: maxSize=" + maxSize);
}
while (maxSize < size && size != 0) {
evictEntry();
@@ -237,22 +262,29 @@
HeaderField evictEntry() {
HeaderField f = dynamicTable.remove();
- size -= sizeOf(f);
+ int s = sizeOf(f);
+ this.size -= s;
+ if (logger.isLoggable(EXTRA)) {
+ logger.log(EXTRA, () -> format("evicted entry ('%s', '%s') of size %s",
+ f.name, f.value, s));
+ logger.log(EXTRA, this::toString);
+ }
return f;
}
@Override
public String toString() {
double used = maxSize == 0 ? 0 : 100 * (((double) size) / maxSize);
- return format("entries: %d; used %s/%s (%.1f%%)", dynamicTable.size(),
- size, maxSize, used);
+ return format("dynamic length: %d, full length: %s, used space: %s/%s (%.1f%%)",
+ dynamicTable.size(), length(), size, maxSize, used);
}
- int checkIndex(int index) {
- if (index < 1 || index > STATIC_TABLE_LENGTH + dynamicTable.size()) {
- throw new IllegalArgumentException(
+ private int checkIndex(int index) {
+ int len = length();
+ if (index < 1 || index > len) {
+ throw new IndexOutOfBoundsException(
format("1 <= index <= length(): index=%s, length()=%s",
- index, length()));
+ index, len));
}
return index;
}
@@ -272,7 +304,7 @@
StringBuilder b = new StringBuilder();
for (int i = 1, size = dynamicTable.size(); i <= size; i++) {
HeaderField e = dynamicTable.get(i);
- b.append(format("[%3d] (s = %3d) %s: %s\n", i,
+ b.append(format("[%3d] (s = %3d) %s: %s%n", i,
sizeOf(e), e.name, e.value));
}
b.append(format(" Table size:%4s", this.size));
--- a/src/jdk.incubator.httpclient/share/classes/jdk/incubator/http/internal/hpack/Huffman.java Sun Nov 05 17:05:57 2017 +0000
+++ b/src/jdk.incubator.httpclient/share/classes/jdk/incubator/http/internal/hpack/Huffman.java Sun Nov 05 17:32:13 2017 +0000
@@ -1,5 +1,5 @@
/*
- * Copyright (c) 2014, 2016, Oracle and/or its affiliates. All rights reserved.
+ * Copyright (c) 2014, 2017, Oracle and/or its affiliates. All rights reserved.
* DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER.
*
* This code is free software; you can redistribute it and/or modify it
@@ -25,7 +25,6 @@
package jdk.incubator.http.internal.hpack;
import java.io.IOException;
-import java.io.UncheckedIOException;
import java.nio.ByteBuffer;
import static java.lang.String.format;
@@ -51,16 +50,18 @@
reset();
}
- public void read(ByteBuffer source, Appendable destination,
- boolean isLast) {
+ public void read(ByteBuffer source,
+ Appendable destination,
+ boolean isLast) throws IOException {
read(source, destination, true, isLast);
}
// Takes 'isLast' rather than returns whether the reading is done or
// not, for more informative exceptions.
- void read(ByteBuffer source, Appendable destination, boolean reportEOS,
- boolean isLast) {
-
+ void read(ByteBuffer source,
+ Appendable destination,
+ boolean reportEOS, /* reportEOS is exposed for tests */
+ boolean isLast) throws IOException {
Node c = curr;
int l = len;
/*
@@ -77,16 +78,20 @@
l++;
if (c.isLeaf()) {
if (reportEOS && c.isEOSPath) {
- throw new IllegalArgumentException("Encountered EOS");
+ throw new IOException("Encountered EOS");
+ }
+ char ch;
+ try {
+ ch = c.getChar();
+ } catch (IllegalStateException e) {
+ source.position(pos); // do we need this?
+ throw new IOException(e);
}
try {
- destination.append(c.getChar());
- } catch (RuntimeException | Error e) {
- source.position(pos);
+ destination.append(ch);
+ } catch (IOException e) {
+ source.position(pos); // do we need this?
throw e;
- } catch (IOException e) {
- source.position(pos);
- throw new UncheckedIOException(e);
}
c = INSTANCE.root;
l = 0;
@@ -107,11 +112,11 @@
return; // it's ok, some extra padding bits
}
if (c.isEOSPath) {
- throw new IllegalArgumentException(
+ throw new IOException(
"Padding is too long (len=" + len + ") " +
"or unexpected end of data");
}
- throw new IllegalArgumentException(
+ throw new IOException(
"Not a EOS prefix padding or unexpected end of data");
}
@@ -509,8 +514,8 @@
* @throws NullPointerException
* if the value is null
* @throws IndexOutOfBoundsException
- * if any invocation of {@code value.charAt(i)}, where {@code start
- * <= i < end} would throw an IndexOutOfBoundsException
+ * if any invocation of {@code value.charAt(i)}, where
+ * {@code start <= i < end} would throw an IndexOutOfBoundsException
*/
public int lengthOf(CharSequence value, int start, int end) {
int len = 0;
--- a/src/jdk.incubator.httpclient/share/classes/jdk/incubator/http/internal/hpack/ISO_8859_1.java Sun Nov 05 17:05:57 2017 +0000
+++ b/src/jdk.incubator.httpclient/share/classes/jdk/incubator/http/internal/hpack/ISO_8859_1.java Sun Nov 05 17:32:13 2017 +0000
@@ -1,5 +1,5 @@
/*
- * Copyright (c) 2015, 2016, Oracle and/or its affiliates. All rights reserved.
+ * Copyright (c) 2015, 2017, Oracle and/or its affiliates. All rights reserved.
* DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER.
*
* This code is free software; you can redistribute it and/or modify it
@@ -25,7 +25,6 @@
package jdk.incubator.http.internal.hpack;
import java.io.IOException;
-import java.io.UncheckedIOException;
import java.nio.ByteBuffer;
//
@@ -47,14 +46,15 @@
public static final class Reader {
- public void read(ByteBuffer source, Appendable destination) {
+ public void read(ByteBuffer source, Appendable destination)
+ throws IOException {
for (int i = 0, len = source.remaining(); i < len; i++) {
char c = (char) (source.get() & 0xff);
try {
destination.append(c);
} catch (IOException e) {
- throw new UncheckedIOException
- ("Error appending to the destination", e);
+ throw new IOException(
+ "Error appending to the destination", e);
}
}
}
--- a/src/jdk.incubator.httpclient/share/classes/jdk/incubator/http/internal/hpack/IndexNameValueWriter.java Sun Nov 05 17:05:57 2017 +0000
+++ b/src/jdk.incubator.httpclient/share/classes/jdk/incubator/http/internal/hpack/IndexNameValueWriter.java Sun Nov 05 17:32:13 2017 +0000
@@ -1,5 +1,5 @@
/*
- * Copyright (c) 2015, 2016, Oracle and/or its affiliates. All rights reserved.
+ * Copyright (c) 2015, 2017, Oracle and/or its affiliates. All rights reserved.
* DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER.
*
* This code is free software; you can redistribute it and/or modify it
@@ -73,7 +73,8 @@
return false;
}
} else {
- if (!intWriter.write(destination) || !nameWriter.write(destination)) {
+ if (!intWriter.write(destination) ||
+ !nameWriter.write(destination)) {
return false;
}
}
--- a/src/jdk.incubator.httpclient/share/classes/jdk/incubator/http/internal/hpack/IntegerReader.java Sun Nov 05 17:05:57 2017 +0000
+++ b/src/jdk.incubator.httpclient/share/classes/jdk/incubator/http/internal/hpack/IntegerReader.java Sun Nov 05 17:32:13 2017 +0000
@@ -1,5 +1,5 @@
/*
- * Copyright (c) 2014, 2016, Oracle and/or its affiliates. All rights reserved.
+ * Copyright (c) 2014, 2017, Oracle and/or its affiliates. All rights reserved.
* DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER.
*
* This code is free software; you can redistribute it and/or modify it
@@ -24,6 +24,7 @@
*/
package jdk.incubator.http.internal.hpack;
+import java.io.IOException;
import java.nio.ByteBuffer;
import java.util.Arrays;
@@ -73,7 +74,7 @@
return this;
}
- public boolean read(ByteBuffer input) {
+ public boolean read(ByteBuffer input) throws IOException {
if (state == NEW) {
throw new IllegalStateException("Configure first");
}
@@ -105,7 +106,7 @@
i = input.get();
long increment = b * (i & 127);
if (r + increment > maxValue) {
- throw new IllegalArgumentException(format(
+ throw new IOException(format(
"Integer overflow: maxValue=%,d, value=%,d",
maxValue, r + increment));
}
--- a/src/jdk.incubator.httpclient/share/classes/jdk/incubator/http/internal/hpack/StringReader.java Sun Nov 05 17:05:57 2017 +0000
+++ b/src/jdk.incubator.httpclient/share/classes/jdk/incubator/http/internal/hpack/StringReader.java Sun Nov 05 17:32:13 2017 +0000
@@ -1,5 +1,5 @@
/*
- * Copyright (c) 2015, 2016, Oracle and/or its affiliates. All rights reserved.
+ * Copyright (c) 2015, 2017, Oracle and/or its affiliates. All rights reserved.
* DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER.
*
* This code is free software; you can redistribute it and/or modify it
@@ -24,6 +24,7 @@
*/
package jdk.incubator.http.internal.hpack;
+import java.io.IOException;
import java.nio.ByteBuffer;
import java.util.Arrays;
@@ -51,7 +52,7 @@
private boolean huffman;
private int remainingLength;
- boolean read(ByteBuffer input, Appendable output) {
+ boolean read(ByteBuffer input, Appendable output) throws IOException {
if (state == DONE) {
return true;
}
--- a/src/jdk.incubator.httpclient/share/classes/jdk/incubator/http/internal/hpack/StringWriter.java Sun Nov 05 17:05:57 2017 +0000
+++ b/src/jdk.incubator.httpclient/share/classes/jdk/incubator/http/internal/hpack/StringWriter.java Sun Nov 05 17:32:13 2017 +0000
@@ -1,5 +1,5 @@
/*
- * Copyright (c) 2015, 2016, Oracle and/or its affiliates. All rights reserved.
+ * Copyright (c) 2015, 2017, Oracle and/or its affiliates. All rights reserved.
* DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER.
*
* This code is free software; you can redistribute it and/or modify it
@@ -62,7 +62,9 @@
return configure(input, 0, input.length(), huffman);
}
- StringWriter configure(CharSequence input, int start, int end,
+ StringWriter configure(CharSequence input,
+ int start,
+ int end,
boolean huffman) {
if (start < 0 || end < 0 || end > input.length() || start > end) {
throw new IndexOutOfBoundsException(
--- a/src/jdk.incubator.httpclient/share/classes/jdk/incubator/http/internal/websocket/CooperativeHandler.java Sun Nov 05 17:05:57 2017 +0000
+++ /dev/null Thu Jan 01 00:00:00 1970 +0000
@@ -1,208 +0,0 @@
-/*
- * Copyright (c) 2016, 2017, Oracle and/or its affiliates. All rights reserved.
- * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER.
- *
- * This code is free software; you can redistribute it and/or modify it
- * under the terms of the GNU General Public License version 2 only, as
- * published by the Free Software Foundation. Oracle designates this
- * particular file as subject to the "Classpath" exception as provided
- * by Oracle in the LICENSE file that accompanied this code.
- *
- * This code is distributed in the hope that it will be useful, but WITHOUT
- * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or
- * FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License
- * version 2 for more details (a copy is included in the LICENSE file that
- * accompanied this code).
- *
- * You should have received a copy of the GNU General Public License version
- * 2 along with this work; if not, write to the Free Software Foundation,
- * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA.
- *
- * Please contact Oracle, 500 Oracle Parkway, Redwood Shores, CA 94065 USA
- * or visit www.oracle.com if you need additional information or have any
- * questions.
- */
-
-package jdk.incubator.http.internal.websocket;
-
-import java.util.concurrent.atomic.AtomicInteger;
-import java.util.function.Consumer;
-
-import static java.util.Objects.requireNonNull;
-
-/*
- * A synchronization aid that assists a number of parties in running a task
- * in a mutually exclusive fashion.
- *
- * To run the task, a party invokes `handle`. To permanently prevent the task
- * from subsequent runs, the party invokes `stop`.
- *
- * The parties do not have to operate in different threads.
- *
- * The task can be either synchronous or asynchronous.
- *
- * If the task is synchronous, it is represented with `Runnable`.
- * The handler invokes `Runnable.run` to run the task.
- *
- * If the task is asynchronous, it is represented with `Consumer<Runnable>`.
- * The handler invokes `Consumer.accept(end)` to begin the task. The task
- * invokes `end.run()` when it has ended.
- *
- * The next run of the task will not begin until the previous run has finished.
- *
- * The task may invoke `handle()` by itself, it's a normal situation.
- */
-public final class CooperativeHandler {
-
- /*
- Since the task is fixed and known beforehand, no blocking synchronization
- (locks, queues, etc.) is required. The job can be done solely using
- nonblocking primitives.
-
- The machinery below addresses two problems:
-
- 1. Running the task in a sequential order (no concurrent runs):
-
- begin, end, begin, end...
-
- 2. Avoiding indefinite recursion:
-
- begin
- end
- begin
- end
- ...
-
- Problem #1 is solved with a finite state machine with 4 states:
-
- BEGIN, AGAIN, END, and STOP.
-
- Problem #2 is solved with a "state modifier" OFFLOAD.
-
- Parties invoke `handle()` to signal the task must run. A party that has
- invoked `handle()` either begins the task or exploits the party that is
- either beginning the task or ending it.
-
- The party that is trying to end the task either ends it or begins it
- again.
-
- To avoid indefinite recursion, before re-running the task tryEnd() sets
- OFFLOAD bit, signalling to its "child" tryEnd() that this ("parent")
- tryEnd() is available and the "child" must offload the task on to the
- "parent". Then a race begins. Whichever invocation of tryEnd() manages
- to unset OFFLOAD bit first does not do the work.
-
- There is at most 1 thread that is beginning the task and at most 2
- threads that are trying to end it: "parent" and "child". In case of a
- synchronous task "parent" and "child" are the same thread.
- */
-
- private static final int OFFLOAD = 1;
- private static final int AGAIN = 2;
- private static final int BEGIN = 4;
- private static final int STOP = 8;
- private static final int END = 16;
-
- private final AtomicInteger state = new AtomicInteger(END);
- private final Consumer<Runnable> begin;
-
- public CooperativeHandler(Runnable task) {
- this(asyncOf(task));
- }
-
- public CooperativeHandler(Consumer<Runnable> begin) {
- this.begin = requireNonNull(begin);
- }
-
- /*
- * Runs the task (though maybe by a different party).
- *
- * The recursion which is possible here will have the maximum depth of 1:
- *
- * this.handle()
- * begin.accept()
- * this.handle()
- */
- public void handle() {
- while (true) {
- int s = state.get();
- if (s == END) {
- if (state.compareAndSet(END, BEGIN)) {
- break;
- }
- } else if ((s & BEGIN) != 0) {
- // Tries to change the state to AGAIN, preserving OFFLOAD bit
- if (state.compareAndSet(s, AGAIN | (s & OFFLOAD))) {
- return;
- }
- } else if ((s & AGAIN) != 0 || s == STOP) {
- return;
- } else {
- throw new InternalError(String.valueOf(s));
- }
- }
- begin.accept(this::tryEnd);
- }
-
- private void tryEnd() {
- while (true) {
- int s;
- while (((s = state.get()) & OFFLOAD) != 0) {
- // Tries to offload ending of the task to the parent
- if (state.compareAndSet(s, s & ~OFFLOAD)) {
- return;
- }
- }
- while (true) {
- if (s == BEGIN) {
- if (state.compareAndSet(BEGIN, END)) {
- return;
- }
- } else if (s == AGAIN) {
- if (state.compareAndSet(AGAIN, BEGIN | OFFLOAD)) {
- break;
- }
- } else if (s == STOP) {
- return;
- } else {
- throw new InternalError(String.valueOf(s));
- }
- s = state.get();
- }
- begin.accept(this::tryEnd);
- }
- }
-
- /*
- * Checks whether or not this handler has been permanently stopped.
- *
- * Should be used from inside the task to poll the status of the handler,
- * pretty much the same way as it is done for threads:
- *
- * if (!Thread.currentThread().isInterrupted()) {
- * ...
- * }
- */
- public boolean isStopped() {
- return state.get() == STOP;
- }
-
- /*
- * Signals this handler to ignore subsequent invocations to `handle()`.
- *
- * If the task has already begun, this invocation will not affect it,
- * unless the task itself uses `isStopped()` method to check the state
- * of the handler.
- */
- public void stop() {
- state.set(STOP);
- }
-
- private static Consumer<Runnable> asyncOf(Runnable task) {
- requireNonNull(task);
- return ender -> {
- task.run();
- ender.run();
- };
- }
-}
--- a/src/jdk.incubator.httpclient/share/classes/jdk/incubator/http/internal/websocket/Receiver.java Sun Nov 05 17:05:57 2017 +0000
+++ b/src/jdk.incubator.httpclient/share/classes/jdk/incubator/http/internal/websocket/Receiver.java Sun Nov 05 17:32:13 2017 +0000
@@ -29,6 +29,7 @@
import java.nio.ByteBuffer;
import java.nio.channels.SelectionKey;
import java.util.concurrent.atomic.AtomicLong;
+import jdk.incubator.http.internal.common.SequentialScheduler;
/*
* Receives incoming data from the channel on demand and converts it into a
@@ -57,7 +58,7 @@
private final Frame.Reader reader = new Frame.Reader();
private final RawChannel.RawEvent event = createHandler();
private final AtomicLong demand = new AtomicLong();
- private final CooperativeHandler handler;
+ private final SequentialScheduler pushScheduler;
private ByteBuffer data;
private volatile int state;
@@ -74,7 +75,7 @@
// To ensure the initial non-final `data` will be visible
// (happens-before) when `handler` invokes `pushContinuously`
// the following assignment is done last:
- handler = new CooperativeHandler(this::pushContinuously);
+ pushScheduler = new SequentialScheduler(new PushContinuouslyTask());
}
private RawChannel.RawEvent createHandler() {
@@ -88,7 +89,7 @@
@Override
public void handle() {
state = AVAILABLE;
- handler.handle();
+ pushScheduler.runOrSchedule();
}
};
}
@@ -98,7 +99,7 @@
throw new IllegalArgumentException("Negative: " + n);
}
demand.accumulateAndGet(n, (p, i) -> p + i < 0 ? Long.MAX_VALUE : p + i);
- handler.handle();
+ pushScheduler.runOrSchedule();
}
void acknowledge() {
@@ -113,60 +114,64 @@
* regardless of the current demand and data availability.
*/
void close() {
- handler.stop();
+ pushScheduler.stop();
}
- private void pushContinuously() {
- while (!handler.isStopped()) {
- if (data.hasRemaining()) {
- if (demand.get() > 0) {
- try {
- int oldPos = data.position();
- reader.readFrame(data, frameConsumer);
- int newPos = data.position();
- assert oldPos != newPos : data; // reader always consumes bytes
- } catch (FailWebSocketException e) {
- handler.stop();
- messageConsumer.onError(e);
+ private class PushContinuouslyTask
+ extends SequentialScheduler.CompleteRestartableTask
+ {
+ @Override
+ public void run() {
+ while (!pushScheduler.isStopped()) {
+ if (data.hasRemaining()) {
+ if (demand.get() > 0) {
+ try {
+ int oldPos = data.position();
+ reader.readFrame(data, frameConsumer);
+ int newPos = data.position();
+ assert oldPos != newPos : data; // reader always consumes bytes
+ } catch (FailWebSocketException e) {
+ pushScheduler.stop();
+ messageConsumer.onError(e);
+ }
+ continue;
}
- continue;
+ break;
}
- break;
- }
- switch (state) {
- case WAITING:
- return;
- case UNREGISTERED:
- try {
- state = WAITING;
- channel.registerEvent(event);
- } catch (IOException e) {
- handler.stop();
- messageConsumer.onError(e);
- }
- return;
- case AVAILABLE:
- try {
- data = channel.read();
- } catch (IOException e) {
- handler.stop();
- messageConsumer.onError(e);
+ switch (state) {
+ case WAITING:
+ return;
+ case UNREGISTERED:
+ try {
+ state = WAITING;
+ channel.registerEvent(event);
+ } catch (IOException e) {
+ pushScheduler.stop();
+ messageConsumer.onError(e);
+ }
return;
- }
- if (data == null) { // EOF
- handler.stop();
- messageConsumer.onComplete();
- return;
- } else if (!data.hasRemaining()) { // No data at the moment
- // Pretty much a "goto", reusing the existing code path
- // for registration
- state = UNREGISTERED;
- }
- continue;
- default:
- throw new InternalError(String.valueOf(state));
+ case AVAILABLE:
+ try {
+ data = channel.read();
+ } catch (IOException e) {
+ pushScheduler.stop();
+ messageConsumer.onError(e);
+ return;
+ }
+ if (data == null) { // EOF
+ pushScheduler.stop();
+ messageConsumer.onComplete();
+ return;
+ } else if (!data.hasRemaining()) { // No data at the moment
+ // Pretty much a "goto", reusing the existing code path
+ // for registration
+ state = UNREGISTERED;
+ }
+ continue;
+ default:
+ throw new InternalError(String.valueOf(state));
+ }
}
}
}
}
-
--- a/src/jdk.incubator.httpclient/share/classes/jdk/incubator/http/internal/websocket/WebSocketImpl.java Sun Nov 05 17:05:57 2017 +0000
+++ b/src/jdk.incubator.httpclient/share/classes/jdk/incubator/http/internal/websocket/WebSocketImpl.java Sun Nov 05 17:32:13 2017 +0000
@@ -25,17 +25,6 @@
package jdk.incubator.http.internal.websocket;
-import jdk.incubator.http.WebSocket;
-import jdk.incubator.http.internal.common.Log;
-import jdk.incubator.http.internal.common.Pair;
-import jdk.incubator.http.internal.websocket.OpeningHandshake.Result;
-import jdk.incubator.http.internal.websocket.OutgoingMessage.Binary;
-import jdk.incubator.http.internal.websocket.OutgoingMessage.Close;
-import jdk.incubator.http.internal.websocket.OutgoingMessage.Context;
-import jdk.incubator.http.internal.websocket.OutgoingMessage.Ping;
-import jdk.incubator.http.internal.websocket.OutgoingMessage.Pong;
-import jdk.incubator.http.internal.websocket.OutgoingMessage.Text;
-
import java.io.IOException;
import java.net.ProtocolException;
import java.net.URI;
@@ -47,10 +36,23 @@
import java.util.concurrent.atomic.AtomicBoolean;
import java.util.function.Consumer;
import java.util.function.Function;
+import jdk.incubator.http.WebSocket;
+import jdk.incubator.http.internal.common.Log;
+import jdk.incubator.http.internal.common.Pair;
+import jdk.incubator.http.internal.common.SequentialScheduler;
+import jdk.incubator.http.internal.common.SequentialScheduler.DeferredCompleter;
+import jdk.incubator.http.internal.websocket.OpeningHandshake.Result;
+import jdk.incubator.http.internal.websocket.OutgoingMessage.Binary;
+import jdk.incubator.http.internal.websocket.OutgoingMessage.Close;
+import jdk.incubator.http.internal.websocket.OutgoingMessage.Context;
+import jdk.incubator.http.internal.websocket.OutgoingMessage.Ping;
+import jdk.incubator.http.internal.websocket.OutgoingMessage.Pong;
+import jdk.incubator.http.internal.websocket.OutgoingMessage.Text;
import static java.util.Objects.requireNonNull;
import static java.util.concurrent.CompletableFuture.failedFuture;
import static jdk.incubator.http.internal.common.Pair.pair;
+import jdk.incubator.http.internal.common.Utils;
import static jdk.incubator.http.internal.websocket.StatusCodes.CLOSED_ABNORMALLY;
import static jdk.incubator.http.internal.websocket.StatusCodes.NO_STATUS_CODE;
import static jdk.incubator.http.internal.websocket.StatusCodes.isLegalToSendFromClient;
@@ -72,8 +74,7 @@
*/
private boolean lastMethodInvoked;
private final AtomicBoolean outstandingSend = new AtomicBoolean();
- private final CooperativeHandler sendHandler =
- new CooperativeHandler(this::sendFirst);
+ private final SequentialScheduler sendScheduler;
private final Queue<Pair<OutgoingMessage, CompletableFuture<WebSocket>>>
queue = new ConcurrentLinkedQueue<>();
private final Context context = new OutgoingMessage.Context();
@@ -136,6 +137,7 @@
this.listener = requireNonNull(listener);
this.transmitter = new Transmitter(channel);
this.receiver = new Receiver(messageConsumerOf(listener), channel);
+ this.sendScheduler = new SequentialScheduler(new SendFirstTask());
// Set up the Closing Handshake action
CompletableFuture.allOf(closeReceived, closeSent)
@@ -325,36 +327,39 @@
// The queue is supposed to be unbounded
throw new InternalError();
}
- sendHandler.handle();
+ sendScheduler.runOrSchedule();
return cf;
}
/*
- * This is the main sending method. It may be run in different threads,
+ * This is the main sending task. It may be run in different threads,
* but never concurrently.
*/
- private void sendFirst(Runnable whenSent) {
- Pair<OutgoingMessage, CompletableFuture<WebSocket>> p = queue.poll();
- if (p == null) {
- whenSent.run();
- return;
- }
- OutgoingMessage message = p.first;
- CompletableFuture<WebSocket> cf = p.second;
- try {
- message.contextualize(context);
- Consumer<Exception> h = e -> {
- if (e == null) {
- cf.complete(WebSocketImpl.this);
- } else {
- cf.completeExceptionally(e);
- }
- sendHandler.handle();
- whenSent.run();
- };
- transmitter.send(message, h);
- } catch (Exception t) {
- cf.completeExceptionally(t);
+ private class SendFirstTask implements SequentialScheduler.RestartableTask {
+ @Override
+ public void run (DeferredCompleter taskCompleter){
+ Pair<OutgoingMessage, CompletableFuture<WebSocket>> p = queue.poll();
+ if (p == null) {
+ taskCompleter.complete();
+ return;
+ }
+ OutgoingMessage message = p.first;
+ CompletableFuture<WebSocket> cf = p.second;
+ try {
+ message.contextualize(context);
+ Consumer<Exception> h = e -> {
+ if (e == null) {
+ cf.complete(WebSocketImpl.this);
+ } else {
+ cf.completeExceptionally(e);
+ }
+ sendScheduler.runOrSchedule();
+ taskCompleter.complete();
+ };
+ transmitter.send(message, h);
+ } catch (Exception t) {
+ cf.completeExceptionally(t);
+ }
}
}
@@ -436,7 +441,8 @@
pongSent.whenComplete(
(r, error) -> {
if (error != null) {
- WebSocketImpl.this.signalError(error);
+ WebSocketImpl.this.signalError(
+ Utils.getCompletionCause(error));
}
}
);
@@ -482,7 +488,7 @@
enqueueClose(new Close(code, ""))
.whenComplete((r, e) -> {
if (e != null) {
- ex.addSuppressed(e);
+ ex.addSuppressed(Utils.getCompletionCause(e));
}
try {
channel.close();
--- a/src/jdk.incubator.httpclient/share/classes/module-info.java Sun Nov 05 17:05:57 2017 +0000
+++ b/src/jdk.incubator.httpclient/share/classes/module-info.java Sun Nov 05 17:32:13 2017 +0000
@@ -33,4 +33,3 @@
module jdk.incubator.httpclient {
exports jdk.incubator.http;
}
-
--- a/test/jdk/java/net/httpclient/APIErrors.java Sun Nov 05 17:05:57 2017 +0000
+++ /dev/null Thu Jan 01 00:00:00 1970 +0000
@@ -1,193 +0,0 @@
-/*
- * Copyright (c) 2015, 2016, Oracle and/or its affiliates. All rights reserved.
- * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER.
- *
- * This code is free software; you can redistribute it and/or modify it
- * under the terms of the GNU General Public License version 2 only, as
- * published by the Free Software Foundation.
- *
- * This code is distributed in the hope that it will be useful, but WITHOUT
- * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or
- * FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License
- * version 2 for more details (a copy is included in the LICENSE file that
- * accompanied this code).
- *
- * You should have received a copy of the GNU General Public License version
- * 2 along with this work; if not, write to the Free Software Foundation,
- * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA.
- *
- * Please contact Oracle, 500 Oracle Parkway, Redwood Shores, CA 94065 USA
- * or visit www.oracle.com if you need additional information or have any
- * questions.
- */
-
-/*
- * @test
- * @bug 8087112
- * @modules jdk.incubator.httpclient
- * java.logging
- * jdk.httpserver
- * @library /lib/testlibrary/
- * @build jdk.testlibrary.SimpleSSLContext ProxyServer
- * @build TestKit
- * @compile ../../../com/sun/net/httpserver/LogFilter.java
- * @compile ../../../com/sun/net/httpserver/FileServerHandler.java
- * @run main/othervm APIErrors
- * @summary Basic checks for appropriate errors/exceptions thrown from the API
- */
-
-import com.sun.net.httpserver.HttpContext;
-import com.sun.net.httpserver.HttpHandler;
-import com.sun.net.httpserver.HttpServer;
-import com.sun.net.httpserver.HttpsServer;
-import java.io.IOException;
-import java.net.InetSocketAddress;
-import java.net.PasswordAuthentication;
-import java.net.ProxySelector;
-import java.net.URI;
-import jdk.incubator.http.HttpClient;
-import jdk.incubator.http.HttpRequest;
-import jdk.incubator.http.HttpResponse;
-import java.util.LinkedList;
-import java.util.List;
-import java.util.concurrent.ExecutionException;
-import java.util.concurrent.Executors;
-import java.util.concurrent.ExecutorService;
-import java.util.function.Supplier;
-import static jdk.incubator.http.HttpResponse.BodyHandler.discard;
-
-public class APIErrors {
-
- static ExecutorService serverExecutor = Executors.newCachedThreadPool();
- static String httproot, fileuri;
- static List<HttpClient> clients = new LinkedList<>();
-
- public static void main(String[] args) throws Exception {
- HttpServer server = createServer();
-
- int port = server.getAddress().getPort();
- System.out.println("HTTP server port = " + port);
-
- httproot = "http://127.0.0.1:" + port + "/files/";
- fileuri = httproot + "foo.txt";
-
- HttpClient client = HttpClient.newHttpClient();
-
- try {
- test1();
- test2();
- //test3();
- } finally {
- server.stop(0);
- serverExecutor.shutdownNow();
- for (HttpClient c : clients)
- ((ExecutorService)c.executor()).shutdownNow();
- }
- }
-
- static void checkNonNull(Supplier<?> r) {
- if (r.get() == null)
- throw new RuntimeException("Unexpected null return:");
- }
-
- static void assertTrue(Supplier<Boolean> r) {
- if (r.get() == false)
- throw new RuntimeException("Assertion failure:");
- }
-
- // HttpClient.Builder
- static void test1() throws Exception {
- System.out.println("Test 1");
- HttpClient.Builder cb = HttpClient.newBuilder();
- TestKit.assertThrows(IllegalArgumentException.class, () -> cb.priority(-1));
- TestKit.assertThrows(IllegalArgumentException.class, () -> cb.priority(0));
- TestKit.assertThrows(IllegalArgumentException.class, () -> cb.priority(257));
- TestKit.assertThrows(IllegalArgumentException.class, () -> cb.priority(500));
- TestKit.assertNotThrows(() -> cb.priority(1));
- TestKit.assertNotThrows(() -> cb.priority(256));
- TestKit.assertNotThrows(() -> {
- clients.add(cb.build());
- clients.add(cb.build());
- });
- }
-
- static void test2() throws Exception {
- System.out.println("Test 2");
- HttpClient.Builder cb = HttpClient.newBuilder();
- InetSocketAddress addr = new InetSocketAddress("127.0.0.1", 5000);
- cb.proxy(ProxySelector.of(addr));
- HttpClient c = cb.build();
- clients.add(c);
- checkNonNull(()-> c.executor());
- assertTrue(()-> c.followRedirects() == HttpClient.Redirect.NEVER);
- assertTrue(()-> !c.authenticator().isPresent());
- }
-
- static URI accessibleURI() {
- return URI.create(fileuri);
- }
-
- static HttpRequest request() {
- return HttpRequest.newBuilder(accessibleURI()).GET().build();
- }
-
-// static void test3() throws Exception {
-// System.out.println("Test 3");
-// TestKit.assertThrows(IllegalStateException.class, ()-> {
-// try {
-// HttpRequest r1 = request();
-// HttpResponse<Object> resp = r1.response(discard(null));
-// HttpResponse<Object> resp1 = r1.response(discard(null));
-// } catch (IOException |InterruptedException e) {
-// throw new RuntimeException(e);
-// }
-// });
-//
-// TestKit.assertThrows(IllegalStateException.class, ()-> {
-// try {
-// HttpRequest r1 = request();
-// HttpResponse<Object> resp = r1.response(discard(null));
-// HttpResponse<Object> resp1 = r1.responseAsync(discard(null)).get();
-// } catch (IOException |InterruptedException | ExecutionException e) {
-// throw new RuntimeException(e);
-// }
-// });
-// TestKit.assertThrows(IllegalStateException.class, ()-> {
-// try {
-// HttpRequest r1 = request();
-// HttpResponse<Object> resp1 = r1.responseAsync(discard(null)).get();
-// HttpResponse<Object> resp = r1.response(discard(null));
-// } catch (IOException |InterruptedException | ExecutionException e) {
-// throw new RuntimeException(e);
-// }
-// });
-// }
-
- static class Auth extends java.net.Authenticator {
- int count = 0;
- @Override
- protected PasswordAuthentication getPasswordAuthentication() {
- if (count++ == 0) {
- return new PasswordAuthentication("user", "passwd".toCharArray());
- } else {
- return new PasswordAuthentication("user", "goober".toCharArray());
- }
- }
- int count() {
- return count;
- }
- }
-
- static HttpServer createServer() throws Exception {
- HttpServer s = HttpServer.create(new InetSocketAddress(0), 0);
- if (s instanceof HttpsServer)
- throw new RuntimeException ("should not be httpsserver");
-
- String root = System.getProperty("test.src") + "/docs";
- s.createContext("/files", new FileServerHandler(root));
- s.setExecutor(serverExecutor);
- s.start();
-
- return s;
- }
-}
--- a/test/jdk/java/net/httpclient/BasicAuthTest.java Sun Nov 05 17:05:57 2017 +0000
+++ b/test/jdk/java/net/httpclient/BasicAuthTest.java Sun Nov 05 17:32:13 2017 +0000
@@ -45,7 +45,7 @@
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
import static java.nio.charset.StandardCharsets.US_ASCII;
-import static jdk.incubator.http.HttpRequest.BodyProcessor.fromString;
+import static jdk.incubator.http.HttpRequest.BodyPublisher.fromString;
import static jdk.incubator.http.HttpResponse.BodyHandler.asString;
public class BasicAuthTest {
--- /dev/null Thu Jan 01 00:00:00 1970 +0000
+++ b/test/jdk/java/net/httpclient/BodyProcessorInputStreamTest.java Sun Nov 05 17:32:13 2017 +0000
@@ -0,0 +1,161 @@
+/*
+ * Copyright (c) 2017, Oracle and/or its affiliates. All rights reserved.
+ * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER.
+ *
+ * This code is free software; you can redistribute it and/or modify it
+ * under the terms of the GNU General Public License version 2 only, as
+ * published by the Free Software Foundation.
+ *
+ * This code is distributed in the hope that it will be useful, but WITHOUT
+ * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or
+ * FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License
+ * version 2 for more details (a copy is included in the LICENSE file that
+ * accompanied this code).
+ *
+ * You should have received a copy of the GNU General Public License version
+ * 2 along with this work; if not, write to the Free Software Foundation,
+ * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA.
+ *
+ * Please contact Oracle, 500 Oracle Parkway, Redwood Shores, CA 94065 USA
+ * or visit www.oracle.com if you need additional information or have any
+ * questions.
+ */
+
+import java.io.IOException;
+import java.io.InputStream;
+import java.io.InputStreamReader;
+import java.io.Reader;
+import java.net.URI;
+import jdk.incubator.http.HttpClient;
+import jdk.incubator.http.HttpHeaders;
+import jdk.incubator.http.HttpRequest;
+import jdk.incubator.http.HttpResponse;
+import java.nio.ByteBuffer;
+import java.nio.charset.Charset;
+import java.util.ArrayList;
+import java.util.Iterator;
+import java.util.List;
+import java.util.Locale;
+import java.util.Optional;
+import java.util.concurrent.ArrayBlockingQueue;
+import java.util.concurrent.BlockingQueue;
+import java.util.concurrent.CompletableFuture;
+import java.util.concurrent.CompletionStage;
+import java.util.concurrent.Flow;
+import java.util.stream.Stream;
+import static java.lang.System.err;
+
+/*
+ * @test
+ * @bug 8187503
+ * @summary An example on how to read a response body with InputStream...
+ * @run main/othervm -Dtest.debug=true BodyProcessorInputStreamTest
+ * @author daniel fuchs
+ */
+public class BodyProcessorInputStreamTest {
+
+ public static boolean DEBUG = Boolean.getBoolean("test.debug");
+
+ /**
+ * Examine the response headers to figure out the charset used to
+ * encode the body content.
+ * If the content type is not textual, returns an empty Optional.
+ * Otherwise, returns the body content's charset, defaulting to
+ * ISO-8859-1 if none is explicitly specified.
+ * @param headers The response headers.
+ * @return The charset to use for decoding the response body, if
+ * the response body content is text/...
+ */
+ public static Optional<Charset> getCharset(HttpHeaders headers) {
+ Optional<String> contentType = headers.firstValue("Content-Type");
+ Optional<Charset> charset = Optional.empty();
+ if (contentType.isPresent()) {
+ final String[] values = contentType.get().split(";");
+ if (values[0].startsWith("text/")) {
+ charset = Optional.of(Stream.of(values)
+ .map(x -> x.toLowerCase(Locale.ROOT))
+ .map(String::trim)
+ .filter(x -> x.startsWith("charset="))
+ .map(x -> x.substring("charset=".length()))
+ .findFirst()
+ .orElse("ISO-8859-1"))
+ .map(Charset::forName);
+ }
+ }
+ return charset;
+ }
+
+ public static void main(String[] args) throws Exception {
+ HttpClient client = HttpClient.newHttpClient();
+ HttpRequest request = HttpRequest
+ .newBuilder(new URI("http://hg.openjdk.java.net/jdk9/sandbox/jdk/shortlog/http-client-branch/"))
+ .GET()
+ .build();
+
+ // This example shows how to return an InputStream that can be used to
+ // start reading the response body before the response is fully received.
+ // In comparison, the snipet below (which uses
+ // HttpResponse.BodyHandler.asString()) obviously will not return before the
+ // response body is fully read:
+ //
+ // System.out.println(
+ // client.sendAsync(request, HttpResponse.BodyHandler.asString()).get().body());
+
+ CompletableFuture<HttpResponse<InputStream>> handle =
+ client.sendAsync(request, HttpResponse.BodyHandler.asInputStream());
+ if (DEBUG) err.println("Request sent");
+
+ HttpResponse<InputStream> pending = handle.get();
+
+ // At this point, the response headers have been received, but the
+ // response body may not have arrived yet. This comes from
+ // the implementation of HttpResponseInputStream::getBody above,
+ // which returns an already completed completion stage, without
+ // waiting for any data.
+ // We can therefore access the headers - and the body, which
+ // is our live InputStream, without waiting...
+ HttpHeaders responseHeaders = pending.headers();
+
+ // Get the charset declared in the response headers.
+ // The optional will be empty if the content type is not
+ // of type text/...
+ Optional<Charset> charset = getCharset(responseHeaders);
+
+ try (InputStream is = pending.body();
+ // We assume a textual content type. Construct an InputStream
+ // Reader with the appropriate Charset.
+ // charset.get() will throw NPE if the content is not textual.
+ Reader r = new InputStreamReader(is, charset.get())) {
+
+ char[] buff = new char[32];
+ int off=0, n=0;
+ if (DEBUG) err.println("Start receiving response body");
+ if (DEBUG) err.println("Charset: " + charset.get());
+
+ // Start consuming the InputStream as the data arrives.
+ // Will block until there is something to read...
+ while ((n = r.read(buff, off, buff.length - off)) > 0) {
+ assert (buff.length - off) > 0;
+ assert n <= (buff.length - off);
+ if (n == (buff.length - off)) {
+ System.out.print(buff);
+ off = 0;
+ } else {
+ off += n;
+ }
+ assert off < buff.length;
+ }
+
+ // last call to read may not have filled 'buff' completely.
+ // flush out the remaining characters.
+ assert off >= 0 && off < buff.length;
+ for (int i=0; i < off; i++) {
+ System.out.print(buff[i]);
+ }
+
+ // We're done!
+ System.out.println("Done!");
+ }
+ }
+
+}
--- /dev/null Thu Jan 01 00:00:00 1970 +0000
+++ b/test/jdk/java/net/httpclient/BufferingSubscriberTest.java Sun Nov 05 17:32:13 2017 +0000
@@ -0,0 +1,356 @@
+/*
+ * Copyright (c) 2017, Oracle and/or its affiliates. All rights reserved.
+ * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER.
+ *
+ * This code is free software; you can redistribute it and/or modify it
+ * under the terms of the GNU General Public License version 2 only, as
+ * published by the Free Software Foundation.
+ *
+ * This code is distributed in the hope that it will be useful, but WITHOUT
+ * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or
+ * FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License
+ * version 2 for more details (a copy is included in the LICENSE file that
+ * accompanied this code).
+ *
+ * You should have received a copy of the GNU General Public License version
+ * 2 along with this work; if not, write to the Free Software Foundation,
+ * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA.
+ *
+ * Please contact Oracle, 500 Oracle Parkway, Redwood Shores, CA 94065 USA
+ * or visit www.oracle.com if you need additional information or have any
+ * questions.
+ */
+
+import java.nio.ByteBuffer;
+import java.util.List;
+import java.util.Random;
+import java.util.concurrent.CompletableFuture;
+import java.util.concurrent.CompletionStage;
+import java.util.concurrent.Executor;
+import java.util.concurrent.ExecutorService;
+import java.util.concurrent.Executors;
+import java.util.concurrent.Flow;
+import java.util.concurrent.Flow.Subscription;
+import java.util.concurrent.SubmissionPublisher;
+import jdk.incubator.http.HttpResponse.BodyHandler;
+import jdk.incubator.http.HttpResponse.BodySubscriber;
+import jdk.test.lib.RandomFactory;
+import org.testng.annotations.DataProvider;
+import org.testng.annotations.Test;
+import static java.lang.Long.MAX_VALUE;
+import static java.lang.System.out;
+import static java.util.concurrent.CompletableFuture.delayedExecutor;
+import static java.util.concurrent.TimeUnit.MILLISECONDS;
+import static org.testng.Assert.*;
+
+/*
+ * @test
+ * @bug 8184285
+ * @summary Direct test for HttpResponse.BodySubscriber.buffering() API
+ * @key randomness
+ * @library /test/lib
+ * @build jdk.test.lib.RandomFactory
+ * @run testng/othervm -Djdk.internal.httpclient.debug=true BufferingSubscriberTest
+ */
+
+public class BufferingSubscriberTest {
+
+ static final Random random = RandomFactory.getRandom();
+
+ @DataProvider(name = "negatives")
+ public Object[][] negatives() {
+ return new Object[][] { { 0 }, { -1 }, { -1000 } };
+ }
+
+ @Test(dataProvider = "negatives", expectedExceptions = IllegalArgumentException.class)
+ public void subscriberThrowsIAE(int bufferSize) {
+ BodySubscriber<?> bp = BodySubscriber.asByteArray();
+ BodySubscriber.buffering(bp, bufferSize);
+ }
+
+ @Test(dataProvider = "negatives", expectedExceptions = IllegalArgumentException.class)
+ public void handlerThrowsIAE(int bufferSize) {
+ BodyHandler<?> bp = BodyHandler.asByteArray();
+ BodyHandler.buffering(bp, bufferSize);
+ }
+
+ // ---
+
+ @DataProvider(name = "config")
+ public Object[][] config() {
+ return new Object[][] {
+ // iterations delayMillis numBuffers bufferSize maxBufferSize minBufferSize
+ { 1, 0, 1, 1, 2, 1 },
+ { 1, 0, 1, 10, 1000, 1 },
+ { 1, 10, 1, 10, 1000, 1 },
+ { 1, 0, 1, 1000, 1000, 1 },
+ { 1, 0, 10, 1000, 1000, 1 },
+ { 1, 0, 1000, 10 , 1000, 50 },
+ { 1, 100, 1, 1000 * 4, 1000, 1 },
+ { 100, 0, 1000, 1, 2, 1 },
+ { 3, 0, 4, 5006, 1000, 1 },
+ { 20, 0, 100, 4888, 1000, 100 },
+ { 16, 10, 1000, 50 , 1000, 100 },
+ };
+ }
+
+ @Test(dataProvider = "config")
+ public void test(int iterations,
+ int delayMillis,
+ int numBuffers,
+ int bufferSize,
+ int maxBufferSize,
+ int minbufferSize) {
+ for (long perRequestAmount : new long[] { 1L, MAX_VALUE })
+ test(iterations,
+ delayMillis,
+ numBuffers,
+ bufferSize,
+ maxBufferSize,
+ minbufferSize,
+ perRequestAmount);
+ }
+
+ public void test(int iterations,
+ int delayMillis,
+ int numBuffers,
+ int bufferSize,
+ int maxBufferSize,
+ int minBufferSize,
+ long requestAmount) {
+ ExecutorService executor = Executors.newFixedThreadPool(1);
+ try {
+
+ out.printf("Iterations %d\n", iterations);
+ for (int i=0; i<iterations; i++ ) {
+ out.printf("Iteration: %d\n", i);
+ SubmissionPublisher<List<ByteBuffer>> publisher =
+ new SubmissionPublisher<>(executor, 1);
+ CompletableFuture<?> cf = sink(publisher,
+ delayMillis,
+ numBuffers * bufferSize,
+ requestAmount,
+ maxBufferSize,
+ minBufferSize);
+ source(publisher, numBuffers, bufferSize);
+ publisher.close();
+ cf.join();
+ }
+ out.println("OK");
+ } finally {
+ executor.shutdown();
+ }
+ }
+
+ static int accumulatedDataSize(List<ByteBuffer> bufs) {
+ return bufs.stream().mapToInt(ByteBuffer::remaining).sum();
+ }
+
+ /** Returns a new BB with its contents set to monotonically increasing
+ * values, staring at the given start index and wrapping every 100. */
+ static ByteBuffer allocateBuffer(int size, int startIdx) {
+ ByteBuffer b = ByteBuffer.allocate(size);
+ for (int i=0; i<size; i++)
+ b.put((byte)((startIdx + i) % 100));
+ b.position(0);
+ return b;
+ }
+
+ static class TestSubscriber implements BodySubscriber<Integer> {
+ final int delayMillis;
+ final int bufferSize;
+ final int expectedTotalSize;
+ final long requestAmount;
+ final CompletableFuture<Integer> completion;
+ final Executor delayedExecutor;
+ volatile Flow.Subscription subscription;
+
+ TestSubscriber(int bufferSize,
+ int delayMillis,
+ int expectedTotalSize,
+ long requestAmount) {
+ this.bufferSize = bufferSize;
+ this.completion = new CompletableFuture<>();
+ this.delayMillis = delayMillis;
+ this.delayedExecutor = delayedExecutor(delayMillis, MILLISECONDS);
+ this.expectedTotalSize = expectedTotalSize;
+ this.requestAmount = requestAmount;
+ }
+
+ /**
+ * Example of a factory method which would decorate a buffering
+ * subscriber to create a new subscriber dependent on buffering capability.
+ *
+ * The integer type parameter simulates the body just by counting the
+ * number of bytes in the body.
+ */
+ static BodySubscriber<Integer> createSubscriber(int bufferSize,
+ int delay,
+ int expectedTotalSize,
+ long requestAmount) {
+ TestSubscriber s = new TestSubscriber(bufferSize,
+ delay,
+ expectedTotalSize,
+ requestAmount);
+ return BodySubscriber.buffering(s, bufferSize);
+ }
+
+ private void requestMore() { subscription.request(requestAmount); }
+
+ @Override
+ public void onSubscribe(Subscription subscription) {
+ assertNull(this.subscription);
+ this.subscription = subscription;
+ if (delayMillis > 0)
+ delayedExecutor.execute(this::requestMore);
+ else
+ requestMore();
+ }
+
+ volatile int wrongSizes;
+ volatile int totalBytesReceived;
+ volatile int onNextInvocations;
+ volatile int lastSeenSize = -1;
+ volatile boolean noMoreOnNext; // false
+ volatile int index; // 0
+
+ @Override
+ public void onNext(List<ByteBuffer> items) {
+ long sz = accumulatedDataSize(items);
+ onNextInvocations++;
+ assertNotEquals(sz, 0L, "Unexpected empty buffers");
+ items.stream().forEach(b -> assertEquals(b.position(), 0));
+ assertFalse(noMoreOnNext);
+
+ if (sz != bufferSize) {
+ String msg = sz + ", should be less than bufferSize, " + bufferSize;
+ assertTrue(sz < bufferSize, msg);
+ assertTrue(lastSeenSize == -1 || lastSeenSize == bufferSize);
+ noMoreOnNext = true;
+ wrongSizes++;
+ } else {
+ assertEquals(sz, bufferSize, "Expected to receive exactly bufferSize");
+ }
+
+ // Ensure expected contents
+ for (ByteBuffer b : items) {
+ while (b.hasRemaining()) {
+ assertEquals(b.get(), (byte) (index % 100));
+ index++;
+ }
+ }
+
+ totalBytesReceived += sz;
+ assertEquals(totalBytesReceived, index );
+ if (delayMillis > 0)
+ delayedExecutor.execute(this::requestMore);
+ else
+ requestMore();
+ }
+
+ @Override
+ public void onError(Throwable throwable) {
+ completion.completeExceptionally(throwable);
+ }
+
+ @Override
+ public void onComplete() {
+ if (wrongSizes > 1) { // allow just the final item to be smaller
+ String msg = "Wrong sizes. Expected no more than 1. [" + this + "]";
+ completion.completeExceptionally(new Throwable(msg));
+ }
+ if (totalBytesReceived != expectedTotalSize) {
+ String msg = "Wrong number of bytes. [" + this + "]";
+ completion.completeExceptionally(new Throwable(msg));
+ } else {
+ completion.complete(totalBytesReceived);
+ }
+ }
+
+ @Override
+ public CompletionStage<Integer> getBody() { return completion; }
+
+ @Override
+ public String toString() {
+ StringBuilder sb = new StringBuilder();
+ sb.append(super.toString());
+ sb.append(", bufferSize=").append(bufferSize);
+ sb.append(", onNextInvocations=").append(onNextInvocations);
+ sb.append(", totalBytesReceived=").append(totalBytesReceived);
+ sb.append(", expectedTotalSize=").append(expectedTotalSize);
+ sb.append(", requestAmount=").append(requestAmount);
+ sb.append(", lastSeenSize=").append(lastSeenSize);
+ sb.append(", wrongSizes=").append(wrongSizes);
+ sb.append(", index=").append(index);
+ return sb.toString();
+ }
+ }
+
+ /**
+ * Publishes data, through the given publisher, using the main thread.
+ *
+ * Note: The executor supplied when creating the SubmissionPublisher provides
+ * the threads for executing the Subscribers.
+ *
+ * @param publisher the publisher
+ * @param numBuffers the number of buffers to send ( before splitting in two )
+ * @param bufferSize the total size of the data to send ( before splitting in two )
+ */
+ static void source(SubmissionPublisher<List<ByteBuffer>> publisher,
+ int numBuffers,
+ int bufferSize) {
+ out.printf("Publishing %d buffers of size %d each\n", numBuffers, bufferSize);
+ int index = 0;
+ for (int i=0; i<numBuffers; i++) {
+ int chunkSize = random.nextInt(bufferSize);
+ ByteBuffer buf1 = allocateBuffer(chunkSize, index);
+ index += chunkSize;
+ ByteBuffer buf2 = allocateBuffer(bufferSize - chunkSize, index);
+ index += bufferSize - chunkSize;
+ publisher.submit(List.of(buf1, buf2));
+ }
+ out.println("source complete");
+ }
+
+ /**
+ * Creates and subscribes Subscribers that receive data from the given
+ * publisher.
+ *
+ * @param publisher the publisher
+ * @param delayMillis time, in milliseconds, to delay the Subscription
+ * requesting more bytes ( for simulating slow consumption )
+ * @param expectedTotalSize the total number of bytes expected to be received
+ * by the subscribers
+ * @return a CompletableFuture which completes when the subscription is complete
+ */
+ static CompletableFuture<?> sink(SubmissionPublisher<List<ByteBuffer>> publisher,
+ int delayMillis,
+ int expectedTotalSize,
+ long requestAmount,
+ int maxBufferSize,
+ int minBufferSize) {
+ int bufferSize = random.nextInt(maxBufferSize - minBufferSize) + minBufferSize;
+ BodySubscriber<Integer> sub = TestSubscriber.createSubscriber(bufferSize,
+ delayMillis,
+ expectedTotalSize,
+ requestAmount);
+ publisher.subscribe(sub);
+ out.printf("Subscriber reads data with buffer size: %d\n", bufferSize);
+ out.printf("Subscription delay is %d msec\n", delayMillis);
+ out.printf("Request amount is %d items\n", requestAmount);
+ return sub.getBody().toCompletableFuture();
+ }
+
+ // ---
+
+ // TODO: Add a test for cancel
+
+ // ---
+
+ /* Main entry point for standalone testing of the main functional test. */
+ public static void main(String... args) {
+ BufferingSubscriberTest t = new BufferingSubscriberTest();
+ for (Object[] objs : t.config())
+ t.test((int)objs[0], (int)objs[1], (int)objs[2], (int)objs[3], (int)objs[4], (int)objs[5]);
+ }
+}
--- /dev/null Thu Jan 01 00:00:00 1970 +0000
+++ b/test/jdk/java/net/httpclient/CustomRequestPublisher.java Sun Nov 05 17:32:13 2017 +0000
@@ -0,0 +1,349 @@
+/*
+ * Copyright (c) 2017, Oracle and/or its affiliates. All rights reserved.
+ * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER.
+ *
+ * This code is free software; you can redistribute it and/or modify it
+ * under the terms of the GNU General Public License version 2 only, as
+ * published by the Free Software Foundation.
+ *
+ * This code is distributed in the hope that it will be useful, but WITHOUT
+ * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or
+ * FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License
+ * version 2 for more details (a copy is included in the LICENSE file that
+ * accompanied this code).
+ *
+ * You should have received a copy of the GNU General Public License version
+ * 2 along with this work; if not, write to the Free Software Foundation,
+ * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA.
+ *
+ * Please contact Oracle, 500 Oracle Parkway, Redwood Shores, CA 94065 USA
+ * or visit www.oracle.com if you need additional information or have any
+ * questions.
+ */
+
+/*
+ * @test
+ * @summary Checks correct handling of Publishers that call onComplete without demand
+ * @modules java.base/sun.net.www.http
+ * jdk.incubator.httpclient/jdk.incubator.http.internal.common
+ * jdk.incubator.httpclient/jdk.incubator.http.internal.frame
+ * jdk.incubator.httpclient/jdk.incubator.http.internal.hpack
+ * java.logging
+ * jdk.httpserver
+ * @library /lib/testlibrary http2/server
+ * @build Http2TestServer
+ * @build jdk.testlibrary.SimpleSSLContext
+ * @run testng/othervm CustomRequestPublisher
+ */
+
+import com.sun.net.httpserver.HttpExchange;
+import com.sun.net.httpserver.HttpHandler;
+import com.sun.net.httpserver.HttpServer;
+import com.sun.net.httpserver.HttpsConfigurator;
+import com.sun.net.httpserver.HttpsServer;
+import java.io.IOException;
+import java.io.InputStream;
+import java.io.OutputStream;
+import java.net.InetSocketAddress;
+import java.net.URI;
+import java.nio.ByteBuffer;
+import java.util.Arrays;
+import java.util.concurrent.CompletableFuture;
+import java.util.concurrent.Flow;
+import java.util.concurrent.atomic.AtomicBoolean;
+import java.util.concurrent.atomic.AtomicInteger;
+import java.util.concurrent.atomic.AtomicLong;
+import java.util.function.Supplier;
+import java.util.stream.Collectors;
+import javax.net.ssl.SSLContext;
+import jdk.incubator.http.HttpClient;
+import jdk.incubator.http.HttpRequest;
+import jdk.incubator.http.HttpResponse;
+import jdk.testlibrary.SimpleSSLContext;
+import org.testng.annotations.AfterTest;
+import org.testng.annotations.BeforeTest;
+import org.testng.annotations.DataProvider;
+import org.testng.annotations.Test;
+import static java.lang.System.out;
+import static java.nio.charset.StandardCharsets.US_ASCII;
+import static jdk.incubator.http.HttpResponse.BodyHandler.asString;
+import static org.testng.Assert.assertEquals;
+import static org.testng.Assert.assertTrue;
+
+public class CustomRequestPublisher {
+
+ SSLContext sslContext;
+ HttpServer httpTestServer; // HTTP/1.1 [ 4 servers ]
+ HttpsServer httpsTestServer; // HTTPS/1.1
+ Http2TestServer http2TestServer; // HTTP/2 ( h2c )
+ Http2TestServer https2TestServer; // HTTP/2 ( h2 )
+ String httpURI;
+ String httpsURI;
+ String http2URI;
+ String https2URI;
+
+ @DataProvider(name = "variants")
+ public Object[][] variants() {
+ Supplier<BodyPublisher> fixedSupplier = () -> new FixedLengthBodyPublisher();
+ Supplier<BodyPublisher> unknownSupplier = () -> new UnknownLengthBodyPublisher();
+
+ return new Object[][]{
+ { httpURI, fixedSupplier, false },
+ { httpURI, unknownSupplier, false },
+ { httpsURI, fixedSupplier, false },
+ { httpsURI, unknownSupplier, false },
+ { http2URI, fixedSupplier, false },
+ { http2URI, unknownSupplier, false },
+ { https2URI, fixedSupplier, false,},
+ { https2URI, unknownSupplier, false },
+
+ { httpURI, fixedSupplier, true },
+ { httpURI, unknownSupplier, true },
+ { httpsURI, fixedSupplier, true },
+ { httpsURI, unknownSupplier, true },
+ { http2URI, fixedSupplier, true },
+ { http2URI, unknownSupplier, true },
+ { https2URI, fixedSupplier, true,},
+ { https2URI, unknownSupplier, true },
+ };
+ }
+
+ static final int ITERATION_COUNT = 10;
+
+ @Test(dataProvider = "variants")
+ void test(String uri, Supplier<BodyPublisher> bpSupplier, boolean sameClient)
+ throws Exception
+ {
+ HttpClient client = null;
+ for (int i=0; i< ITERATION_COUNT; i++) {
+ if (!sameClient || client == null)
+ client = HttpClient.newBuilder().sslContext(sslContext).build();
+
+ BodyPublisher bodyPublisher = bpSupplier.get();
+ HttpRequest request = HttpRequest.newBuilder(URI.create(uri))
+ .POST(bodyPublisher)
+ .build();
+
+ HttpResponse<String> resp = client.send(request, asString());
+
+ out.println("Got response: " + resp);
+ out.println("Got body: " + resp.body());
+ assertTrue(resp.statusCode() == 200,
+ "Expected 200, got:" + resp.statusCode());
+ assertEquals(resp.body(), bodyPublisher.bodyAsString());
+ }
+ }
+
+ @Test(dataProvider = "variants")
+ void testAsync(String uri, Supplier<BodyPublisher> bpSupplier, boolean sameClient)
+ throws Exception
+ {
+ HttpClient client = null;
+ for (int i=0; i< ITERATION_COUNT; i++) {
+ if (!sameClient || client == null)
+ client = HttpClient.newBuilder().sslContext(sslContext).build();
+
+ BodyPublisher bodyPublisher = bpSupplier.get();
+ HttpRequest request = HttpRequest.newBuilder(URI.create(uri))
+ .POST(bodyPublisher)
+ .build();
+
+ CompletableFuture<HttpResponse<String>> cf = client.sendAsync(request, asString());
+ HttpResponse<String> resp = cf.get();
+
+ out.println("Got response: " + resp);
+ out.println("Got body: " + resp.body());
+ assertTrue(resp.statusCode() == 200,
+ "Expected 200, got:" + resp.statusCode());
+ assertEquals(resp.body(), bodyPublisher.bodyAsString());
+ }
+ }
+
+ /** A Publisher that returns an UNKNOWN content length. */
+ static class UnknownLengthBodyPublisher extends BodyPublisher {
+ @Override
+ public long contentLength() {
+ return -1; // unknown
+ }
+ }
+
+ /** A Publisher that returns a FIXED content length. */
+ static class FixedLengthBodyPublisher extends BodyPublisher {
+ final int LENGTH = Arrays.stream(BODY)
+ .mapToInt(s-> s.getBytes(US_ASCII).length)
+ .sum();
+ @Override
+ public long contentLength() {
+ return LENGTH;
+ }
+ }
+
+ /**
+ * A Publisher that ( quite correctly ) invokes onComplete, after the last
+ * item has been published, even without any outstanding demand.
+ */
+ static abstract class BodyPublisher implements HttpRequest.BodyPublisher {
+
+ String[] BODY = new String[]
+ { "Say ", "Hello ", "To ", "My ", "Little ", "Friend" };
+
+ protected volatile Flow.Subscriber subscriber;
+
+ @Override
+ public void subscribe(Flow.Subscriber<? super ByteBuffer> subscriber) {
+ this.subscriber = subscriber;
+ subscriber.onSubscribe(new InternalSubscription());
+ }
+
+ @Override
+ public abstract long contentLength();
+
+ String bodyAsString() {
+ return Arrays.stream(BODY).collect(Collectors.joining());
+ }
+
+ class InternalSubscription implements Flow.Subscription {
+
+ private final AtomicLong demand = new AtomicLong();
+ private final AtomicBoolean cancelled = new AtomicBoolean();
+ private volatile int position;
+
+ private static final int IDLE = 1;
+ private static final int PUSHING = 2;
+ private static final int AGAIN = 4;
+ private final AtomicInteger state = new AtomicInteger(IDLE);
+
+ @Override
+ public void request(long n) {
+ if (n <= 0L) {
+ subscriber.onError(new IllegalArgumentException(
+ "non-positive subscription request"));
+ return;
+ }
+ if (cancelled.get()) {
+ return;
+ }
+
+ while (true) {
+ long prev = demand.get(), d;
+ if ((d = prev + n) < prev) // saturate
+ d = Long.MAX_VALUE;
+ if (demand.compareAndSet(prev, d))
+ break;
+ }
+
+ while (true) {
+ int s = state.get();
+ if (s == IDLE) {
+ if (state.compareAndSet(IDLE, PUSHING)) {
+ while (true) {
+ push();
+ if (state.compareAndSet(PUSHING, IDLE))
+ return;
+ else if (state.compareAndSet(AGAIN, PUSHING))
+ continue;
+ }
+ }
+ } else if (s == PUSHING) {
+ if (state.compareAndSet(PUSHING, AGAIN))
+ return;
+ } else if (s == AGAIN){
+ // do nothing, the pusher will already rerun
+ return;
+ } else {
+ throw new AssertionError("Unknown state:" + s);
+ }
+ }
+ }
+
+ private void push() {
+ long prev;
+ while ((prev = demand.get()) > 0) {
+ if (!demand.compareAndSet(prev, prev -1))
+ continue;
+
+ int index = position;
+ if (index < BODY.length) {
+ position++;
+ subscriber.onNext(ByteBuffer.wrap(BODY[index].getBytes(US_ASCII)));
+ }
+ }
+
+ if (position == BODY.length && !cancelled.get()) {
+ cancelled.set(true);
+ subscriber.onComplete(); // NOTE: onComplete without demand
+ }
+ }
+
+ @Override
+ public void cancel() {
+ if (cancelled.compareAndExchange(false, true))
+ return; // already cancelled
+ }
+ }
+ }
+
+ @BeforeTest
+ public void setup() throws Exception {
+ sslContext = new SimpleSSLContext().get();
+ if (sslContext == null)
+ throw new AssertionError("Unexpected null sslContext");
+
+ InetSocketAddress sa = new InetSocketAddress("localhost", 0);
+ httpTestServer = HttpServer.create(sa, 0);
+ httpTestServer.createContext("/http1/echo", new Http1EchoHandler());
+ httpURI = "http://127.0.0.1:" + httpTestServer.getAddress().getPort() + "/http1/echo";
+
+ httpsTestServer = HttpsServer.create(sa, 0);
+ httpsTestServer.setHttpsConfigurator(new HttpsConfigurator(sslContext));
+ httpsTestServer.createContext("/https1/echo", new Http1EchoHandler());
+ httpsURI = "https://127.0.0.1:" + httpsTestServer.getAddress().getPort() + "/https1/echo";
+
+ http2TestServer = new Http2TestServer("127.0.0.1", false, 0);
+ http2TestServer.addHandler(new Http2EchoHandler(), "/http2/echo");
+ int port = http2TestServer.getAddress().getPort();
+ http2URI = "http://127.0.0.1:" + port + "/http2/echo";
+
+ https2TestServer = new Http2TestServer("127.0.0.1", true, 0);
+ https2TestServer.addHandler(new Http2EchoHandler(), "/https2/echo");
+ port = https2TestServer.getAddress().getPort();
+ https2URI = "https://127.0.0.1:" + port + "/https2/echo";
+
+ httpTestServer.start();
+ httpsTestServer.start();
+ http2TestServer.start();
+ https2TestServer.start();
+ }
+
+ @AfterTest
+ public void teardown() throws Exception {
+ httpTestServer.stop(0);
+ httpsTestServer.stop(0);
+ http2TestServer.stop();
+ https2TestServer.stop();
+ }
+
+ static class Http1EchoHandler implements HttpHandler {
+ @Override
+ public void handle(HttpExchange t) throws IOException {
+ try (InputStream is = t.getRequestBody();
+ OutputStream os = t.getResponseBody()) {
+ byte[] bytes = is.readAllBytes();
+ t.sendResponseHeaders(200, bytes.length);
+ os.write(bytes);
+ }
+ }
+ }
+
+ static class Http2EchoHandler implements Http2Handler {
+ @Override
+ public void handle(Http2TestExchange t) throws IOException {
+ try (InputStream is = t.getRequestBody();
+ OutputStream os = t.getResponseBody()) {
+ byte[] bytes = is.readAllBytes();
+ t.sendResponseHeaders(200, bytes.length);
+ os.write(bytes);
+ }
+ }
+ }
+}
--- /dev/null Thu Jan 01 00:00:00 1970 +0000
+++ b/test/jdk/java/net/httpclient/HandshakeFailureTest.java Sun Nov 05 17:32:13 2017 +0000
@@ -0,0 +1,288 @@
+/*
+ * Copyright (c) 2017, Oracle and/or its affiliates. All rights reserved.
+ * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER.
+ *
+ * This code is free software; you can redistribute it and/or modify it
+ * under the terms of the GNU General Public License version 2 only, as
+ * published by the Free Software Foundation.
+ *
+ * This code is distributed in the hope that it will be useful, but WITHOUT
+ * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or
+ * FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License
+ * version 2 for more details (a copy is included in the LICENSE file that
+ * accompanied this code).
+ *
+ * You should have received a copy of the GNU General Public License version
+ * 2 along with this work; if not, write to the Free Software Foundation,
+ * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA.
+ *
+ * Please contact Oracle, 500 Oracle Parkway, Redwood Shores, CA 94065 USA
+ * or visit www.oracle.com if you need additional information or have any
+ * questions.
+ */
+
+import javax.net.ServerSocketFactory;
+import javax.net.ssl.SSLContext;
+import javax.net.ssl.SSLHandshakeException;
+import javax.net.ssl.SSLSocket;
+import java.io.DataInputStream;
+import java.io.IOException;
+import java.io.UncheckedIOException;
+import java.net.ServerSocket;
+import java.net.Socket;
+import java.net.URI;
+import java.util.List;
+import java.util.concurrent.CompletableFuture;
+import java.util.concurrent.CompletionException;
+import jdk.incubator.http.HttpClient;
+import jdk.incubator.http.HttpClient.Version;
+import jdk.incubator.http.HttpResponse;
+import jdk.incubator.http.HttpRequest;
+import static java.lang.System.out;
+import static jdk.incubator.http.HttpResponse.BodyHandler.discard;
+
+/**
+ * @test
+ * @run main/othervm HandshakeFailureTest
+ * @summary Verify SSLHandshakeException is received when the handshake fails,
+ * either because the server closes ( EOF ) the connection during handshaking
+ * or no cipher suite ( or similar ) can be negotiated.
+ */
+// To switch on debugging use:
+// @run main/othervm -Djdk.internal.httpclient.debug=true HandshakeFailureTest
+public class HandshakeFailureTest {
+
+ // The number of iterations each testXXXClient performs. Can be increased
+ // when running standalone testing.
+ static final int TIMES = 10;
+
+ public static void main(String[] args) throws Exception {
+ HandshakeFailureTest test = new HandshakeFailureTest();
+ List<AbstractServer> servers = List.of( new PlainServer(), new SSLServer());
+
+ for (AbstractServer server : servers) {
+ try (server) {
+ out.format("%n%n------ Testing with server:%s ------%n", server);
+ URI uri = new URI("https://127.0.0.1:" + server.getPort() + "/");
+
+ test.testSyncSameClient(uri, Version.HTTP_1_1);
+ test.testSyncSameClient(uri, Version.HTTP_2);
+ test.testSyncDiffClient(uri, Version.HTTP_1_1);
+ test.testSyncDiffClient(uri, Version.HTTP_2);
+
+ test.testAsyncSameClient(uri, Version.HTTP_1_1);
+ test.testAsyncSameClient(uri, Version.HTTP_2);
+ test.testAsyncDiffClient(uri, Version.HTTP_1_1);
+ test.testAsyncDiffClient(uri, Version.HTTP_2);
+ }
+ }
+ }
+
+ void testSyncSameClient(URI uri, Version version) throws Exception {
+ out.printf("%n--- testSyncSameClient %s ---%n", version);
+ HttpClient client = HttpClient.newHttpClient();
+ for (int i = 0; i < TIMES; i++) {
+ out.printf("iteration %d%n", i);
+ HttpRequest request = HttpRequest.newBuilder(uri)
+ .version(version)
+ .build();
+ try {
+ HttpResponse<Void> response = client.send(request, discard(null));
+ String msg = String.format("UNEXPECTED response=%s%n", response);
+ throw new RuntimeException(msg);
+ } catch (SSLHandshakeException expected) {
+ out.printf("Client: caught expected exception: %s%n", expected);
+ }
+ }
+ }
+
+ void testSyncDiffClient(URI uri, Version version) throws Exception {
+ out.printf("%n--- testSyncDiffClient %s ---%n", version);
+ for (int i = 0; i < TIMES; i++) {
+ out.printf("iteration %d%n", i);
+ // a new client each time
+ HttpClient client = HttpClient.newHttpClient();
+ HttpRequest request = HttpRequest.newBuilder(uri)
+ .version(version)
+ .build();
+ try {
+ HttpResponse<Void> response = client.send(request, discard(null));
+ String msg = String.format("UNEXPECTED response=%s%n", response);
+ throw new RuntimeException(msg);
+ } catch (SSLHandshakeException expected) {
+ out.printf("Client: caught expected exception: %s%n", expected);
+ }
+ }
+ }
+
+ void testAsyncSameClient(URI uri, Version version) throws Exception {
+ out.printf("%n--- testAsyncSameClient %s ---%n", version);
+ HttpClient client = HttpClient.newHttpClient();
+ for (int i = 0; i < TIMES; i++) {
+ out.printf("iteration %d%n", i);
+ HttpRequest request = HttpRequest.newBuilder(uri)
+ .version(version)
+ .build();
+ CompletableFuture<HttpResponse<Void>> response =
+ client.sendAsync(request, discard(null));
+ try {
+ response.join();
+ String msg = String.format("UNEXPECTED response=%s%n", response);
+ throw new RuntimeException(msg);
+ } catch (CompletionException ce) {
+ if (ce.getCause() instanceof SSLHandshakeException) {
+ out.printf("Client: caught expected exception: %s%n", ce.getCause());
+ } else {
+ out.printf("Client: caught UNEXPECTED exception: %s%n", ce.getCause());
+ throw ce;
+ }
+ }
+ }
+ }
+
+ void testAsyncDiffClient(URI uri, Version version) throws Exception {
+ out.printf("%n--- testAsyncDiffClient %s ---%n", version);
+ for (int i = 0; i < TIMES; i++) {
+ out.printf("iteration %d%n", i);
+ // a new client each time
+ HttpClient client = HttpClient.newHttpClient();
+ HttpRequest request = HttpRequest.newBuilder(uri)
+ .version(version)
+ .build();
+ CompletableFuture<HttpResponse<Void>> response =
+ client.sendAsync(request, discard(null));
+ try {
+ response.join();
+ String msg = String.format("UNEXPECTED response=%s%n", response);
+ throw new RuntimeException(msg);
+ } catch (CompletionException ce) {
+ if (ce.getCause() instanceof SSLHandshakeException) {
+ out.printf("Client: caught expected exception: %s%n", ce.getCause());
+ } else {
+ out.printf("Client: caught UNEXPECTED exception: %s%n", ce.getCause());
+ throw ce;
+ }
+ }
+ }
+ }
+
+ /** Common supertype for PlainServer and SSLServer. */
+ static abstract class AbstractServer extends Thread implements AutoCloseable {
+ protected final ServerSocket ss;
+ protected volatile boolean closed;
+
+ AbstractServer(String name, ServerSocket ss) throws IOException {
+ super(name);
+ this.ss = ss;
+ this.start();
+ }
+
+ int getPort() { return ss.getLocalPort(); }
+
+ @Override
+ public void close() {
+ if (closed)
+ return;
+ closed = true;
+ try {
+ ss.close();
+ } catch (IOException e) {
+ throw new UncheckedIOException("Unexpected", e);
+ }
+ }
+ }
+
+ /** Emulates a server-side, using plain cleartext Sockets, that just closes
+ * the connection, after a small variable delay. */
+ static class PlainServer extends AbstractServer {
+ private volatile int count;
+
+ PlainServer() throws IOException {
+ super("PlainServer", new ServerSocket(0));
+ }
+
+ @Override
+ public void run() {
+ while (!closed) {
+ try (Socket s = ss.accept()) {
+ count++;
+
+ /* SSL record layer - contains the client hello
+ struct {
+ uint8 major, minor;
+ } ProtocolVersion;
+
+ enum {
+ change_cipher_spec(20), alert(21), handshake(22),
+ application_data(23), (255)
+ } ContentType;
+
+ struct {
+ ContentType type;
+ ProtocolVersion version;
+ uint16 length;
+ opaque fragment[SSLPlaintext.length];
+ } SSLPlaintext; */
+ DataInputStream din = new DataInputStream(s.getInputStream());
+ int contentType = din.read();
+ out.println("ContentType:" + contentType);
+ int majorVersion = din.read();
+ out.println("Major:" + majorVersion);
+ int minorVersion = din.read();
+ out.println("Minor:" + minorVersion);
+ int length = din.readShort();
+ out.println("length:" + length);
+ byte[] ba = new byte[length];
+ din.readFully(ba);
+
+ // simulate various delays in response
+ Thread.sleep(10 * (count % 10));
+ s.close(); // close without giving any reply
+ } catch (IOException e) {
+ if (!closed)
+ out.println("Unexpected" + e);
+ } catch (InterruptedException e) {
+ throw new RuntimeException(e);
+ }
+ }
+ }
+ }
+
+ /** Emulates a server-side, using SSL Sockets, that will fail during
+ * handshaking, as there are no cipher suites in common. */
+ static class SSLServer extends AbstractServer {
+ static final SSLContext sslContext = createUntrustingContext();
+ static final ServerSocketFactory factory = sslContext.getServerSocketFactory();
+
+ static SSLContext createUntrustingContext() {
+ try {
+ SSLContext sslContext = SSLContext.getInstance("TLSv1.2");
+ sslContext.init(null, null, null);
+ return sslContext;
+ } catch (Throwable t) {
+ throw new AssertionError(t);
+ }
+ }
+
+ SSLServer() throws IOException {
+ super("SSLServer", factory.createServerSocket(0));
+ }
+
+ @Override
+ public void run() {
+ while (!closed) {
+ try (SSLSocket s = (SSLSocket)ss.accept()) {
+ s.getInputStream().read(); // will throw SHE here
+
+ throw new AssertionError("Should not reach here");
+ } catch (SSLHandshakeException expected) {
+ // Expected: SSLHandshakeException: no cipher suites in common
+ out.printf("Server: caught expected exception: %s%n", expected);
+ } catch (IOException e) {
+ if (!closed)
+ out.printf("UNEXPECTED %s", e);
+ }
+ }
+ }
+ }
+}
--- /dev/null Thu Jan 01 00:00:00 1970 +0000
+++ b/test/jdk/java/net/httpclient/HeadersTest2.java Sun Nov 05 17:32:13 2017 +0000
@@ -0,0 +1,118 @@
+/*
+ * Copyright (c) 2015, 2016, Oracle and/or its affiliates. All rights reserved.
+ * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER.
+ *
+ * This code is free software; you can redistribute it and/or modify it
+ * under the terms of the GNU General Public License version 2 only, as
+ * published by the Free Software Foundation.
+ *
+ * This code is distributed in the hope that it will be useful, but WITHOUT
+ * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or
+ * FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License
+ * version 2 for more details (a copy is included in the LICENSE file that
+ * accompanied this code).
+ *
+ * You should have received a copy of the GNU General Public License version
+ * 2 along with this work; if not, write to the Free Software Foundation,
+ * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA.
+ *
+ * Please contact Oracle, 500 Oracle Parkway, Redwood Shores, CA 94065 USA
+ * or visit www.oracle.com if you need additional information or have any
+ * questions.
+ */
+
+/**
+ * @test
+ * @bug 8087112
+ * @summary Basic test for headers
+ */
+
+import jdk.incubator.http.HttpHeaders;
+import jdk.incubator.http.HttpRequest;
+import java.net.URI;
+import java.util.List;
+import java.util.Iterator;
+
+public class HeadersTest2 {
+ static URI uri = URI.create("http://www.foo.com/");
+
+ static class CompareTest {
+ boolean succeed;
+ List<String> nameValues1;
+ List<String> nameValues2;
+
+
+ /**
+ * Each list contains header-name, header-value, header-name, header-value
+ * sequences. The test creates two requests with the two lists
+ * and compares the HttpHeaders objects returned from the requests
+ *
+ * @param succeed
+ * @param l1
+ * @param l2
+ */
+ CompareTest(boolean succeed, List<String> l1, List<String> l2) {
+ this.succeed = succeed;
+ this.nameValues1 = l1;
+ this.nameValues2 = l2;
+ }
+
+ public void run() {
+ HttpRequest r1 = getRequest(nameValues1);
+ HttpRequest r2 = getRequest(nameValues2);
+ HttpHeaders h1 = r1.headers();
+ HttpHeaders h2 = r2.headers();
+ boolean equal = h1.equals(h2);
+ if (equal && !succeed) {
+ System.err.println("Expected failure");
+ print(nameValues1);
+ print(nameValues2);
+ throw new RuntimeException();
+ } else if (!equal && succeed) {
+ System.err.println("Expected success");
+ print(nameValues1);
+ print(nameValues2);
+ throw new RuntimeException();
+ }
+ }
+
+ static void print(List<String> list) {
+ System.err.print("{");
+ for (String s : list) {
+ System.err.print(s + " ");
+ }
+ System.err.println("}");
+ }
+
+ HttpRequest getRequest(List<String> headers) {
+ HttpRequest.Builder builder = HttpRequest.newBuilder(uri);
+ Iterator<String> iterator = headers.iterator();
+ while (iterator.hasNext()) {
+ String name = iterator.next();
+ String value = iterator.next();
+ builder.header(name, value);
+ }
+ return builder.GET().build();
+ }
+ }
+
+ static CompareTest test(boolean s, List<String> l1, List<String> l2) {
+ return new CompareTest(s, l1, l2);
+ }
+
+ static CompareTest[] compareTests = new CompareTest[] {
+ test(true, List.of("Dontent-length", "99"), List.of("dontent-length", "99")),
+ test(false, List.of("Dontent-length", "99"), List.of("dontent-length", "100")),
+ test(false, List.of("Name1", "val1", "Name1", "val2", "name1", "val3"),
+ List.of("Name1", "val1", "Name1", "val2")),
+ test(true, List.of("Name1", "val1", "Name1", "val2", "name1", "val3"),
+ List.of("NaMe1", "val1", "NAme1", "val2", "name1", "val3"))
+ };
+
+ public static void main(String[] args) {
+ for (CompareTest test : compareTests) {
+ test.run();
+ }
+ }
+}
+
--- /dev/null Thu Jan 01 00:00:00 1970 +0000
+++ b/test/jdk/java/net/httpclient/HttpClientBuilderTest.java Sun Nov 05 17:32:13 2017 +0000
@@ -0,0 +1,243 @@
+/*
+ * Copyright (c) 2017, Oracle and/or its affiliates. All rights reserved.
+ * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER.
+ *
+ * This code is free software; you can redistribute it and/or modify it
+ * under the terms of the GNU General Public License version 2 only, as
+ * published by the Free Software Foundation.
+ *
+ * This code is distributed in the hope that it will be useful, but WITHOUT
+ * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or
+ * FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License
+ * version 2 for more details (a copy is included in the LICENSE file that
+ * accompanied this code).
+ *
+ * You should have received a copy of the GNU General Public License version
+ * 2 along with this work; if not, write to the Free Software Foundation,
+ * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA.
+ *
+ * Please contact Oracle, 500 Oracle Parkway, Redwood Shores, CA 94065 USA
+ * or visit www.oracle.com if you need additional information or have any
+ * questions.
+ */
+
+import java.lang.reflect.Method;
+import java.net.Authenticator;
+import java.net.CookieManager;
+import java.net.InetSocketAddress;
+import java.net.ProxySelector;
+import java.util.List;
+import java.util.concurrent.Executor;
+import javax.net.ssl.SSLContext;
+import javax.net.ssl.SSLParameters;
+import jdk.incubator.http.HttpClient;
+import jdk.incubator.http.HttpClient.Redirect;
+import jdk.incubator.http.HttpClient.Version;
+import jdk.testlibrary.SimpleSSLContext;
+import org.testng.annotations.Test;
+import static org.testng.Assert.*;
+
+/*
+ * @test
+ * @summary HttpClient[.Builder] API and behaviour checks
+ * @library /lib/testlibrary/
+ * @build jdk.testlibrary.SimpleSSLContext
+ * @run testng HttpClientBuilderTest
+ */
+
+public class HttpClientBuilderTest {
+
+ @Test
+ public void testDefaults() throws Exception {
+ List<HttpClient> clients = List.of(HttpClient.newHttpClient(),
+ HttpClient.newBuilder().build());
+
+ for (HttpClient client : clients) {
+ // Empty optionals and defaults
+ assertFalse(client.authenticator().isPresent());
+ assertFalse(client.cookieManager().isPresent());
+ assertFalse(client.executor().isPresent());
+ assertFalse(client.proxy().isPresent());
+ assertTrue(client.sslParameters() != null);
+ assertTrue(client.followRedirects().equals(HttpClient.Redirect.NEVER));
+ assertTrue(client.sslContext() == SSLContext.getDefault());
+ assertTrue(client.version().equals(HttpClient.Version.HTTP_2));
+ }
+ }
+
+ @Test
+ public void testNull() throws Exception {
+ HttpClient.Builder builder = HttpClient.newBuilder();
+ assertThrows(NullPointerException.class, () -> builder.authenticator(null));
+ assertThrows(NullPointerException.class, () -> builder.cookieManager(null));
+ assertThrows(NullPointerException.class, () -> builder.executor(null));
+ assertThrows(NullPointerException.class, () -> builder.proxy(null));
+ assertThrows(NullPointerException.class, () -> builder.sslParameters(null));
+ assertThrows(NullPointerException.class, () -> builder.followRedirects(null));
+ assertThrows(NullPointerException.class, () -> builder.sslContext(null));
+ assertThrows(NullPointerException.class, () -> builder.version(null));
+ }
+
+ static class TestAuthenticator extends Authenticator { }
+
+ @Test
+ public void testAuthenticator() {
+ HttpClient.Builder builder = HttpClient.newBuilder();
+ Authenticator a = new TestAuthenticator();
+ builder.authenticator(a);
+ assertTrue(builder.build().authenticator().get() == a);
+ Authenticator b = new TestAuthenticator();
+ builder.authenticator(b);
+ assertTrue(builder.build().authenticator().get() == b);
+ assertThrows(NullPointerException.class, () -> builder.authenticator(null));
+ Authenticator c = new TestAuthenticator();
+ builder.authenticator(c);
+ assertTrue(builder.build().authenticator().get() == c);
+ }
+
+ @Test
+ public void testCookieManager() {
+ HttpClient.Builder builder = HttpClient.newBuilder();
+ CookieManager a = new CookieManager();
+ builder.cookieManager(a);
+ assertTrue(builder.build().cookieManager().get() == a);
+ CookieManager b = new CookieManager();
+ builder.cookieManager(b);
+ assertTrue(builder.build().cookieManager().get() == b);
+ assertThrows(NullPointerException.class, () -> builder.cookieManager(null));
+ CookieManager c = new CookieManager();
+ builder.cookieManager(c);
+ assertTrue(builder.build().cookieManager().get() == c);
+ }
+
+ static class TestExecutor implements Executor {
+ public void execute(Runnable r) { }
+ }
+
+ @Test
+ public void testExecutor() {
+ HttpClient.Builder builder = HttpClient.newBuilder();
+ TestExecutor a = new TestExecutor();
+ builder.executor(a);
+ assertTrue(builder.build().executor().get() == a);
+ TestExecutor b = new TestExecutor();
+ builder.executor(b);
+ assertTrue(builder.build().executor().get() == b);
+ assertThrows(NullPointerException.class, () -> builder.executor(null));
+ TestExecutor c = new TestExecutor();
+ builder.executor(c);
+ assertTrue(builder.build().executor().get() == c);
+ }
+
+ @Test
+ public void testProxySelector() {
+ HttpClient.Builder builder = HttpClient.newBuilder();
+ ProxySelector a = ProxySelector.of(null);
+ builder.proxy(a);
+ assertTrue(builder.build().proxy().get() == a);
+ ProxySelector b = ProxySelector.of(InetSocketAddress.createUnresolved("foo", 80));
+ builder.proxy(b);
+ assertTrue(builder.build().proxy().get() == b);
+ assertThrows(NullPointerException.class, () -> builder.proxy(null));
+ ProxySelector c = ProxySelector.of(InetSocketAddress.createUnresolved("bar", 80));
+ builder.proxy(c);
+ assertTrue(builder.build().proxy().get() == c);
+ }
+
+ @Test
+ public void testSSLParameters() {
+ HttpClient.Builder builder = HttpClient.newBuilder();
+ SSLParameters a = new SSLParameters();
+ a.setCipherSuites(new String[] { "A" });
+ builder.sslParameters(a);
+ a.setCipherSuites(new String[] { "Z" });
+ assertTrue(builder.build().sslParameters() != (a));
+ assertTrue(builder.build().sslParameters().getCipherSuites()[0].equals("A"));
+ SSLParameters b = new SSLParameters();
+ b.setEnableRetransmissions(true);
+ builder.sslParameters(b);
+ assertTrue(builder.build().sslParameters() != b);
+ assertTrue(builder.build().sslParameters().getEnableRetransmissions());
+ assertThrows(NullPointerException.class, () -> builder.sslParameters(null));
+ SSLParameters c = new SSLParameters();
+ c.setProtocols(new String[] { "C" });
+ builder.sslParameters(c);
+ c.setProtocols(new String[] { "D" });
+ assertTrue(builder.build().sslParameters().getProtocols()[0].equals("C"));
+ }
+
+ @Test
+ public void testSSLContext() throws Exception {
+ HttpClient.Builder builder = HttpClient.newBuilder();
+ SSLContext a = (new SimpleSSLContext()).get();
+ builder.sslContext(a);
+ assertTrue(builder.build().sslContext() == a);
+ SSLContext b = (new SimpleSSLContext()).get();
+ builder.sslContext(b);
+ assertTrue(builder.build().sslContext() == b);
+ assertThrows(NullPointerException.class, () -> builder.sslContext(null));
+ SSLContext c = (new SimpleSSLContext()).get();
+ builder.sslContext(c);
+ assertTrue(builder.build().sslContext() == c);
+ }
+
+ @Test
+ public void testFollowRedirects() {
+ HttpClient.Builder builder = HttpClient.newBuilder();
+ builder.followRedirects(Redirect.ALWAYS);
+ assertTrue(builder.build().followRedirects() == Redirect.ALWAYS);
+ builder.followRedirects(Redirect.NEVER);
+ assertTrue(builder.build().followRedirects() == Redirect.NEVER);
+ assertThrows(NullPointerException.class, () -> builder.followRedirects(null));
+ builder.followRedirects(Redirect.SAME_PROTOCOL);
+ assertTrue(builder.build().followRedirects() == Redirect.SAME_PROTOCOL);
+ builder.followRedirects(Redirect.SECURE);
+ assertTrue(builder.build().followRedirects() == Redirect.SECURE);
+ }
+
+ @Test
+ public void testVersion() {
+ HttpClient.Builder builder = HttpClient.newBuilder();
+ builder.version(Version.HTTP_2);
+ assertTrue(builder.build().version() == Version.HTTP_2);
+ builder.version(Version.HTTP_1_1);
+ assertTrue(builder.build().version() == Version.HTTP_1_1);
+ assertThrows(NullPointerException.class, () -> builder.version(null));
+ builder.version(Version.HTTP_2);
+ assertTrue(builder.build().version() == Version.HTTP_2);
+ builder.version(Version.HTTP_1_1);
+ assertTrue(builder.build().version() == Version.HTTP_1_1);
+ }
+
+ @Test
+ static void testPriority() throws Exception {
+ HttpClient.Builder builder = HttpClient.newBuilder();
+ assertThrows(IllegalArgumentException.class, () -> builder.priority(-1));
+ assertThrows(IllegalArgumentException.class, () -> builder.priority(0));
+ assertThrows(IllegalArgumentException.class, () -> builder.priority(257));
+ assertThrows(IllegalArgumentException.class, () -> builder.priority(500));
+
+ builder.priority(1);
+ builder.build();
+ builder.priority(256);
+ builder.build();
+ }
+
+
+ /* ---- standalone entry point ---- */
+
+ public static void main(String[] args) throws Exception {
+ HttpClientBuilderTest test = new HttpClientBuilderTest();
+ for (Method m : HttpClientBuilderTest.class.getDeclaredMethods()) {
+ if (m.isAnnotationPresent(Test.class)) {
+ try {
+ m.invoke(test);
+ System.out.printf("test %s: success%n", m.getName());
+ } catch (Throwable t ) {
+ System.out.printf("test %s: failed%n", m.getName());
+ t.printStackTrace();
+ }
+ }
+ }
+ }
+}
--- a/test/jdk/java/net/httpclient/HttpInputStreamTest.java Sun Nov 05 17:05:57 2017 +0000
+++ b/test/jdk/java/net/httpclient/HttpInputStreamTest.java Sun Nov 05 17:32:13 2017 +0000
@@ -32,6 +32,8 @@
import jdk.incubator.http.HttpResponse;
import java.nio.ByteBuffer;
import java.nio.charset.Charset;
+import java.util.Iterator;
+import java.util.List;
import java.util.Locale;
import java.util.Optional;
import java.util.concurrent.ArrayBlockingQueue;
@@ -40,11 +42,12 @@
import java.util.concurrent.CompletionStage;
import java.util.concurrent.Flow;
import java.util.stream.Stream;
+import static java.lang.System.err;
/*
* @test
* @summary An example on how to read a response body with InputStream...
- * @run main/othervm HttpInputStreamTest
+ * @run main/othervm -Dtest.debug=true HttpInputStreamTest
* @author daniel fuchs
*/
public class HttpInputStreamTest {
@@ -61,7 +64,7 @@
public static class HttpInputStreamHandler
implements HttpResponse.BodyHandler<InputStream> {
- public static final int MAX_BUFFERS_IN_QUEUE = 1;
+ public static final int MAX_BUFFERS_IN_QUEUE = 1; // lock-step with the producer
private final int maxBuffers;
@@ -74,7 +77,7 @@
}
@Override
- public synchronized HttpResponse.BodyProcessor<InputStream>
+ public HttpResponse.BodySubscriber<InputStream>
apply(int i, HttpHeaders hh) {
return new HttpResponseInputStream(maxBuffers);
}
@@ -83,17 +86,19 @@
* An InputStream built on top of the Flow API.
*/
private static class HttpResponseInputStream extends InputStream
- implements HttpResponse.BodyProcessor<InputStream> {
+ implements HttpResponse.BodySubscriber<InputStream> {
// An immutable ByteBuffer sentinel to mark that the last byte was received.
- private static final ByteBuffer LAST = ByteBuffer.wrap(new byte[0]);
+ private static final ByteBuffer LAST_BUFFER = ByteBuffer.wrap(new byte[0]);
+ private static final List<ByteBuffer> LAST_LIST = List.of(LAST_BUFFER);
// A queue of yet unprocessed ByteBuffers received from the flow API.
- private final BlockingQueue<ByteBuffer> buffers;
+ private final BlockingQueue<List<ByteBuffer>> buffers;
private volatile Flow.Subscription subscription;
private volatile boolean closed;
private volatile Throwable failed;
- private volatile ByteBuffer current;
+ private volatile Iterator<ByteBuffer> currentListItr;
+ private volatile ByteBuffer currentBuffer;
HttpResponseInputStream() {
this(MAX_BUFFERS_IN_QUEUE);
@@ -101,7 +106,8 @@
HttpResponseInputStream(int maxBuffers) {
int capacity = maxBuffers <= 0 ? MAX_BUFFERS_IN_QUEUE : maxBuffers;
- this.buffers = new ArrayBlockingQueue<>(capacity);
+ // 1 additional slot for LAST_LIST added by onComplete
+ this.buffers = new ArrayBlockingQueue<>(capacity + 1);
}
@Override
@@ -119,40 +125,49 @@
// a new buffer is made available through the Flow API, or the
// end of the flow is reached.
private ByteBuffer current() throws IOException {
- while (current == null || !current.hasRemaining()) {
- // Check whether the stream is claused or exhausted
+ while (currentBuffer == null || !currentBuffer.hasRemaining()) {
+ // Check whether the stream is closed or exhausted
if (closed || failed != null) {
throw new IOException("closed", failed);
}
- if (current == LAST) break;
+ if (currentBuffer == LAST_BUFFER) break;
try {
- // Take a new buffer from the queue, blocking
- // if none is available yet...
- if (DEBUG) System.err.println("Taking Buffer");
- current = buffers.take();
- if (DEBUG) System.err.println("Buffer Taken");
+ if (currentListItr == null || !currentListItr.hasNext()) {
+ // Take a new list of buffers from the queue, blocking
+ // if none is available yet...
+
+ if (DEBUG) err.println("Taking list of Buffers");
+ List<ByteBuffer> lb = buffers.take();
+ currentListItr = lb.iterator();
+ if (DEBUG) err.println("List of Buffers Taken");
+
+ // Check whether an exception was encountered upstream
+ if (closed || failed != null)
+ throw new IOException("closed", failed);
- // Check whether some exception was encountered
- // upstream
- if (closed || failed != null) {
- throw new IOException("closed", failed);
- }
+ // Check whether we're done.
+ if (lb == LAST_LIST) {
+ currentListItr = null;
+ currentBuffer = LAST_BUFFER;
+ break;
+ }
- // Check whether we're done.
- if (current == LAST) break;
-
- // Inform the producer that it can start sending
- // us a new buffer
- Flow.Subscription s = subscription;
- if (s != null) s.request(1);
-
+ // Request another upstream item ( list of buffers )
+ Flow.Subscription s = subscription;
+ if (s != null)
+ s.request(1);
+ }
+ assert currentListItr != null;
+ assert currentListItr.hasNext();
+ if (DEBUG) err.println("Next Buffer");
+ currentBuffer = currentListItr.next();
} catch (InterruptedException ex) {
// continue
}
}
- assert current == LAST || current.hasRemaining();
- return current;
+ assert currentBuffer == LAST_BUFFER || currentBuffer.hasRemaining();
+ return currentBuffer;
}
@Override
@@ -160,7 +175,7 @@
// get the buffer to read from, possibly blocking if
// none is available
ByteBuffer buffer;
- if ((buffer = current()) == LAST) return -1;
+ if ((buffer = current()) == LAST_BUFFER) return -1;
// don't attempt to read more than what is available
// in the current buffer.
@@ -175,22 +190,31 @@
@Override
public int read() throws IOException {
ByteBuffer buffer;
- if ((buffer = current()) == LAST) return -1;
+ if ((buffer = current()) == LAST_BUFFER) return -1;
return buffer.get() & 0xFF;
}
@Override
public void onSubscribe(Flow.Subscription s) {
+ if (this.subscription != null) {
+ s.cancel();
+ return;
+ }
this.subscription = s;
- s.request(Math.max(2, buffers.remainingCapacity() + 1));
+ assert buffers.remainingCapacity() > 1; // should at least be 2
+ if (DEBUG) err.println("onSubscribe: requesting "
+ + Math.max(1, buffers.remainingCapacity() - 1));
+ s.request(Math.max(1, buffers.remainingCapacity() - 1));
}
@Override
- public synchronized void onNext(ByteBuffer t) {
+ public void onNext(List<ByteBuffer> t) {
try {
- if (DEBUG) System.err.println("next buffer received");
- buffers.put(t);
- if (DEBUG) System.err.println("buffered offered");
+ if (DEBUG) err.println("next item received");
+ if (!buffers.offer(t)) {
+ throw new IllegalStateException("queue is full");
+ }
+ if (DEBUG) err.println("item offered");
} catch (Exception ex) {
failed = ex;
try {
@@ -203,24 +227,26 @@
@Override
public void onError(Throwable thrwbl) {
+ subscription = null;
failed = thrwbl;
}
@Override
- public synchronized void onComplete() {
+ public void onComplete() {
subscription = null;
- onNext(LAST);
+ onNext(LAST_LIST);
}
@Override
public void close() throws IOException {
synchronized (this) {
+ if (closed) return;
closed = true;
- Flow.Subscription s = subscription;
- if (s != null) {
- s.cancel();
- }
- subscription = null;
+ }
+ Flow.Subscription s = subscription;
+ subscription = null;
+ if (s != null) {
+ s.cancel();
}
super.close();
}
@@ -274,8 +300,8 @@
// client.sendAsync(request, HttpResponse.BodyHandler.asString()).get().body());
CompletableFuture<HttpResponse<InputStream>> handle =
- client.sendAsync(request, new HttpInputStreamHandler());
- if (DEBUG) System.err.println("Request sent");
+ client.sendAsync(request, new HttpInputStreamHandler(3));
+ if (DEBUG) err.println("Request sent");
HttpResponse<InputStream> pending = handle.get();
@@ -301,8 +327,8 @@
char[] buff = new char[32];
int off=0, n=0;
- if (DEBUG) System.err.println("Start receiving response body");
- if (DEBUG) System.err.println("Charset: " + charset.get());
+ if (DEBUG) err.println("Start receiving response body");
+ if (DEBUG) err.println("Charset: " + charset.get());
// Start consuming the InputStream as the data arrives.
// Will block until there is something to read...
--- a/test/jdk/java/net/httpclient/HttpRequestBuilderTest.java Sun Nov 05 17:05:57 2017 +0000
+++ b/test/jdk/java/net/httpclient/HttpRequestBuilderTest.java Sun Nov 05 17:32:13 2017 +0000
@@ -1,5 +1,5 @@
/*
- * Copyright (c) 2016, Oracle and/or its affiliates. All rights reserved.
+ * Copyright (c) 2016, 2017, Oracle and/or its affiliates. All rights reserved.
* DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER.
*
* This code is free software; you can redistribute it and/or modify it
@@ -21,21 +21,22 @@
* questions.
*/
-import jdk.incubator.http.HttpRequest;
import java.net.URI;
import jdk.incubator.http.HttpClient;
import java.time.Duration;
+import java.util.Arrays;
import java.util.function.BiFunction;
import java.util.function.Function;
+import java.util.function.Supplier;
import java.util.stream.Collectors;
import java.util.stream.Stream;
+import jdk.incubator.http.HttpRequest;
+import static jdk.incubator.http.HttpRequest.BodyPublisher.fromString;
/**
* @test
* @bug 8170064
- * @summary HttpRequest API documentation says:" Unless otherwise stated,
- * {@code null} parameter values will cause methods
- * of this class to throw {@code NullPointerException}".
+ * @summary HttpRequest[.Builder] API and behaviour checks
*/
public class HttpRequestBuilderTest {
@@ -44,70 +45,166 @@
public static void main(String[] args) throws Exception {
+ test0("newBuilder().build()",
+ () -> HttpRequest.newBuilder().build(),
+ IllegalStateException.class);
+
+ test0("newBuilder(null)",
+ () -> HttpRequest.newBuilder(null),
+ NullPointerException.class);
+
+ test0("newBuilder(URI.create(\"badScheme://www.foo.com/\")",
+ () -> HttpRequest.newBuilder(URI.create("badScheme://www.foo.com/")),
+ IllegalArgumentException.class);
+
+ test0("newBuilder(URI.create(\"http://www.foo.com:-1/\")",
+ () -> HttpRequest.newBuilder(URI.create("http://www.foo.com:-1/")),
+ IllegalArgumentException.class);
+
+ test0("newBuilder(URI.create(\"https://www.foo.com:-1/\")",
+ () -> HttpRequest.newBuilder(URI.create("https://www.foo.com:-1/")),
+ IllegalArgumentException.class);
+
+ test0("newBuilder(" + TEST_URI + ").uri(null)",
+ () -> HttpRequest.newBuilder(TEST_URI).uri(null),
+ NullPointerException.class);
+
+ test0("newBuilder(uri).build()",
+ () -> HttpRequest.newBuilder(TEST_URI).build()
+ /* no expected exceptions */ );
+
HttpRequest.Builder builder = HttpRequest.newBuilder();
+
builder = test1("uri", builder, builder::uri, (URI)null,
NullPointerException.class);
+
+ builder = test1("uri", builder, builder::uri, URI.create("http://www.foo.com:-1/"),
+ IllegalArgumentException.class);
+
+ builder = test1("uri", builder, builder::uri, URI.create("https://www.foo.com:-1/"),
+ IllegalArgumentException.class);
+
builder = test2("header", builder, builder::header, (String) null, "bar",
NullPointerException.class);
+
builder = test2("header", builder, builder::header, "foo", (String) null,
NullPointerException.class);
+
builder = test2("header", builder, builder::header, (String)null,
(String) null, NullPointerException.class);
+
+ builder = test2("header", builder, builder::header, "", "bar",
+ IllegalArgumentException.class);
+
+ builder = test2("header", builder, builder::header, "foo", "\r",
+ IllegalArgumentException.class);
+
builder = test1("headers", builder, builder::headers, (String[]) null,
NullPointerException.class);
+
+ builder = test1("headers", builder, builder::headers, new String[0],
+ IllegalArgumentException.class);
+
builder = test1("headers", builder, builder::headers,
(String[]) new String[] {null, "bar"},
NullPointerException.class);
+
builder = test1("headers", builder, builder::headers,
(String[]) new String[] {"foo", null},
NullPointerException.class);
+
builder = test1("headers", builder, builder::headers,
(String[]) new String[] {null, null},
NullPointerException.class);
+
builder = test1("headers", builder, builder::headers,
- (String[]) new String[] {"foo", "bar", null},
- NullPointerException.class,
- IllegalArgumentException.class);
+ (String[]) new String[] {"foo", "bar", null},
+ NullPointerException.class,
+ IllegalArgumentException.class);
+
+ builder = test1("headers", builder, builder::headers,
+ (String[]) new String[] {"foo", "bar", null, null},
+ NullPointerException.class);
+
+ builder = test1("headers", builder, builder::headers,
+ (String[]) new String[] {"foo", "bar", "baz", null},
+ NullPointerException.class);
+
builder = test1("headers", builder, builder::headers,
- (String[]) new String[] {"foo", "bar", null, null},
- NullPointerException.class);
+ (String[]) new String[] {"foo", "bar", "\r", "baz"},
+ IllegalArgumentException.class);
+
+ builder = test1("headers", builder, builder::headers,
+ (String[]) new String[] {"foo", "bar", "baz", "\n"},
+ IllegalArgumentException.class);
+
builder = test1("headers", builder, builder::headers,
- (String[]) new String[] {"foo", "bar", "baz", null},
- NullPointerException.class);
+ (String[]) new String[] {"foo", "bar", "", "baz"},
+ IllegalArgumentException.class);
+
builder = test1("headers", builder, builder::headers,
- (String[]) new String[] {"foo", "bar", null, "baz"},
- NullPointerException.class);
+ (String[]) new String[] {"foo", "bar", null, "baz"},
+ NullPointerException.class);
+
builder = test1("headers", builder, builder::headers,
- (String[]) new String[] {"foo", "bar", "baz"},
- IllegalArgumentException.class);
+ (String[]) new String[] {"foo", "bar", "baz"},
+ IllegalArgumentException.class);
+
builder = test1("headers", builder, builder::headers,
- (String[]) new String[] {"foo"},
- IllegalArgumentException.class);
+ (String[]) new String[] {"foo"},
+ IllegalArgumentException.class);
+
builder = test1("DELETE", builder, builder::DELETE,
- (HttpRequest.BodyProcessor)null, null);
+ HttpRequest.noBody(), null);
+
builder = test1("POST", builder, builder::POST,
- (HttpRequest.BodyProcessor)null, null);
+ HttpRequest.noBody(), null);
+
builder = test1("PUT", builder, builder::PUT,
- (HttpRequest.BodyProcessor)null, null);
+ HttpRequest.noBody(), null);
+
builder = test2("method", builder, builder::method, "GET",
- (HttpRequest.BodyProcessor) null, null);
+ HttpRequest.noBody(), null);
+
+ builder = test1("DELETE", builder, builder::DELETE,
+ (HttpRequest.BodyPublisher)null,
+ NullPointerException.class);
+
+ builder = test1("POST", builder, builder::POST,
+ (HttpRequest.BodyPublisher)null,
+ NullPointerException.class);
+
+ builder = test1("PUT", builder, builder::PUT,
+ (HttpRequest.BodyPublisher)null,
+ NullPointerException.class);
+
+ builder = test2("method", builder, builder::method, "GET",
+ (HttpRequest.BodyPublisher) null,
+ NullPointerException.class);
+
builder = test2("setHeader", builder, builder::setHeader,
(String) null, "bar",
NullPointerException.class);
+
builder = test2("setHeader", builder, builder::setHeader,
"foo", (String) null,
NullPointerException.class);
+
builder = test2("setHeader", builder, builder::setHeader,
(String)null, (String) null,
NullPointerException.class);
+
builder = test1("timeout", builder, builder::timeout,
- (Duration)null, NullPointerException.class);
+ (Duration)null,
+ NullPointerException.class);
+
builder = test1("version", builder, builder::version,
(HttpClient.Version)null,
NullPointerException.class);
+
builder = test2("method", builder, builder::method, null,
- HttpRequest.BodyProcessor.fromString("foo"),
- NullPointerException.class);
+ HttpRequest.BodyPublisher.fromString("foo"),
+ NullPointerException.class);
// see JDK-8170093
//
// builder = test2("method", builder, builder::method, "foo",
@@ -116,6 +213,53 @@
//
// builder.build();
+
+ method("newBuilder(TEST_URI).build().method() == GET",
+ () -> HttpRequest.newBuilder(TEST_URI),
+ "GET");
+
+ method("newBuilder(TEST_URI).GET().build().method() == GET",
+ () -> HttpRequest.newBuilder(TEST_URI).GET(),
+ "GET");
+
+ method("newBuilder(TEST_URI).POST(fromString(\"\")).GET().build().method() == GET",
+ () -> HttpRequest.newBuilder(TEST_URI).POST(fromString("")).GET(),
+ "GET");
+
+ method("newBuilder(TEST_URI).PUT(fromString(\"\")).GET().build().method() == GET",
+ () -> HttpRequest.newBuilder(TEST_URI).PUT(fromString("")).GET(),
+ "GET");
+
+ method("newBuilder(TEST_URI).DELETE(fromString(\"\")).GET().build().method() == GET",
+ () -> HttpRequest.newBuilder(TEST_URI).DELETE(fromString("")).GET(),
+ "GET");
+
+ method("newBuilder(TEST_URI).POST(fromString(\"\")).build().method() == POST",
+ () -> HttpRequest.newBuilder(TEST_URI).POST(fromString("")),
+ "POST");
+
+ method("newBuilder(TEST_URI).PUT(fromString(\"\")).build().method() == PUT",
+ () -> HttpRequest.newBuilder(TEST_URI).PUT(fromString("")),
+ "PUT");
+
+ method("newBuilder(TEST_URI).DELETE(fromString(\"\")).build().method() == DELETE",
+ () -> HttpRequest.newBuilder(TEST_URI).DELETE(fromString("")),
+ "DELETE");
+
+ method("newBuilder(TEST_URI).GET().POST(fromString(\"\")).build().method() == POST",
+ () -> HttpRequest.newBuilder(TEST_URI).GET().POST(fromString("")),
+ "POST");
+
+ method("newBuilder(TEST_URI).GET().PUT(fromString(\"\")).build().method() == PUT",
+ () -> HttpRequest.newBuilder(TEST_URI).GET().PUT(fromString("")),
+ "PUT");
+
+ method("newBuilder(TEST_URI).GET().DELETE(fromString(\"\")).build().method() == DELETE",
+ () -> HttpRequest.newBuilder(TEST_URI).GET().DELETE(fromString("")),
+ "DELETE");
+
+
+
}
private static boolean shouldFail(Class<? extends Exception> ...exceptions) {
@@ -133,22 +277,66 @@
.findAny().isPresent();
}
- public static <R,P> R test1(String name, R receiver, Function<P, R> m, P arg,
- Class<? extends Exception> ...ex) {
+ static void method(String name,
+ Supplier<HttpRequest.Builder> supplier,
+ String expectedMethod) {
+ HttpRequest request = supplier.get().build();
+ String method = request.method();
+ if (request.method().equals("GET") && request.bodyPublisher().isPresent())
+ throw new AssertionError("failed: " + name
+ + ". Unexpected body processor for GET: "
+ + request.bodyPublisher().get());
+
+ if (expectedMethod.equals(method)) {
+ System.out.println("success: " + name);
+ } else {
+ throw new AssertionError("failed: " + name
+ + ". Expected " + expectedMethod + ", got " + method);
+ }
+ }
+
+ static void test0(String name,
+ Runnable r,
+ Class<? extends Exception> ...ex) {
try {
- R result = m.apply(arg);
+ r.run();
if (!shouldFail(ex)) {
- System.out.println("success: " + name + "(" + arg + ")");
- return result;
+ System.out.println("success: " + name);
+ return;
} else {
throw new AssertionError("Expected " + expectedNames(ex)
- + " not raised for " + name + "(" + arg + ")");
+ + " not raised for " + name);
}
} catch (Exception x) {
if (!isExpected(x, ex)) {
throw x;
} else {
- System.out.println("success: " + name + "(" + arg + ")" +
+ System.out.println("success: " + name +
+ " - Got expected exception: " + x);
+ }
+ }
+ }
+
+ public static <R,P> R test1(String name, R receiver, Function<P, R> m, P arg,
+ Class<? extends Exception> ...ex) {
+ String argMessage = arg == null ? "null" : arg.toString();
+ if (arg instanceof String[]) {
+ argMessage = Arrays.asList((String[])arg).toString();
+ }
+ try {
+ R result = m.apply(arg);
+ if (!shouldFail(ex)) {
+ System.out.println("success: " + name + "(" + argMessage + ")");
+ return result;
+ } else {
+ throw new AssertionError("Expected " + expectedNames(ex)
+ + " not raised for " + name + "(" + argMessage + ")");
+ }
+ } catch (Exception x) {
+ if (!isExpected(x, ex)) {
+ throw x;
+ } else {
+ System.out.println("success: " + name + "(" + argMessage + ")" +
" - Got expected exception: " + x);
return receiver;
}
--- a/test/jdk/java/net/httpclient/LightWeightHttpServer.java Sun Nov 05 17:05:57 2017 +0000
+++ b/test/jdk/java/net/httpclient/LightWeightHttpServer.java Sun Nov 05 17:32:13 2017 +0000
@@ -80,7 +80,7 @@
ch.setLevel(Level.ALL);
logger.addHandler(ch);
- String root = System.getProperty("test.src") + "/docs";
+ String root = System.getProperty("test.src", ".") + "/docs";
InetSocketAddress addr = new InetSocketAddress(0);
httpServer = HttpServer.create(addr, 0);
if (httpServer instanceof HttpsServer) {
@@ -301,11 +301,12 @@
@Override
public synchronized void handle(HttpExchange he) throws IOException {
- byte[] buf = Util.readAll(he.getRequestBody());
- try {
+ try(InputStream is = he.getRequestBody()) {
+ is.readAllBytes();
bar1.await();
bar2.await();
} catch (InterruptedException | BrokenBarrierException e) {
+ throw new IOException(e);
}
he.sendResponseHeaders(200, -1); // will probably fail
he.close();
--- a/test/jdk/java/net/httpclient/ManyRequests.java Sun Nov 05 17:05:57 2017 +0000
+++ b/test/jdk/java/net/httpclient/ManyRequests.java Sun Nov 05 17:32:13 2017 +0000
@@ -56,13 +56,12 @@
import java.util.HashMap;
import java.util.LinkedList;
import java.util.Random;
-import java.util.concurrent.ExecutorService;
import java.util.logging.Logger;
import java.util.logging.Level;
import java.util.concurrent.CompletableFuture;
import javax.net.ssl.SSLContext;
import jdk.testlibrary.SimpleSSLContext;
-import static jdk.incubator.http.HttpRequest.BodyProcessor.fromByteArray;
+import static jdk.incubator.http.HttpRequest.BodyPublisher.fromByteArray;
import static jdk.incubator.http.HttpResponse.BodyHandler.asByteArray;
public class ManyRequests {
@@ -91,7 +90,6 @@
System.out.println("OK");
} finally {
server.stop(0);
- ((ExecutorService)client.executor()).shutdownNow();
}
}
@@ -111,15 +109,17 @@
}
protected void close(OutputStream os) throws IOException {
if (INSERT_DELAY) {
- try { Thread.sleep(rand.nextInt(200)); } catch (InterruptedException e) {}
+ try { Thread.sleep(rand.nextInt(200)); }
+ catch (InterruptedException e) {}
}
- super.close(os);
+ os.close();
}
protected void close(InputStream is) throws IOException {
if (INSERT_DELAY) {
- try { Thread.sleep(rand.nextInt(200)); } catch (InterruptedException e) {}
+ try { Thread.sleep(rand.nextInt(200)); }
+ catch (InterruptedException e) {}
}
- super.close(is);
+ is.close();
}
}
--- a/test/jdk/java/net/httpclient/ManyRequests2.java Sun Nov 05 17:05:57 2017 +0000
+++ b/test/jdk/java/net/httpclient/ManyRequests2.java Sun Nov 05 17:32:13 2017 +0000
@@ -36,7 +36,7 @@
* @run main/othervm/timeout=40 -Dtest.XFixed=true ManyRequests2
* @run main/othervm/timeout=40 -Dtest.XFixed=true -Dtest.insertDelay=true ManyRequests2
* @run main/othervm/timeout=40 -Dtest.XFixed=true -Dtest.chunkSize=64 ManyRequests2
- * @run main/othervm/timeout=40 -Dtest.XFixed=true -Dtest.insertDelay=true -Dtest.chunkSize=64 ManyRequests2
+ * @run main/othervm/timeout=40 -Djdk.internal.httpclient.debug=true -Dtest.XFixed=true -Dtest.insertDelay=true -Dtest.chunkSize=64 ManyRequests2
* @summary Send a large number of requests asynchronously. The server echoes back using known content length.
*/
// * @run main/othervm/timeout=40 -Djdk.httpclient.HttpClient.log=ssl ManyRequests
--- /dev/null Thu Jan 01 00:00:00 1970 +0000
+++ b/test/jdk/java/net/httpclient/ManyRequestsLegacy.java Sun Nov 05 17:32:13 2017 +0000
@@ -0,0 +1,376 @@
+/*
+ * Copyright (c) 2015, 2017, Oracle and/or its affiliates. All rights reserved.
+ * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER.
+ *
+ * This code is free software; you can redistribute it and/or modify it
+ * under the terms of the GNU General Public License version 2 only, as
+ * published by the Free Software Foundation.
+ *
+ * This code is distributed in the hope that it will be useful, but WITHOUT
+ * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or
+ * FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License
+ * version 2 for more details (a copy is included in the LICENSE file that
+ * accompanied this code).
+ *
+ * You should have received a copy of the GNU General Public License version
+ * 2 along with this work; if not, write to the Free Software Foundation,
+ * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA.
+ *
+ * Please contact Oracle, 500 Oracle Parkway, Redwood Shores, CA 94065 USA
+ * or visit www.oracle.com if you need additional information or have any
+ * questions.
+ */
+
+/*
+ * @test
+ * @modules jdk.incubator.httpclient
+ * java.logging
+ * jdk.httpserver
+ * @library /lib/testlibrary/ /
+ * @build jdk.testlibrary.SimpleSSLContext
+ * @compile ../../../com/sun/net/httpserver/LogFilter.java
+ * @compile ../../../com/sun/net/httpserver/EchoHandler.java
+ * @compile ../../../com/sun/net/httpserver/FileServerHandler.java
+ * @run main/othervm/timeout=40 ManyRequestsLegacy
+ * @run main/othervm/timeout=40 -Dtest.insertDelay=true ManyRequestsLegacy
+ * @run main/othervm/timeout=40 -Dtest.chunkSize=64 ManyRequestsLegacy
+ * @run main/othervm/timeout=40 -Dtest.insertDelay=true -Dtest.chunkSize=64 ManyRequestsLegacy
+ * @summary Send a large number of requests asynchronously using the legacy URL.openConnection(), to help sanitize results of the test ManyRequest.java.
+ */
+
+import javax.net.ssl.HttpsURLConnection;
+import javax.net.ssl.HostnameVerifier;
+import com.sun.net.httpserver.HttpsConfigurator;
+import com.sun.net.httpserver.HttpsParameters;
+import com.sun.net.httpserver.HttpsServer;
+import com.sun.net.httpserver.HttpExchange;
+import java.io.IOException;
+import java.io.InputStream;
+import java.io.OutputStream;
+import java.net.HttpURLConnection;
+import java.net.URI;
+import java.net.URLConnection;
+import java.security.NoSuchAlgorithmException;
+import java.util.concurrent.CompletableFuture;
+import java.util.stream.Collectors;
+import javax.net.ssl.SSLContext;
+import javax.net.ssl.SSLParameters;
+import javax.net.ssl.SSLSession;
+import jdk.incubator.http.HttpClient;
+import jdk.incubator.http.HttpClient.Version;
+import jdk.incubator.http.HttpHeaders;
+import jdk.incubator.http.HttpRequest;
+import jdk.incubator.http.HttpResponse;
+import java.net.InetSocketAddress;
+import java.util.Arrays;
+import java.util.Formatter;
+import java.util.HashMap;
+import java.util.LinkedList;
+import java.util.Random;
+import java.util.concurrent.ExecutorService;
+import java.util.logging.Logger;
+import java.util.logging.Level;
+import jdk.testlibrary.SimpleSSLContext;
+import static jdk.incubator.http.HttpRequest.BodyPublisher.fromByteArray;
+import static jdk.incubator.http.HttpResponse.BodyHandler.asByteArray;
+
+public class ManyRequestsLegacy {
+
+ volatile static int counter = 0;
+
+ public static void main(String[] args) throws Exception {
+ Logger logger = Logger.getLogger("com.sun.net.httpserver");
+ logger.setLevel(Level.ALL);
+ logger.info("TEST");
+ System.out.println("Sending " + REQUESTS
+ + " requests; delay=" + INSERT_DELAY
+ + ", chunks=" + CHUNK_SIZE
+ + ", XFixed=" + XFIXED);
+ SSLContext ctx = new SimpleSSLContext().get();
+ SSLContext.setDefault(ctx);
+ HttpsURLConnection.setDefaultHostnameVerifier(new HostnameVerifier() {
+ public boolean verify(String hostname, SSLSession session) {
+ return true;
+ }
+ });
+ InetSocketAddress addr = new InetSocketAddress(0);
+ HttpsServer server = HttpsServer.create(addr, 0);
+ server.setHttpsConfigurator(new Configurator(ctx));
+
+ LegacyHttpClient client = new LegacyHttpClient();
+
+ try {
+ test(server, client);
+ System.out.println("OK");
+ } finally {
+ server.stop(0);
+ }
+ }
+
+ //static final int REQUESTS = 1000;
+ static final int REQUESTS = 20;
+ static final boolean INSERT_DELAY = Boolean.getBoolean("test.insertDelay");
+ static final int CHUNK_SIZE = Math.max(0,
+ Integer.parseInt(System.getProperty("test.chunkSize", "0")));
+ static final boolean XFIXED = Boolean.getBoolean("test.XFixed");
+
+ static class LegacyHttpClient {
+ static final class LegacyHttpResponse extends HttpResponse<byte[]> {
+ final HttpRequest request;
+ final byte[] response;
+ final int statusCode;
+ public LegacyHttpResponse(HttpRequest request, int statusCode, byte[] response) {
+ this.request = request;
+ this.statusCode = statusCode;
+ this.response = response;
+ }
+ private <T> T error() {
+ throw new UnsupportedOperationException("Not supported yet.");
+ }
+ @Override
+ public int statusCode() { return statusCode;}
+ @Override
+ public HttpRequest request() {return request;}
+ @Override
+ public HttpRequest finalRequest() {return request;}
+ @Override
+ public HttpHeaders headers() { return error(); }
+ @Override
+ public CompletableFuture<HttpHeaders> trailers() { return error(); }
+ @Override
+ public byte[] body() {return response;}
+ @Override
+ public SSLParameters sslParameters() {
+ try {
+ return SSLContext.getDefault().getDefaultSSLParameters();
+ } catch (NoSuchAlgorithmException ex) {
+ throw new UnsupportedOperationException(ex);
+ }
+ }
+ @Override
+ public URI uri() { return request.uri();}
+ @Override
+ public HttpClient.Version version() { return Version.HTTP_1_1;}
+ }
+
+ private void debugCompleted(String tag, long startNanos, HttpRequest req) {
+ System.err.println(tag + " elapsed "
+ + (System.nanoTime() - startNanos)/1000_000L
+ + " millis for " + req.method()
+ + " to " + req.uri());
+ }
+
+ CompletableFuture<? extends HttpResponse<byte[]>> sendAsync(HttpRequest r, byte[] buf) {
+ long start = System.nanoTime();
+ try {
+ CompletableFuture<LegacyHttpResponse> cf = new CompletableFuture<>();
+ URLConnection urlc = r.uri().toURL().openConnection();
+ HttpURLConnection httpc = (HttpURLConnection)urlc;
+ httpc.setRequestMethod(r.method());
+ for (String s : r.headers().map().keySet()) {
+ httpc.setRequestProperty(s, r.headers().allValues(s)
+ .stream().collect(Collectors.joining(",")));
+ }
+ httpc.setDoInput(true);
+ if (buf != null) httpc.setDoOutput(true);
+ Thread t = new Thread(() -> {
+ try {
+ if (buf != null) {
+ try (OutputStream os = httpc.getOutputStream()) {
+ os.write(buf);
+ os.flush();
+ }
+ }
+ LegacyHttpResponse response = new LegacyHttpResponse(r,
+ httpc.getResponseCode(),httpc.getInputStream().readAllBytes());
+ cf.complete(response);
+ } catch(Throwable x) {
+ cf.completeExceptionally(x);
+ }
+ });
+ t.start();
+ return cf.whenComplete((b,x) -> debugCompleted("ClientImpl (async)", start, r));
+ } catch(Throwable t) {
+ debugCompleted("ClientImpl (async)", start, r);
+ return CompletableFuture.failedFuture(t);
+ }
+ }
+ }
+
+ static class TestEchoHandler extends EchoHandler {
+ final Random rand = new Random();
+ @Override
+ public void handle(HttpExchange e) throws IOException {
+ System.out.println("Server: received " + e.getRequestURI());
+ super.handle(e);
+ }
+ @Override
+ protected void close(OutputStream os) throws IOException {
+ if (INSERT_DELAY) {
+ try { Thread.sleep(rand.nextInt(200)); }
+ catch (InterruptedException e) {}
+ }
+ os.close();
+ }
+ @Override
+ protected void close(InputStream is) throws IOException {
+ if (INSERT_DELAY) {
+ try { Thread.sleep(rand.nextInt(200)); }
+ catch (InterruptedException e) {}
+ }
+ is.close();
+ }
+ }
+
+ static void test(HttpsServer server, LegacyHttpClient client) throws Exception {
+ int port = server.getAddress().getPort();
+ URI baseURI = new URI("https://127.0.0.1:" + port + "/foo/x");
+ server.createContext("/foo", new TestEchoHandler());
+ server.start();
+
+ RequestLimiter limiter = new RequestLimiter(40);
+ Random rand = new Random();
+ CompletableFuture<?>[] results = new CompletableFuture<?>[REQUESTS];
+ HashMap<HttpRequest,byte[]> bodies = new HashMap<>();
+
+ for (int i=0; i<REQUESTS; i++) {
+ byte[] buf = new byte[(i+1)*CHUNK_SIZE+i+1]; // different size bodies
+ rand.nextBytes(buf);
+ URI uri = new URI(baseURI.toString() + String.valueOf(i+1));
+ HttpRequest r = HttpRequest.newBuilder(uri)
+ .header("XFixed", "true")
+ .POST(fromByteArray(buf))
+ .build();
+ bodies.put(r, buf);
+
+ results[i] =
+ limiter.whenOkToSend()
+ .thenCompose((v) -> {
+ System.out.println("Client: sendAsync: " + r.uri());
+ return client.sendAsync(r, buf);
+ })
+ .thenCompose((resp) -> {
+ limiter.requestComplete();
+ if (resp.statusCode() != 200) {
+ String s = "Expected 200, got: " + resp.statusCode();
+ System.out.println(s + " from "
+ + resp.request().uri().getPath());
+ return completedWithIOException(s);
+ } else {
+ counter++;
+ System.out.println("Result (" + counter + ") from "
+ + resp.request().uri().getPath());
+ }
+ return CompletableFuture.completedStage(resp.body())
+ .thenApply((b) -> new Pair<>(resp, b));
+ })
+ .thenAccept((pair) -> {
+ HttpRequest request = pair.t.request();
+ byte[] requestBody = bodies.get(request);
+ check(Arrays.equals(requestBody, pair.u),
+ "bodies not equal:[" + bytesToHexString(requestBody)
+ + "] [" + bytesToHexString(pair.u) + "]");
+
+ });
+ }
+
+ // wait for them all to complete and throw exception in case of error
+ CompletableFuture.allOf(results).join();
+ }
+
+ static <T> CompletableFuture<T> completedWithIOException(String message) {
+ return CompletableFuture.failedFuture(new IOException(message));
+ }
+
+ static String bytesToHexString(byte[] bytes) {
+ if (bytes == null)
+ return "null";
+
+ StringBuilder sb = new StringBuilder(bytes.length * 2);
+
+ Formatter formatter = new Formatter(sb);
+ for (byte b : bytes) {
+ formatter.format("%02x", b);
+ }
+
+ return sb.toString();
+ }
+
+ static final class Pair<T,U> {
+ Pair(T t, U u) {
+ this.t = t; this.u = u;
+ }
+ T t;
+ U u;
+ }
+
+ /**
+ * A simple limiter for controlling the number of requests to be run in
+ * parallel whenOkToSend() is called which returns a CF<Void> that allows
+ * each individual request to proceed, or block temporarily (blocking occurs
+ * on the waiters list here. As each request actually completes
+ * requestComplete() is called to notify this object, and allow some
+ * requests to continue.
+ */
+ static class RequestLimiter {
+
+ static final CompletableFuture<Void> COMPLETED_FUTURE =
+ CompletableFuture.completedFuture(null);
+
+ final int maxnumber;
+ final LinkedList<CompletableFuture<Void>> waiters;
+ int number;
+ boolean blocked;
+
+ RequestLimiter(int maximum) {
+ waiters = new LinkedList<>();
+ maxnumber = maximum;
+ }
+
+ synchronized void requestComplete() {
+ number--;
+ // don't unblock until number of requests has halved.
+ if ((blocked && number <= maxnumber / 2) ||
+ (!blocked && waiters.size() > 0)) {
+ int toRelease = Math.min(maxnumber - number, waiters.size());
+ for (int i=0; i<toRelease; i++) {
+ CompletableFuture<Void> f = waiters.remove();
+ number ++;
+ f.complete(null);
+ }
+ blocked = number >= maxnumber;
+ }
+ }
+
+ synchronized CompletableFuture<Void> whenOkToSend() {
+ if (blocked || number + 1 >= maxnumber) {
+ blocked = true;
+ CompletableFuture<Void> r = new CompletableFuture<>();
+ waiters.add(r);
+ return r;
+ } else {
+ number++;
+ return COMPLETED_FUTURE;
+ }
+ }
+ }
+
+ static void check(boolean cond, Object... msg) {
+ if (cond)
+ return;
+ StringBuilder sb = new StringBuilder();
+ for (Object o : msg)
+ sb.append(o);
+ throw new RuntimeException(sb.toString());
+ }
+
+ static class Configurator extends HttpsConfigurator {
+ public Configurator(SSLContext ctx) {
+ super(ctx);
+ }
+
+ public void configure(HttpsParameters params) {
+ params.setSSLParameters(getSSLContext().getSupportedSSLParameters());
+ }
+ }
+}
--- /dev/null Thu Jan 01 00:00:00 1970 +0000
+++ b/test/jdk/java/net/httpclient/MockServer.java Sun Nov 05 17:32:13 2017 +0000
@@ -0,0 +1,317 @@
+/*
+ * Copyright (c) 2015, 2017, Oracle and/or its affiliates. All rights reserved.
+ * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER.
+ *
+ * This code is free software; you can redistribute it and/or modify it
+ * under the terms of the GNU General Public License version 2 only, as
+ * published by the Free Software Foundation.
+ *
+ * This code is distributed in the hope that it will be useful, but WITHOUT
+ * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or
+ * FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License
+ * version 2 for more details (a copy is included in the LICENSE file that
+ * accompanied this code).
+ *
+ * You should have received a copy of the GNU General Public License version
+ * 2 along with this work; if not, write to the Free Software Foundation,
+ * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA.
+ *
+ * Please contact Oracle, 500 Oracle Parkway, Redwood Shores, CA 94065 USA
+ * or visit www.oracle.com if you need additional information or have any
+ * questions.
+ */
+
+import java.io.Closeable;
+import java.io.IOException;
+import java.io.InputStream;
+import java.io.OutputStream;
+import javax.net.ServerSocketFactory;
+import javax.net.ssl.SSLServerSocket;
+import java.net.ServerSocket;
+import java.net.Socket;
+import java.nio.charset.StandardCharsets;
+import java.util.Collections;
+import java.util.LinkedList;
+import java.util.List;
+import java.util.Iterator;
+import java.util.concurrent.ArrayBlockingQueue;
+import java.util.concurrent.TimeUnit;
+import java.util.concurrent.atomic.AtomicInteger;
+
+/**
+ * A cut-down Http/1 Server for testing various error situations
+ *
+ * use interrupt() to halt
+ */
+public class MockServer extends Thread implements Closeable {
+
+ ServerSocket ss;
+ private final List<Connection> sockets;
+ private final List<Connection> removals;
+ private final List<Connection> additions;
+ AtomicInteger counter = new AtomicInteger(0);
+
+ // waits up to 20 seconds for something to happen
+ // dont use this unless certain activity coming.
+ public Connection activity() {
+ for (int i = 0; i < 80 * 100; i++) {
+ doRemovalsAndAdditions();
+ for (Connection c : sockets) {
+ if (c.poll()) {
+ return c;
+ }
+ }
+ try {
+ Thread.sleep(250);
+ } catch (InterruptedException e) {
+ e.printStackTrace();
+ }
+ }
+ return null;
+ }
+
+ private void doRemovalsAndAdditions() {
+ synchronized (removals) {
+ Iterator<Connection> i = removals.iterator();
+ while (i.hasNext()) {
+ Connection c = i.next();
+ System.out.println("socket removed: " + c);
+ sockets.remove(c);
+ }
+ removals.clear();
+ }
+
+ synchronized (additions) {
+ Iterator<Connection> i = additions.iterator();
+ while (i.hasNext()) {
+ Connection c = i.next();
+ System.out.println("socket added: " + c);
+ sockets.add(c);
+ }
+ additions.clear();
+ }
+ }
+
+ // clears all current connections on Server.
+ public void reset() {
+ for (Connection c : sockets) {
+ c.close();
+ }
+ }
+
+ /**
+ * Reads data into an ArrayBlockingQueue<String> where each String
+ * is a line of input, that was terminated by CRLF (not included)
+ */
+ class Connection extends Thread {
+ Connection(Socket s) throws IOException {
+ this.socket = s;
+ id = counter.incrementAndGet();
+ is = s.getInputStream();
+ os = s.getOutputStream();
+ incoming = new ArrayBlockingQueue<>(100);
+ setName("Server-Connection");
+ setDaemon(true);
+ }
+ final Socket socket;
+ final int id;
+ final InputStream is;
+ final OutputStream os;
+ final ArrayBlockingQueue<String> incoming;
+
+ final static String CRLF = "\r\n";
+
+ // sentinel indicating connection closed
+ final static String CLOSED = "C.L.O.S.E.D";
+ volatile boolean closed = false;
+ volatile boolean released = false;
+
+ @Override
+ public void run() {
+ byte[] buf = new byte[256];
+ String s = "";
+ try {
+ while (true) {
+ int n = is.read(buf);
+ if (n == -1) {
+ cleanup();
+ return;
+ }
+ String s0 = new String(buf, 0, n, StandardCharsets.ISO_8859_1);
+ s = s + s0;
+ int i;
+ while ((i=s.indexOf(CRLF)) != -1) {
+ String s1 = s.substring(0, i+2);
+ System.out.println("Server got: " + s1.substring(0,i));
+ incoming.put(s1);
+ if (i+2 == s.length()) {
+ s = "";
+ break;
+ }
+ s = s.substring(i+2);
+ }
+ }
+ } catch (IOException |InterruptedException e1) {
+ cleanup();
+ } catch (Throwable t) {
+ System.out.println("X: " + t);
+ cleanup();
+ }
+ }
+
+ @Override
+ public String toString() {
+ return "Server.Connection: " + socket.toString();
+ }
+
+ public void sendHttpResponse(int code, String body, String... headers)
+ throws IOException
+ {
+ String r1 = "HTTP/1.1 " + Integer.toString(code) + " status" + CRLF;
+ for (int i=0; i<headers.length; i+=2) {
+ r1 += headers[i] + ": " + headers[i+1] + CRLF;
+ }
+ int clen = body == null ? 0 : body.length();
+ r1 += "Content-Length: " + Integer.toString(clen) + CRLF;
+ r1 += CRLF;
+ if (body != null) {
+ r1 += body;
+ }
+ send(r1);
+ }
+
+ // content-length is 10 bytes too many
+ public void sendIncompleteHttpResponseBody(int code) throws IOException {
+ String body = "Hello World Helloworld Goodbye World";
+ String r1 = "HTTP/1.1 " + Integer.toString(code) + " status" + CRLF;
+ int clen = body.length() + 10;
+ r1 += "Content-Length: " + Integer.toString(clen) + CRLF;
+ r1 += CRLF;
+ if (body != null) {
+ r1 += body;
+ }
+ send(r1);
+ }
+
+ public void sendIncompleteHttpResponseHeaders(int code)
+ throws IOException
+ {
+ String r1 = "HTTP/1.1 " + Integer.toString(code) + " status" + CRLF;
+ send(r1);
+ }
+
+ public void send(String r) throws IOException {
+ os.write(r.getBytes(StandardCharsets.ISO_8859_1));
+ }
+
+ public synchronized void close() {
+ cleanup();
+ closed = true;
+ incoming.clear();
+ }
+
+ public String nextInput(long timeout, TimeUnit unit) {
+ String result = "";
+ while (poll()) {
+ try {
+ String s = incoming.poll(timeout, unit);
+ if (s == null && closed) {
+ return CLOSED;
+ } else {
+ result += s;
+ }
+ } catch (InterruptedException e) {
+ return null;
+ }
+ }
+ return result;
+ }
+
+ public String nextInput() {
+ return nextInput(0, TimeUnit.SECONDS);
+ }
+
+ public boolean poll() {
+ return incoming.peek() != null;
+ }
+
+ private void cleanup() {
+ if (released) return;
+ synchronized(this) {
+ if (released) return;
+ released = true;
+ }
+ try {
+ socket.close();
+ } catch (IOException e) {}
+ synchronized (removals) {
+ removals.add(this);
+ }
+ }
+ }
+
+ MockServer(int port, ServerSocketFactory factory) throws IOException {
+ ss = factory.createServerSocket(port);
+ sockets = Collections.synchronizedList(new LinkedList<>());
+ removals = new LinkedList<>();
+ additions = new LinkedList<>();
+ setName("Test-Server");
+ setDaemon(true);
+ }
+
+ MockServer(int port) throws IOException {
+ this(port, ServerSocketFactory.getDefault());
+ }
+
+ MockServer() throws IOException {
+ this(0);
+ }
+
+ int port() {
+ return ss.getLocalPort();
+ }
+
+ public String getURL() {
+ if (ss instanceof SSLServerSocket) {
+ return "https://127.0.0.1:" + port() + "/foo/";
+ } else {
+ return "http://127.0.0.1:" + port() + "/foo/";
+ }
+ }
+
+ private volatile boolean closed;
+
+ @Override
+ public void close() {
+ closed = true;
+ try {
+ ss.close();
+ } catch (IOException e) {
+ e.printStackTrace();
+ }
+ for (Connection c : sockets) {
+ c.close();
+ }
+ }
+
+ @Override
+ public void run() {
+ while (!closed) {
+ try {
+ System.out.println("Server waiting for connection");
+ Socket s = ss.accept();
+ Connection c = new Connection(s);
+ c.start();
+ System.out.println("Server got new connection: " + c);
+ synchronized (additions) {
+ additions.add(c);
+ }
+ } catch (IOException e) {
+ if (closed)
+ return;
+ e.printStackTrace();
+ }
+ }
+ }
+
+}
--- a/test/jdk/java/net/httpclient/MultiAuthTest.java Sun Nov 05 17:05:57 2017 +0000
+++ b/test/jdk/java/net/httpclient/MultiAuthTest.java Sun Nov 05 17:32:13 2017 +0000
@@ -46,7 +46,7 @@
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
import static java.nio.charset.StandardCharsets.US_ASCII;
-import static jdk.incubator.http.HttpRequest.BodyProcessor.fromString;
+import static jdk.incubator.http.HttpRequest.BodyPublisher.fromString;
import static jdk.incubator.http.HttpResponse.BodyHandler.asString;
import java.util.UUID;
import java.util.concurrent.atomic.AtomicInteger;
--- a/test/jdk/java/net/httpclient/RequestBodyTest.java Sun Nov 05 17:05:57 2017 +0000
+++ b/test/jdk/java/net/httpclient/RequestBodyTest.java Sun Nov 05 17:32:13 2017 +0000
@@ -22,7 +22,8 @@
*/
/*
- * @test @bug 8087112
+ * @test
+ * @bug 8087112
* @modules jdk.incubator.httpclient
* java.logging
* jdk.httpserver
@@ -42,6 +43,7 @@
import jdk.incubator.http.HttpClient;
import jdk.incubator.http.HttpRequest;
import jdk.incubator.http.HttpResponse;
+import jdk.incubator.http.HttpResponse.BodyHandler;
import java.nio.charset.Charset;
import java.nio.charset.StandardCharsets;
import java.nio.file.Files;
@@ -51,14 +53,15 @@
import java.util.Arrays;
import java.util.List;
import java.util.Optional;
-import java.util.concurrent.ExecutorService;
-import java.util.concurrent.Executors;
+import java.util.concurrent.ConcurrentHashMap;
+import java.util.function.Consumer;
import java.util.function.Supplier;
import javax.net.ssl.SSLContext;
import jdk.test.lib.util.FileUtils;
+import static java.lang.System.out;
import static java.nio.charset.StandardCharsets.*;
import static java.nio.file.StandardOpenOption.*;
-import static jdk.incubator.http.HttpRequest.BodyProcessor.*;
+import static jdk.incubator.http.HttpRequest.BodyPublisher.*;
import static jdk.incubator.http.HttpResponse.BodyHandler.*;
import org.testng.annotations.AfterTest;
@@ -69,12 +72,12 @@
public class RequestBodyTest {
- static final String fileroot = System.getProperty("test.src") + "/docs";
+ static final String fileroot = System.getProperty("test.src", ".") + "/docs";
static final String midSizedFilename = "/files/notsobigfile.txt";
static final String smallFilename = "/files/smallfile.txt";
+ final ConcurrentHashMap<String,Throwable> failures = new ConcurrentHashMap<>();
HttpClient client;
- ExecutorService exec = Executors.newCachedThreadPool();
String httpURI;
String httpsURI;
@@ -109,14 +112,25 @@
.sslContext(ctx)
.version(HttpClient.Version.HTTP_1_1)
.followRedirects(HttpClient.Redirect.ALWAYS)
- .executor(exec)
.build();
}
@AfterTest
public void teardown() throws Exception {
- exec.shutdownNow();
- LightWeightHttpServer.stop();
+ try {
+ LightWeightHttpServer.stop();
+ } finally {
+ System.out.println("RequestBodyTest: " + failures.size() + " failures");
+ int i = 0;
+ for (String key: failures.keySet()) {
+ System.out.println("test" + key + " failed: " + failures.get(key));
+ failures.get(key).printStackTrace(System.out);
+ if (i++ > 3) {
+ System.out.println("..... other failures not printed ....");
+ break;
+ }
+ }
+ }
}
@DataProvider
@@ -128,8 +142,9 @@
for (String file : new String[] { smallFilename, midSizedFilename })
for (RequestBody requestBodyType : RequestBody.values())
for (ResponseBody responseBodyType : ResponseBody.values())
- values.add(new Object[]
- {uri, requestBodyType, responseBodyType, file, async});
+ for (boolean bufferResponseBody : new boolean[] { false, true })
+ values.add(new Object[]
+ {uri, requestBodyType, responseBodyType, file, async, bufferResponseBody});
return values.stream().toArray(Object[][]::new);
}
@@ -139,15 +154,25 @@
RequestBody requestBodyType,
ResponseBody responseBodyType,
String file,
- boolean async)
+ boolean async,
+ boolean bufferResponseBody)
throws Exception
{
- Path filePath = Paths.get(fileroot + file);
- URI uri = new URI(target);
+ try {
+ Path filePath = Paths.get(fileroot + file);
+ URI uri = new URI(target);
+
+ HttpRequest request = createRequest(uri, requestBodyType, filePath);
- HttpRequest request = createRequest(uri, requestBodyType, filePath);
-
- checkResponse(client, request, requestBodyType, responseBodyType, filePath, async);
+ checkResponse(client, request, requestBodyType, responseBodyType, filePath, async, bufferResponseBody);
+ } catch (Exception | Error x) {
+ Object[] params = new Object[] {
+ target, requestBodyType, responseBodyType,
+ file, "async=" + async, "buffer=" + bufferResponseBody
+ };
+ failures.put(java.util.Arrays.toString(params), x);
+ throw x;
+ }
}
static final int DEFAULT_OFFSET = 10;
@@ -198,7 +223,8 @@
RequestBody requestBodyType,
ResponseBody responseBodyType,
Path file,
- boolean async)
+ boolean async,
+ boolean bufferResponseBody)
throws InterruptedException, IOException
{
String filename = file.toFile().getAbsolutePath();
@@ -215,43 +241,57 @@
switch (responseBodyType) {
case BYTE_ARRAY:
- HttpResponse<byte[]> bar = getResponse(client, request, asByteArray(), async);
+ BodyHandler<byte[]> bh = asByteArray();
+ if (bufferResponseBody) bh = buffering(bh, 50);
+ HttpResponse<byte[]> bar = getResponse(client, request, bh, async);
assertEquals(bar.statusCode(), 200);
assertEquals(bar.body(), fileAsBytes);
break;
case BYTE_ARRAY_CONSUMER:
ByteArrayOutputStream baos = new ByteArrayOutputStream();
- HttpResponse<Void> v = getResponse(client, request,
- asByteArrayConsumer(o -> consumerBytes(o, baos) ), async);
+ Consumer<Optional<byte[]>> consumer = o -> consumerBytes(o, baos);
+ BodyHandler<Void> bh1 = asByteArrayConsumer(consumer);
+ if (bufferResponseBody) bh1 = buffering(bh1, 49);
+ HttpResponse<Void> v = getResponse(client, request, bh1, async);
byte[] ba = baos.toByteArray();
assertEquals(v.statusCode(), 200);
assertEquals(ba, fileAsBytes);
break;
case DISCARD:
Object o = new Object();
- HttpResponse<Object> or = getResponse(client, request, discard(o), async);
+ BodyHandler<Object> bh2 = discard(o);
+ if (bufferResponseBody) bh2 = buffering(bh2, 51);
+ HttpResponse<Object> or = getResponse(client, request, bh2, async);
assertEquals(or.statusCode(), 200);
assertSame(or.body(), o);
break;
case FILE:
- HttpResponse<Path> fr = getResponse(client, request, asFile(tempFile), async);
+ BodyHandler<Path> bh3 = asFile(tempFile);
+ if (bufferResponseBody) bh3 = buffering(bh3, 48);
+ HttpResponse<Path> fr = getResponse(client, request, bh3, async);
assertEquals(fr.statusCode(), 200);
assertEquals(Files.size(tempFile), fileAsString.length());
assertEquals(Files.readAllBytes(tempFile), fileAsBytes);
break;
case FILE_WITH_OPTION:
- fr = getResponse(client, request, asFile(tempFile, CREATE_NEW, WRITE), async);
+ BodyHandler<Path> bh4 = asFile(tempFile, CREATE_NEW, WRITE);
+ if (bufferResponseBody) bh4 = buffering(bh4, 52);
+ fr = getResponse(client, request, bh4, async);
assertEquals(fr.statusCode(), 200);
assertEquals(Files.size(tempFile), fileAsString.length());
assertEquals(Files.readAllBytes(tempFile), fileAsBytes);
break;
case STRING:
- HttpResponse<String> sr = getResponse(client, request, asString(), async);
+ BodyHandler<String> bh5 = asString();
+ if(bufferResponseBody) bh5 = buffering(bh5, 47);
+ HttpResponse<String> sr = getResponse(client, request, bh5, async);
assertEquals(sr.statusCode(), 200);
assertEquals(sr.body(), fileAsString);
break;
case STRING_WITH_CHARSET:
- HttpResponse<String> r = getResponse(client, request, asString(StandardCharsets.UTF_8), async);
+ BodyHandler<String> bh6 = asString(StandardCharsets.UTF_8);
+ if (bufferResponseBody) bh6 = buffering(bh6, 53);
+ HttpResponse<String> r = getResponse(client, request, bh6, async);
assertEquals(r.statusCode(), 200);
assertEquals(r.body(), fileAsString);
break;
@@ -303,4 +343,28 @@
throw new UncheckedIOException(x);
}
}
+
+ // ---
+
+ /* Main entry point for standalone testing of the main functional test. */
+ public static void main(String... args) throws Exception {
+ RequestBodyTest t = new RequestBodyTest();
+ t.setup();
+ int count = 0;
+ try {
+ for (Object[] objs : t.exchanges()) {
+ count++;
+ out.printf("********* iteration: %d %s %s %s %s %s %s *********%n",
+ count, objs[0], objs[1], objs[2], objs[3], objs[4], objs[5]);
+ t.exchange((String) objs[0],
+ (RequestBody) objs[1],
+ (ResponseBody) objs[2],
+ (String) objs[3],
+ (boolean) objs[4],
+ (boolean) objs[5]);
+ }
+ } finally {
+ t.teardown();
+ }
+ }
}
--- /dev/null Thu Jan 01 00:00:00 1970 +0000
+++ b/test/jdk/java/net/httpclient/RequestBuilderTest.java Sun Nov 05 17:32:13 2017 +0000
@@ -0,0 +1,341 @@
+/*
+ * Copyright (c) 2017, Oracle and/or its affiliates. All rights reserved.
+ * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER.
+ *
+ * This code is free software; you can redistribute it and/or modify it
+ * under the terms of the GNU General Public License version 2 only, as
+ * published by the Free Software Foundation.
+ *
+ * This code is distributed in the hope that it will be useful, but WITHOUT
+ * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or
+ * FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License
+ * version 2 for more details (a copy is included in the LICENSE file that
+ * accompanied this code).
+ *
+ * You should have received a copy of the GNU General Public License version
+ * 2 along with this work; if not, write to the Free Software Foundation,
+ * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA.
+ *
+ * Please contact Oracle, 500 Oracle Parkway, Redwood Shores, CA 94065 USA
+ * or visit www.oracle.com if you need additional information or have any
+ * questions.
+ */
+
+/*
+ * @test
+ * @summary HttpRequest[.Builder] API and behaviour checks
+ * @run testng RequestBuilderTest
+ */
+
+import java.net.URI;
+import java.util.List;
+import jdk.incubator.http.HttpRequest;
+import static java.time.Duration.ofNanos;
+import static java.time.Duration.ofMinutes;
+import static java.time.Duration.ofSeconds;
+import static java.time.Duration.ZERO;
+import static jdk.incubator.http.HttpClient.Version.HTTP_1_1;
+import static jdk.incubator.http.HttpClient.Version.HTTP_2;
+import static jdk.incubator.http.HttpRequest.BodyPublisher.fromString;
+import static jdk.incubator.http.HttpRequest.newBuilder;
+import static org.testng.Assert.*;
+import org.testng.annotations.Test;
+
+public class RequestBuilderTest {
+
+ static final URI uri = URI.create("http://foo.com/");
+ static final Class<NullPointerException> NPE = NullPointerException.class;
+ static final Class<IllegalArgumentException> IAE = IllegalArgumentException.class;
+ static final Class<IllegalStateException> ISE = IllegalStateException.class;
+ static final Class<NumberFormatException> NFE = NumberFormatException.class;
+ static final Class<UnsupportedOperationException> UOE = UnsupportedOperationException.class;
+
+ @Test
+ public void testDefaults() {
+ List<HttpRequest.Builder> builders = List.of(newBuilder().uri(uri),
+ newBuilder(uri));
+ for (HttpRequest.Builder builder : builders) {
+ assertFalse(builder.build().expectContinue());
+ assertEquals(builder.build().method(), "GET");
+ assertFalse(builder.build().bodyPublisher().isPresent());
+ assertFalse(builder.build().version().isPresent());
+ assertFalse(builder.build().timeout().isPresent());
+ assertTrue(builder.build().headers() != null);
+ assertEquals(builder.build().headers().map().size(), 0);
+ }
+ }
+
+ @Test
+ public void testNull() {
+ HttpRequest.Builder builder = newBuilder();
+
+ assertThrows(NPE, () -> newBuilder(null).build());
+ assertThrows(NPE, () -> newBuilder(uri).uri(null).build());
+ assertThrows(NPE, () -> builder.uri(null));
+ assertThrows(NPE, () -> builder.version(null));
+ assertThrows(NPE, () -> builder.header(null, null));
+ assertThrows(NPE, () -> builder.header("name", null));
+ assertThrows(NPE, () -> builder.header(null, "value"));
+ assertThrows(NPE, () -> builder.headers(null));
+ assertThrows(NPE, () -> builder.headers(new String[] { null, null }));
+ assertThrows(NPE, () -> builder.headers(new String[] { "name", null }));
+ assertThrows(NPE, () -> builder.headers(new String[] { null, "value" }));
+ assertThrows(NPE, () -> builder.method(null, null));
+ assertThrows(NPE, () -> builder.method("GET", null));
+ assertThrows(NPE, () -> builder.method("POST", null));
+ assertThrows(NPE, () -> builder.method("PUT", null));
+ assertThrows(NPE, () -> builder.method("DELETE", null));
+ assertThrows(NPE, () -> builder.setHeader(null, null));
+ assertThrows(NPE, () -> builder.setHeader("name", null));
+ assertThrows(NPE, () -> builder.setHeader(null, "value"));
+ assertThrows(NPE, () -> builder.timeout(null));
+ assertThrows(NPE, () -> builder.DELETE(null));
+ assertThrows(NPE, () -> builder.POST(null));
+ assertThrows(NPE, () -> builder.PUT(null));
+ }
+
+ @Test
+ public void testURI() {
+ assertThrows(ISE, () -> newBuilder().build());
+ List<URI> uris = List.of(
+ URI.create("ws://foo.com"),
+ URI.create("wss://foo.com"),
+ URI.create("ftp://foo.com"),
+ URI.create("gopher://foo.com"),
+ URI.create("mailto:a@b.com"),
+ URI.create("scheme:example.com"),
+ URI.create("scheme:example.com"),
+ URI.create("scheme:example.com/path"),
+ URI.create("path"),
+ URI.create("/path")
+ );
+ for (URI u : uris) {
+ assertThrows(IAE, () -> newBuilder(u));
+ assertThrows(IAE, () -> newBuilder().uri(u));
+ }
+
+ assertEquals(newBuilder(uri).build().uri(), uri);
+ assertEquals(newBuilder().uri(uri).build().uri(), uri);
+ URI https = URI.create("https://foo.com");
+ assertEquals(newBuilder(https).build().uri(), https);
+ assertEquals(newBuilder().uri(https).build().uri(), https);
+ }
+
+ @Test
+ public void testMethod() {
+ HttpRequest request = newBuilder(uri).build();
+ assertEquals(request.method(), "GET");
+ assertTrue(!request.bodyPublisher().isPresent());
+
+ request = newBuilder(uri).GET().build();
+ assertEquals(request.method(), "GET");
+ assertTrue(!request.bodyPublisher().isPresent());
+
+ request = newBuilder(uri).POST(fromString("")).GET().build();
+ assertEquals(request.method(), "GET");
+ assertTrue(!request.bodyPublisher().isPresent());
+
+ request = newBuilder(uri).PUT(fromString("")).GET().build();
+ assertEquals(request.method(), "GET");
+ assertTrue(!request.bodyPublisher().isPresent());
+
+ request = newBuilder(uri).DELETE(fromString("")).GET().build();
+ assertEquals(request.method(), "GET");
+ assertTrue(!request.bodyPublisher().isPresent());
+
+ request = newBuilder(uri).POST(fromString("")).build();
+ assertEquals(request.method(), "POST");
+ assertTrue(request.bodyPublisher().isPresent());
+
+ request = newBuilder(uri).PUT(fromString("")).build();
+ assertEquals(request.method(), "PUT");
+ assertTrue(request.bodyPublisher().isPresent());
+
+ request = newBuilder(uri).DELETE(fromString("")).build();
+ assertEquals(request.method(), "DELETE");
+ assertTrue(request.bodyPublisher().isPresent());
+
+ request = newBuilder(uri).GET().POST(fromString("")).build();
+ assertEquals(request.method(), "POST");
+ assertTrue(request.bodyPublisher().isPresent());
+
+ request = newBuilder(uri).GET().PUT(fromString("")).build();
+ assertEquals(request.method(), "PUT");
+ assertTrue(request.bodyPublisher().isPresent());
+
+ request = newBuilder(uri).GET().DELETE(fromString("")).build();
+ assertEquals(request.method(), "DELETE");
+ assertTrue(request.bodyPublisher().isPresent());
+ }
+
+ @Test
+ public void testHeaders() {
+ HttpRequest.Builder builder = newBuilder(uri);
+
+ String[] empty = new String[0];
+ assertThrows(IAE, () -> builder.headers(empty).build());
+ assertThrows(IAE, () -> builder.headers("1").build());
+ assertThrows(IAE, () -> builder.headers("1", "2", "3").build());
+ assertThrows(IAE, () -> builder.headers("1", "2", "3", "4", "5").build());
+ assertEquals(builder.build().headers().map().size(),0);
+
+ List<HttpRequest> requests = List.of(
+ // same header built from different combinations of the API
+ newBuilder(uri).header("A", "B").build(),
+ newBuilder(uri).headers("A", "B").build(),
+ newBuilder(uri).setHeader("A", "B").build(),
+ newBuilder(uri).header("A", "F").setHeader("A", "B").build(),
+ newBuilder(uri).headers("A", "F").setHeader("A", "B").build()
+ );
+
+ for (HttpRequest r : requests) {
+ assertEquals(r.headers().map().size(), 1);
+ assertTrue(r.headers().firstValue("A").isPresent());
+ assertEquals(r.headers().firstValue("A").get(), "B");
+ assertEquals(r.headers().allValues("A"), List.of("B"));
+ assertEquals(r.headers().allValues("C").size(), 0);
+ assertEquals(r.headers().map().get("A"), List.of("B"));
+ assertThrows(NFE, () -> r.headers().firstValueAsLong("A"));
+ assertFalse(r.headers().firstValue("C").isPresent());
+ // a non-exhaustive list of mutators
+ assertThrows(UOE, () -> r.headers().map().put("Z", List.of("Z")));
+ assertThrows(UOE, () -> r.headers().map().remove("A"));
+ assertThrows(UOE, () -> r.headers().map().remove("A", "B"));
+ assertThrows(UOE, () -> r.headers().map().clear());
+ assertThrows(UOE, () -> r.headers().allValues("A").remove("B"));
+ assertThrows(UOE, () -> r.headers().allValues("A").remove(1));
+ assertThrows(UOE, () -> r.headers().allValues("A").clear());
+ assertThrows(UOE, () -> r.headers().allValues("A").add("Z"));
+ assertThrows(UOE, () -> r.headers().allValues("A").addAll(List.of("Z")));
+ assertThrows(UOE, () -> r.headers().allValues("A").add(1, "Z"));
+ }
+
+ requests = List.of(
+ // same headers built from different combinations of the API
+ newBuilder(uri).header("A", "B")
+ .header("C", "D").build(),
+ newBuilder(uri).header("A", "B")
+ .headers("C", "D").build(),
+ newBuilder(uri).header("A", "B")
+ .setHeader("C", "D").build(),
+ newBuilder(uri).headers("A", "B")
+ .headers("C", "D").build(),
+ newBuilder(uri).headers("A", "B")
+ .header("C", "D").build(),
+ newBuilder(uri).headers("A", "B")
+ .setHeader("C", "D").build(),
+ newBuilder(uri).setHeader("A", "B")
+ .setHeader("C", "D").build(),
+ newBuilder(uri).setHeader("A", "B")
+ .header("C", "D").build(),
+ newBuilder(uri).setHeader("A", "B")
+ .headers("C", "D").build(),
+ newBuilder(uri).headers("A", "B", "C", "D").build()
+ );
+
+ for (HttpRequest r : requests) {
+ assertEquals(r.headers().map().size(), 2);
+ assertTrue(r.headers().firstValue("A").isPresent());
+ assertEquals(r.headers().firstValue("A").get(), "B");
+ assertEquals(r.headers().allValues("A"), List.of("B"));
+ assertTrue(r.headers().firstValue("C").isPresent());
+ assertEquals(r.headers().firstValue("C").get(), "D");
+ assertEquals(r.headers().allValues("C"), List.of("D"));
+ assertEquals(r.headers().map().get("C"), List.of("D"));
+ assertThrows(NFE, () -> r.headers().firstValueAsLong("C"));
+ assertFalse(r.headers().firstValue("E").isPresent());
+ // a smaller non-exhaustive list of mutators
+ assertThrows(UOE, () -> r.headers().map().put("Z", List.of("Z")));
+ assertThrows(UOE, () -> r.headers().map().remove("C"));
+ assertThrows(UOE, () -> r.headers().allValues("A").remove("B"));
+ assertThrows(UOE, () -> r.headers().allValues("A").clear());
+ assertThrows(UOE, () -> r.headers().allValues("C").add("Z"));
+ }
+
+ requests = List.of(
+ // same multi-value headers built from different combinations of the API
+ newBuilder(uri).header("A", "B")
+ .header("A", "C").build(),
+ newBuilder(uri).header("A", "B")
+ .headers("A", "C").build(),
+ newBuilder(uri).headers("A", "B")
+ .headers("A", "C").build(),
+ newBuilder(uri).headers("A", "B")
+ .header("A", "C").build(),
+ newBuilder(uri).setHeader("A", "B")
+ .header("A", "C").build(),
+ newBuilder(uri).setHeader("A", "B")
+ .headers("A", "C").build(),
+ newBuilder(uri).header("A", "D")
+ .setHeader("A", "B")
+ .headers("A", "C").build(),
+ newBuilder(uri).headers("A", "B", "A", "C").build()
+ );
+
+ for (HttpRequest r : requests) {
+ assertEquals(r.headers().map().size(), 1);
+ assertTrue(r.headers().firstValue("A").isPresent());
+ assertTrue(r.headers().allValues("A").containsAll(List.of("B", "C")));
+ assertEquals(r.headers().allValues("C").size(), 0);
+ assertEquals(r.headers().map().get("A"), List.of("B", "C"));
+ assertThrows(NFE, () -> r.headers().firstValueAsLong("A"));
+ assertFalse(r.headers().firstValue("C").isPresent());
+ // a non-exhaustive list of mutators
+ assertThrows(UOE, () -> r.headers().map().put("Z", List.of("Z")));
+ assertThrows(UOE, () -> r.headers().map().remove("A"));
+ assertThrows(UOE, () -> r.headers().map().remove("A", "B"));
+ assertThrows(UOE, () -> r.headers().map().clear());
+ assertThrows(UOE, () -> r.headers().allValues("A").remove("B"));
+ assertThrows(UOE, () -> r.headers().allValues("A").remove(1));
+ assertThrows(UOE, () -> r.headers().allValues("A").clear());
+ assertThrows(UOE, () -> r.headers().allValues("A").add("Z"));
+ assertThrows(UOE, () -> r.headers().allValues("A").addAll(List.of("Z")));
+ assertThrows(UOE, () -> r.headers().allValues("A").add(1, "Z"));
+ }
+ }
+
+ @Test
+ public void testCopy() {
+ HttpRequest.Builder builder = newBuilder(uri).expectContinue(true)
+ .header("A", "B")
+ .POST(fromString(""))
+ .timeout(ofSeconds(30))
+ .version(HTTP_1_1);
+ HttpRequest.Builder copy = builder.copy();
+ assertTrue(builder != copy);
+
+ // modify the original builder before building from the copy
+ builder.GET().timeout(ofSeconds(5)).version(HTTP_2).setHeader("A", "C");
+
+ HttpRequest copyRequest = copy.build();
+ assertEquals(copyRequest.uri(), uri);
+ assertEquals(copyRequest.expectContinue(), true);
+ assertEquals(copyRequest.headers().map().get("A"), List.of("B"));
+ assertEquals(copyRequest.method(), "POST");
+ assertEquals(copyRequest.bodyPublisher().isPresent(), true);
+ assertEquals(copyRequest.timeout().get(), ofSeconds(30));
+ assertTrue(copyRequest.version().isPresent());
+ assertEquals(copyRequest.version().get(), HTTP_1_1);
+ }
+
+ @Test
+ public void testTimeout() {
+ HttpRequest.Builder builder = newBuilder(uri);
+ assertThrows(IAE, () -> builder.timeout(ZERO));
+ assertThrows(IAE, () -> builder.timeout(ofSeconds(0)));
+ assertThrows(IAE, () -> builder.timeout(ofSeconds(-1)));
+ assertThrows(IAE, () -> builder.timeout(ofNanos(-100)));
+ assertEquals(builder.timeout(ofNanos(15)).build().timeout().get(), ofNanos(15));
+ assertEquals(builder.timeout(ofSeconds(50)).build().timeout().get(), ofSeconds(50));
+ assertEquals(builder.timeout(ofMinutes(30)).build().timeout().get(), ofMinutes(30));
+ }
+
+ @Test
+ public void testExpect() {
+ HttpRequest.Builder builder = newBuilder(uri);
+ assertEquals(builder.build().expectContinue(), false);
+ assertEquals(builder.expectContinue(true).build().expectContinue(), true);
+ assertEquals(builder.expectContinue(false).build().expectContinue(), false);
+ assertEquals(builder.expectContinue(true).build().expectContinue(), true);
+ }
+}
--- /dev/null Thu Jan 01 00:00:00 1970 +0000
+++ b/test/jdk/java/net/httpclient/RequestProcessorExceptions.java Sun Nov 05 17:32:13 2017 +0000
@@ -0,0 +1,91 @@
+/*
+ * Copyright (c) 2017, Oracle and/or its affiliates. All rights reserved.
+ * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER.
+ *
+ * This code is free software; you can redistribute it and/or modify it
+ * under the terms of the GNU General Public License version 2 only, as
+ * published by the Free Software Foundation.
+ *
+ * This code is distributed in the hope that it will be useful, but WITHOUT
+ * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or
+ * FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License
+ * version 2 for more details (a copy is included in the LICENSE file that
+ * accompanied this code).
+ *
+ * You should have received a copy of the GNU General Public License version
+ * 2 along with this work; if not, write to the Free Software Foundation,
+ * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA.
+ *
+ * Please contact Oracle, 500 Oracle Parkway, Redwood Shores, CA 94065 USA
+ * or visit www.oracle.com if you need additional information or have any
+ * questions.
+ */
+
+/*
+ * @test
+ * @run testng RequestProcessorExceptions
+ */
+
+import java.io.FileNotFoundException;
+import java.nio.file.Files;
+import java.nio.file.Path;
+import java.nio.file.Paths;
+import java.util.List;
+import org.testng.annotations.DataProvider;
+import org.testng.annotations.Test;
+import static jdk.incubator.http.HttpRequest.BodyPublisher.fromByteArray;
+import static jdk.incubator.http.HttpRequest.BodyPublisher.fromFile;
+
+public class RequestProcessorExceptions {
+
+ @DataProvider(name = "byteArrayOOBs")
+ public Object[][] byteArrayOOBs() {
+ return new Object[][] {
+ { new byte[100], 1, 100 },
+ { new byte[100], -1, 10 },
+ { new byte[100], 99, 2 },
+ { new byte[1], -100, 1 } };
+ }
+
+ @Test(dataProvider = "byteArrayOOBs", expectedExceptions = IndexOutOfBoundsException.class)
+ public void fromByteArrayCheck(byte[] buf, int offset, int length) {
+ fromByteArray(buf, offset, length);
+ }
+
+ @DataProvider(name = "nonExistentFiles")
+ public Object[][] nonExistentFiles() {
+ List<Path> paths = List.of(Paths.get("doesNotExist"),
+ Paths.get("tsixEtoNseod"),
+ Paths.get("doesNotExist2"));
+ paths.forEach(p -> {
+ if (Files.exists(p))
+ throw new AssertionError("Unexpected " + p);
+ });
+
+ return paths.stream().map(p -> new Object[] { p }).toArray(Object[][]::new);
+ }
+
+ @Test(dataProvider = "nonExistentFiles", expectedExceptions = FileNotFoundException.class)
+ public void fromFileCheck(Path path) throws Exception {
+ fromFile(path);
+ }
+
+ // ---
+
+ /* Main entry point for standalone testing of the main functional test. */
+ public static void main(String... args) throws Exception {
+ RequestProcessorExceptions t = new RequestProcessorExceptions();
+ for (Object[] objs : t.byteArrayOOBs()) {
+ try {
+ t.fromByteArrayCheck((byte[]) objs[0], (int) objs[1], (int) objs[2]);
+ throw new RuntimeException("fromByteArrayCheck failed");
+ } catch (IndexOutOfBoundsException expected) { /* Ok */ }
+ }
+ for (Object[] objs : t.nonExistentFiles()) {
+ try {
+ t.fromFileCheck((Path) objs[0]);
+ throw new RuntimeException("fromFileCheck failed");
+ } catch (FileNotFoundException expected) { /* Ok */ }
+ }
+ }
+}
--- a/test/jdk/java/net/httpclient/Server.java Sun Nov 05 17:05:57 2017 +0000
+++ /dev/null Thu Jan 01 00:00:00 1970 +0000
@@ -1,286 +0,0 @@
-/*
- * Copyright (c) 2015, 2016, Oracle and/or its affiliates. All rights reserved.
- * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER.
- *
- * This code is free software; you can redistribute it and/or modify it
- * under the terms of the GNU General Public License version 2 only, as
- * published by the Free Software Foundation.
- *
- * This code is distributed in the hope that it will be useful, but WITHOUT
- * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or
- * FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License
- * version 2 for more details (a copy is included in the LICENSE file that
- * accompanied this code).
- *
- * You should have received a copy of the GNU General Public License version
- * 2 along with this work; if not, write to the Free Software Foundation,
- * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA.
- *
- * Please contact Oracle, 500 Oracle Parkway, Redwood Shores, CA 94065 USA
- * or visit www.oracle.com if you need additional information or have any
- * questions.
- */
-
-import java.io.Closeable;
-import java.io.IOException;
-import java.io.InputStream;
-import java.io.OutputStream;
-import java.net.ServerSocket;
-import java.net.Socket;
-import java.nio.charset.StandardCharsets;
-import java.util.Collections;
-import java.util.LinkedList;
-import java.util.List;
-import java.util.Iterator;
-import java.util.concurrent.ArrayBlockingQueue;
-import java.util.concurrent.TimeUnit;
-import java.util.concurrent.atomic.AtomicInteger;
-
-/**
- * A cut-down Http/1 Server for testing various error situations
- *
- * use interrupt() to halt
- */
-public class Server extends Thread implements Closeable {
-
- ServerSocket ss;
- private final List<Connection> sockets;
- private final List<Connection> removals;
- private final List<Connection> additions;
- AtomicInteger counter = new AtomicInteger(0);
-
- // waits up to 20 seconds for something to happen
- // dont use this unless certain activity coming.
- public Connection activity() {
- for (int i = 0; i < 80 * 100; i++) {
- doRemovalsAndAdditions();
- for (Connection c : sockets) {
- if (c.poll()) {
- return c;
- }
- }
- try {
- Thread.sleep(250);
- } catch (InterruptedException e) {
- e.printStackTrace();
- }
- }
- return null;
- }
-
- private void doRemovalsAndAdditions() {
- if (removals.isEmpty() && additions.isEmpty())
- return;
- Iterator<Connection> i = removals.iterator();
- while (i.hasNext())
- sockets.remove(i.next());
- removals.clear();
-
- i = additions.iterator();
- while (i.hasNext())
- sockets.add(i.next());
- additions.clear();
- }
-
- // clears all current connections on Server.
- public void reset() {
- for (Connection c : sockets) {
- c.close();
- }
- }
-
- /**
- * Reads data into an ArrayBlockingQueue<String> where each String
- * is a line of input, that was terminated by CRLF (not included)
- */
- class Connection extends Thread {
- Connection(Socket s) throws IOException {
- this.socket = s;
- id = counter.incrementAndGet();
- is = s.getInputStream();
- os = s.getOutputStream();
- incoming = new ArrayBlockingQueue<>(100);
- setName("Server-Connection");
- setDaemon(true);
- }
- final Socket socket;
- final int id;
- final InputStream is;
- final OutputStream os;
- final ArrayBlockingQueue<String> incoming;
-
- final static String CRLF = "\r\n";
-
- // sentinel indicating connection closed
- final static String CLOSED = "C.L.O.S.E.D";
- volatile boolean closed = false;
-
- @Override
- public void run() {
- byte[] buf = new byte[256];
- String s = "";
- try {
- while (true) {
- int n = is.read(buf);
- if (n == -1) {
- cleanup();
- return;
- }
- String s0 = new String(buf, 0, n, StandardCharsets.ISO_8859_1);
- s = s + s0;
- int i;
- while ((i=s.indexOf(CRLF)) != -1) {
- String s1 = s.substring(0, i+2);
- incoming.put(s1);
- if (i+2 == s.length()) {
- s = "";
- break;
- }
- s = s.substring(i+2);
- }
- }
- } catch (IOException |InterruptedException e1) {
- cleanup();
- } catch (Throwable t) {
- System.out.println("X: " + t);
- cleanup();
- }
- }
-
- @Override
- public String toString() {
- return "Server.Connection: " + socket.toString();
- }
-
- public void sendHttpResponse(int code, String body, String... headers)
- throws IOException
- {
- String r1 = "HTTP/1.1 " + Integer.toString(code) + " status" + CRLF;
- for (int i=0; i<headers.length; i+=2) {
- r1 += headers[i] + ": " + headers[i+1] + CRLF;
- }
- int clen = body == null ? 0 : body.length();
- r1 += "Content-Length: " + Integer.toString(clen) + CRLF;
- r1 += CRLF;
- if (body != null) {
- r1 += body;
- }
- send(r1);
- }
-
- // content-length is 10 bytes too many
- public void sendIncompleteHttpResponseBody(int code) throws IOException {
- String body = "Hello World Helloworld Goodbye World";
- String r1 = "HTTP/1.1 " + Integer.toString(code) + " status" + CRLF;
- int clen = body.length() + 10;
- r1 += "Content-Length: " + Integer.toString(clen) + CRLF;
- r1 += CRLF;
- if (body != null) {
- r1 += body;
- }
- send(r1);
- }
-
- public void sendIncompleteHttpResponseHeaders(int code)
- throws IOException
- {
- String r1 = "HTTP/1.1 " + Integer.toString(code) + " status" + CRLF;
- send(r1);
- }
-
- public void send(String r) throws IOException {
- os.write(r.getBytes(StandardCharsets.ISO_8859_1));
- }
-
- public synchronized void close() {
- cleanup();
- closed = true;
- incoming.clear();
- }
-
- public String nextInput(long timeout, TimeUnit unit) {
- String result = "";
- while (poll()) {
- try {
- String s = incoming.poll(timeout, unit);
- if (s == null && closed) {
- return CLOSED;
- } else {
- result += s;
- }
- } catch (InterruptedException e) {
- return null;
- }
- }
- return result;
- }
-
- public String nextInput() {
- return nextInput(0, TimeUnit.SECONDS);
- }
-
- public boolean poll() {
- return incoming.peek() != null;
- }
-
- private void cleanup() {
- try {
- socket.close();
- } catch (IOException e) {}
- removals.add(this);
- }
- }
-
- Server(int port) throws IOException {
- ss = new ServerSocket(port);
- sockets = Collections.synchronizedList(new LinkedList<>());
- removals = Collections.synchronizedList(new LinkedList<>());
- additions = Collections.synchronizedList(new LinkedList<>());
- setName("Test-Server");
- setDaemon(true);
- }
-
- Server() throws IOException {
- this(0);
- }
-
- int port() {
- return ss.getLocalPort();
- }
-
- public String getURL() {
- return "http://127.0.0.1:" + port() + "/foo/";
- }
-
- private volatile boolean closed;
-
- @Override
- public void close() {
- closed = true;
- try {
- ss.close();
- } catch (IOException e) {
- e.printStackTrace();
- }
- for (Connection c : sockets) {
- c.close();
- }
- }
-
- @Override
- public void run() {
- while (!closed) {
- try {
- Socket s = ss.accept();
- Connection c = new Connection(s);
- c.start();
- additions.add(c);
- } catch (IOException e) {
- if (closed)
- return;
- e.printStackTrace();
- }
- }
- }
-
-}
--- a/test/jdk/java/net/httpclient/ShortRequestBody.java Sun Nov 05 17:05:57 2017 +0000
+++ b/test/jdk/java/net/httpclient/ShortRequestBody.java Sun Nov 05 17:32:13 2017 +0000
@@ -1,5 +1,5 @@
/*
- * Copyright (c) 2015, 2016, Oracle and/or its affiliates. All rights reserved.
+ * Copyright (c) 2015, 2017, Oracle and/or its affiliates. All rights reserved.
* DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER.
*
* This code is free software; you can redistribute it and/or modify it
@@ -21,10 +21,10 @@
* questions.
*/
-import java.io.*;
-import jdk.incubator.http.HttpClient;
-import jdk.incubator.http.HttpResponse;
-import jdk.incubator.http.HttpRequest;
+import java.io.IOException;
+import java.io.InputStream;
+import java.io.OutputStream;
+import java.io.UncheckedIOException;
import java.net.ServerSocket;
import java.net.Socket;
import java.net.URI;
@@ -32,14 +32,20 @@
import java.nio.file.Path;
import java.nio.file.Paths;
import java.nio.ByteBuffer;
+import java.util.ArrayList;
+import java.util.List;
import java.util.concurrent.CompletableFuture;
-import java.util.concurrent.Executor;
-import java.util.concurrent.ExecutorService;
import java.util.concurrent.ExecutionException;
import java.util.concurrent.Flow;
import java.util.concurrent.TimeoutException;
import java.util.concurrent.TimeUnit;
-import static java.lang.System.out;
+import java.util.function.Supplier;
+import jdk.incubator.http.HttpClient;
+import jdk.incubator.http.HttpResponse;
+import jdk.incubator.http.HttpRequest;
+import jdk.incubator.http.HttpTimeoutException;
+
+import static java.lang.System.err;
import static java.nio.charset.StandardCharsets.US_ASCII;
import static jdk.incubator.http.HttpResponse.BodyHandler.discard;
import static java.nio.charset.StandardCharsets.UTF_8;
@@ -48,23 +54,13 @@
* @test
* @bug 8151441
* @summary Request body of incorrect (larger or smaller) sizes than that
- * reported by the body processor
+ * reported by the body publisher
* @run main/othervm ShortRequestBody
*/
public class ShortRequestBody {
static final Path testSrc = Paths.get(System.getProperty("test.src", "."));
- static volatile HttpClient staticDefaultClient;
-
- static HttpClient defaultClient() {
- if (staticDefaultClient == null) {
- synchronized (ShortRequestBody.class) {
- staticDefaultClient = HttpClient.newHttpClient();
- }
- }
- return staticDefaultClient;
- }
// Some body types ( sources ) for testing.
static final String STRING_BODY = "Hello world";
@@ -79,14 +75,14 @@
fileSize(FILE_BODY) };
static final int[] BODY_OFFSETS = new int[] { 0, +1, -1, +2, -2, +3, -3 };
- // A delegating body processor. Subtypes will have a concrete body type.
+ // A delegating Body Publisher. Subtypes will have a concrete body type.
static abstract class AbstractDelegateRequestBody
- implements HttpRequest.BodyProcessor {
+ implements HttpRequest.BodyPublisher {
- final HttpRequest.BodyProcessor delegate;
+ final HttpRequest.BodyPublisher delegate;
final long contentLength;
- AbstractDelegateRequestBody(HttpRequest.BodyProcessor delegate,
+ AbstractDelegateRequestBody(HttpRequest.BodyPublisher delegate,
long contentLength) {
this.delegate = delegate;
this.contentLength = contentLength;
@@ -101,26 +97,26 @@
public long contentLength() { return contentLength; /* may be wrong! */ }
}
- // Request body processors that may generate a different number of actual
+ // Request body Publishers that may generate a different number of actual
// bytes to that of what is reported through their {@code contentLength}.
static class StringRequestBody extends AbstractDelegateRequestBody {
StringRequestBody(String body, int additionalLength) {
- super(HttpRequest.BodyProcessor.fromString(body),
+ super(HttpRequest.BodyPublisher.fromString(body),
body.getBytes(UTF_8).length + additionalLength);
}
}
static class ByteArrayRequestBody extends AbstractDelegateRequestBody {
ByteArrayRequestBody(byte[] body, int additionalLength) {
- super(HttpRequest.BodyProcessor.fromByteArray(body),
+ super(HttpRequest.BodyPublisher.fromByteArray(body),
body.length + additionalLength);
}
}
static class FileRequestBody extends AbstractDelegateRequestBody {
FileRequestBody(Path path, int additionalLength) throws IOException {
- super(HttpRequest.BodyProcessor.fromFile(path),
+ super(HttpRequest.BodyPublisher.fromFile(path),
Files.size(path) + additionalLength);
}
}
@@ -128,53 +124,63 @@
// ---
public static void main(String[] args) throws Exception {
- try (Server server = new Server()) {
- URI uri = new URI("http://127.0.0.1:" + server.getPort() + "/");
+ HttpClient sharedClient = HttpClient.newHttpClient();
+ List<Supplier<HttpClient>> clientSuppliers = new ArrayList<>();
+ clientSuppliers.add(() -> HttpClient.newHttpClient());
+ clientSuppliers.add(() -> sharedClient);
- // sanity
- success(uri, new StringRequestBody(STRING_BODY, 0));
- success(uri, new ByteArrayRequestBody(BYTE_ARRAY_BODY, 0));
- success(uri, new FileRequestBody(FILE_BODY, 0));
+ try (Server server = new Server()) {
+ for (Supplier<HttpClient> cs : clientSuppliers) {
+ err.println("\n---- next supplier ----\n");
+ URI uri = new URI("http://127.0.0.1:" + server.getPort() + "/");
- for (int i=1; i< BODY_OFFSETS.length; i++) {
- failureBlocking(uri, new StringRequestBody(STRING_BODY, BODY_OFFSETS[i]));
- failureBlocking(uri, new ByteArrayRequestBody(BYTE_ARRAY_BODY, BODY_OFFSETS[i]));
- failureBlocking(uri, new FileRequestBody(FILE_BODY, BODY_OFFSETS[i]));
+ // sanity ( 6 requests to keep client and server offsets easy to workout )
+ success(cs, uri, new StringRequestBody(STRING_BODY, 0));
+ success(cs, uri, new ByteArrayRequestBody(BYTE_ARRAY_BODY, 0));
+ success(cs, uri, new FileRequestBody(FILE_BODY, 0));
+ success(cs, uri, new StringRequestBody(STRING_BODY, 0));
+ success(cs, uri, new ByteArrayRequestBody(BYTE_ARRAY_BODY, 0));
+ success(cs, uri, new FileRequestBody(FILE_BODY, 0));
- failureNonBlocking(uri, new StringRequestBody(STRING_BODY, BODY_OFFSETS[i]));
- failureNonBlocking(uri, new ByteArrayRequestBody(BYTE_ARRAY_BODY, BODY_OFFSETS[i]));
- failureNonBlocking(uri, new FileRequestBody(FILE_BODY, BODY_OFFSETS[i]));
- }
- } finally {
- Executor def = defaultClient().executor();
- if (def instanceof ExecutorService) {
- ((ExecutorService)def).shutdownNow();
+ for (int i = 1; i < BODY_OFFSETS.length; i++) {
+ failureBlocking(cs, uri, new StringRequestBody(STRING_BODY, BODY_OFFSETS[i]));
+ failureBlocking(cs, uri, new ByteArrayRequestBody(BYTE_ARRAY_BODY, BODY_OFFSETS[i]));
+ failureBlocking(cs, uri, new FileRequestBody(FILE_BODY, BODY_OFFSETS[i]));
+
+ failureNonBlocking(cs, uri, new StringRequestBody(STRING_BODY, BODY_OFFSETS[i]));
+ failureNonBlocking(cs, uri, new ByteArrayRequestBody(BYTE_ARRAY_BODY, BODY_OFFSETS[i]));
+ failureNonBlocking(cs, uri, new FileRequestBody(FILE_BODY, BODY_OFFSETS[i]));
+ }
}
}
}
- static void success(URI uri, HttpRequest.BodyProcessor processor)
+ static void success(Supplier<HttpClient> clientSupplier,
+ URI uri,
+ HttpRequest.BodyPublisher publisher)
throws Exception
{
CompletableFuture<HttpResponse<Void>> cf;
HttpRequest request = HttpRequest.newBuilder(uri)
- .POST(processor)
+ .POST(publisher)
.build();
- cf = defaultClient().sendAsync(request, discard(null));
+ cf = clientSupplier.get().sendAsync(request, discard(null));
HttpResponse<Void> resp = cf.get(30, TimeUnit.SECONDS);
- out.println("Response code: " + resp.statusCode());
+ err.println("Response code: " + resp.statusCode());
check(resp.statusCode() == 200, "Expected 200, got ", resp.statusCode());
}
- static void failureNonBlocking(URI uri, HttpRequest.BodyProcessor processor)
+ static void failureNonBlocking(Supplier<HttpClient> clientSupplier,
+ URI uri,
+ HttpRequest.BodyPublisher publisher)
throws Exception
{
CompletableFuture<HttpResponse<Void>> cf;
HttpRequest request = HttpRequest.newBuilder(uri)
- .POST(processor)
+ .POST(publisher)
.build();
- cf = defaultClient().sendAsync(request, discard(null));
+ cf = clientSupplier.get().sendAsync(request, discard(null));
try {
HttpResponse<Void> r = cf.get(30, TimeUnit.SECONDS);
@@ -182,23 +188,34 @@
} catch (TimeoutException x) {
throw new RuntimeException("Unexpected timeout", x);
} catch (ExecutionException expected) {
- out.println("Caught expected: " + expected);
- check(expected.getCause() instanceof IOException,
+ err.println("Caught expected: " + expected);
+ Throwable t = expected.getCause();
+ check(t instanceof IOException,
"Expected cause IOException, but got: ", expected.getCause());
+ String msg = t.getMessage();
+ check(msg.contains("Too many") || msg.contains("Too few"),
+ "Expected Too many|Too few, got: ", t);
}
}
- static void failureBlocking(URI uri, HttpRequest.BodyProcessor processor)
+ static void failureBlocking(Supplier<HttpClient> clientSupplier,
+ URI uri,
+ HttpRequest.BodyPublisher publisher)
throws Exception
{
HttpRequest request = HttpRequest.newBuilder(uri)
- .POST(processor)
+ .POST(publisher)
.build();
try {
- HttpResponse<Void> r = defaultClient().send(request, discard(null));
+ HttpResponse<Void> r = clientSupplier.get().send(request, discard(null));
throw new RuntimeException("Unexpected response: " + r.statusCode());
+ } catch (HttpTimeoutException x) {
+ throw new RuntimeException("Unexpected timeout", x);
} catch (IOException expected) {
- out.println("Caught expected: " + expected);
+ err.println("Caught expected: " + expected);
+ String msg = expected.getMessage();
+ check(msg.contains("Too many") || msg.contains("Too few"),
+ "Expected Too many|Too few, got: ", expected);
}
}
@@ -225,20 +242,40 @@
while (!closed) {
try (Socket s = ss.accept()) {
+ err.println("Server: got connection");
InputStream is = s.getInputStream();
readRequestHeaders(is);
byte[] ba = new byte[1024];
int length = BODY_LENGTHS[count % 3];
length += BODY_OFFSETS[offset];
-
- is.readNBytes(ba, 0, length);
+ err.println("Server: count=" + count + ", offset=" + offset);
+ err.println("Server: expecting " +length+ " bytes");
+ int read = is.readNBytes(ba, 0, length);
+ err.println("Server: actually read " + read + " bytes");
- OutputStream os = s.getOutputStream();
- os.write(RESPONSE.getBytes(US_ASCII));
+ // Update the counts before replying, to prevent the
+ // client-side racing reset with this thread.
count++;
if (count % 6 == 0) // 6 is the number of failure requests per offset
offset++;
+ if (count % 42 == 0) {
+ count = 0; // reset, for second iteration
+ offset = 0;
+ }
+
+ if (read < length) {
+ // no need to reply, client has already closed
+ // ensure closed
+ if (is.read() != -1)
+ new AssertionError("Unexpected read");
+ } else {
+ OutputStream os = s.getOutputStream();
+ err.println("Server: writing "
+ + RESPONSE.getBytes(US_ASCII).length + " bytes");
+ os.write(RESPONSE.getBytes(US_ASCII));
+ }
+
} catch (IOException e) {
if (!closed)
System.out.println("Unexpected" + e);
--- a/test/jdk/java/net/httpclient/SmallTimeout.java Sun Nov 05 17:05:57 2017 +0000
+++ b/test/jdk/java/net/httpclient/SmallTimeout.java Sun Nov 05 17:32:13 2017 +0000
@@ -40,7 +40,7 @@
* @test
* @bug 8178147
* @summary Ensures that small timeouts do not cause hangs due to race conditions
- * @run main/othervm SmallTimeout
+ * @run main/othervm -Djdk.incubator.http.internal.common.DEBUG=true SmallTimeout
*/
// To enable logging use. Not enabled by default as it changes the dynamics
@@ -52,7 +52,25 @@
static int[] TIMEOUTS = {2, 1, 3, 2, 100, 1};
// A queue for placing timed out requests so that their order can be checked.
- static LinkedBlockingQueue<HttpRequest> queue = new LinkedBlockingQueue<>();
+ static LinkedBlockingQueue<HttpResult> queue = new LinkedBlockingQueue<>();
+
+ static final class HttpResult {
+ final HttpRequest request;
+ final Throwable failed;
+ HttpResult(HttpRequest request, Throwable failed) {
+ this.request = request;
+ this.failed = failed;
+ }
+
+ static HttpResult of(HttpRequest request) {
+ return new HttpResult(request, null);
+ }
+
+ static HttpResult of(HttpRequest request, Throwable t) {
+ return new HttpResult(request, t);
+ }
+
+ }
static volatile boolean error;
@@ -76,8 +94,10 @@
CompletableFuture<HttpResponse<Object>> response = client
.sendAsync(req, discard(null))
.whenComplete((HttpResponse<Object> r, Throwable t) -> {
+ Throwable cause = null;
if (r != null) {
out.println("Unexpected response: " + r);
+ cause = new RuntimeException("Unexpected response");
error = true;
}
if (t != null) {
@@ -85,6 +105,7 @@
out.println("Wrong exception type:" + t.toString());
Throwable c = t.getCause() == null ? t : t.getCause();
c.printStackTrace();
+ cause = c;
error = true;
} else {
out.println("Caught expected timeout: " + t.getCause());
@@ -92,9 +113,10 @@
}
if (t == null && r == null) {
out.println("Both response and throwable are null!");
+ cause = new RuntimeException("Both response and throwable are null!");
error = true;
}
- queue.add(req);
+ queue.add(HttpResult.of(req,cause));
});
}
System.out.println("All requests submitted. Waiting ...");
@@ -118,15 +140,18 @@
final HttpRequest req = requests[i];
executor.execute(() -> {
+ Throwable cause = null;
try {
client.send(req, discard(null));
} catch (HttpTimeoutException e) {
out.println("Caught expected timeout: " + e);
- queue.offer(req);
- } catch (IOException | InterruptedException ee) {
+ } catch (Throwable ee) {
Throwable c = ee.getCause() == null ? ee : ee.getCause();
c.printStackTrace();
+ cause = c;
error = true;
+ } finally {
+ queue.offer(HttpResult.of(req, cause));
}
});
}
@@ -139,18 +164,20 @@
if (error)
throw new RuntimeException("Failed. Check output");
- } finally {
- ((ExecutorService) client.executor()).shutdownNow();
}
}
static void checkReturn(HttpRequest[] requests) throws InterruptedException {
// wait for exceptions and check order
+ boolean ok = true;
for (int j = 0; j < TIMEOUTS.length; j++) {
- HttpRequest req = queue.take();
- out.println("Got request from queue " + req + ", order: " + getRequest(req, requests));
+ HttpResult res = queue.take();
+ HttpRequest req = res.request;
+ out.println("Got request from queue " + req + ", order: " + getRequest(req, requests)
+ + (res.failed == null ? "" : " failed: " + res.failed));
+ ok = ok && res.failed == null;
}
- out.println("Return ok");
+ out.println("Return " + (ok ? "ok" : "nok"));
}
/** Returns the index of the request in the array. */
--- a/test/jdk/java/net/httpclient/SmokeTest.java Sun Nov 05 17:05:57 2017 +0000
+++ b/test/jdk/java/net/httpclient/SmokeTest.java Sun Nov 05 17:32:13 2017 +0000
@@ -32,7 +32,7 @@
* @compile ../../../com/sun/net/httpserver/LogFilter.java
* @compile ../../../com/sun/net/httpserver/EchoHandler.java
* @compile ../../../com/sun/net/httpserver/FileServerHandler.java
- * @run main/othervm -Djdk.httpclient.HttpClient.log=errors,trace SmokeTest
+ * @run main/othervm -Djdk.internal.httpclient.debug=true -Djdk.httpclient.HttpClient.log=errors,trace SmokeTest
*/
import com.sun.net.httpserver.Headers;
@@ -47,8 +47,6 @@
import java.net.InetSocketAddress;
import java.net.PasswordAuthentication;
import java.net.ProxySelector;
-import java.net.ServerSocket;
-import java.net.Socket;
import java.net.URI;
import jdk.incubator.http.HttpClient;
import jdk.incubator.http.HttpRequest;
@@ -81,9 +79,9 @@
import java.util.List;
import java.util.Random;
import jdk.testlibrary.SimpleSSLContext;
-import static jdk.incubator.http.HttpRequest.BodyProcessor.fromFile;
-import static jdk.incubator.http.HttpRequest.BodyProcessor.fromInputStream;
-import static jdk.incubator.http.HttpRequest.BodyProcessor.fromString;
+import static jdk.incubator.http.HttpRequest.BodyPublisher.fromFile;
+import static jdk.incubator.http.HttpRequest.BodyPublisher.fromInputStream;
+import static jdk.incubator.http.HttpRequest.BodyPublisher.fromString;
import static jdk.incubator.http.HttpResponse.*;
import static jdk.incubator.http.HttpResponse.BodyHandler.asFile;
import static jdk.incubator.http.HttpResponse.BodyHandler.asString;
@@ -172,7 +170,6 @@
.build();
try {
-
test1(httproot + "files/foo.txt", true);
test1(httproot + "files/foo.txt", false);
test1(httpsroot + "files/foo.txt", true);
@@ -255,7 +252,10 @@
String body = response.body();
if (!body.equals("This is foo.txt\r\n")) {
- throw new RuntimeException();
+ throw new RuntimeException("Did not get expected body: "
+ + "\n\t expected \"This is foo.txt\\r\\n\""
+ + "\n\t received \""
+ + body.replace("\r", "\\r").replace("\n","\\n") + "\"");
}
// repeat async
@@ -296,14 +296,13 @@
static void test2a(String s) throws Exception {
System.out.print("test2a: " + s);
URI uri = new URI(s);
- Path p = Util.getTempFile(128 * 1024);
- //Path p = Util.getTempFile(1 * 1024);
+ Path p = getTempFile(128 * 1024);
HttpRequest request = HttpRequest.newBuilder(uri)
.POST(fromFile(p))
.build();
- Path resp = Util.getTempFile(1); // will be overwritten
+ Path resp = getTempFile(1); // will be overwritten
HttpResponse<Path> response =
client.send(request,
@@ -465,7 +464,7 @@
@SuppressWarnings("rawtypes")
static void test7(String target) throws Exception {
System.out.print("test7: " + target);
- Path requestBody = Util.getTempFile(128 * 1024);
+ Path requestBody = getTempFile(128 * 1024);
// First test
URI uri = new URI(target);
HttpRequest request = HttpRequest.newBuilder().uri(uri).GET().build();
@@ -644,7 +643,7 @@
ch.setLevel(Level.SEVERE);
logger.addHandler(ch);
- String root = System.getProperty ("test.src")+ "/docs";
+ String root = System.getProperty ("test.src", ".")+ "/docs";
InetSocketAddress addr = new InetSocketAddress (0);
s1 = HttpServer.create (addr, 0);
if (s1 instanceof HttpsServer) {
@@ -690,167 +689,106 @@
proxyPort = proxy.getPort();
System.out.println("Proxy port = " + proxyPort);
}
-}
-class Configurator extends HttpsConfigurator {
- public Configurator(SSLContext ctx) {
- super(ctx);
- }
+ static class RedirectHandler implements HttpHandler {
+ private final String root;
+ private volatile int count = 0;
- public void configure (HttpsParameters params) {
- params.setSSLParameters (getSSLContext().getSupportedSSLParameters());
- }
-}
+ RedirectHandler(String root) {
+ this.root = root;
+ }
-class UploadServer extends Thread {
- int statusCode;
- ServerSocket ss;
- int port;
- int size;
- Object lock;
- boolean failed = false;
+ @Override
+ public synchronized void handle(HttpExchange t) throws IOException {
+ byte[] buf = new byte[2048];
+ try (InputStream is = t.getRequestBody()) {
+ while (is.read(buf) != -1) ;
+ }
- UploadServer(int size) throws IOException {
- this.statusCode = statusCode;
- this.size = size;
- ss = new ServerSocket(0);
- port = ss.getLocalPort();
- lock = new Object();
- }
+ Headers responseHeaders = t.getResponseHeaders();
- int port() {
- return port;
- }
+ if (count++ < 1) {
+ responseHeaders.add("Location", root + "/foo/" + count);
+ } else {
+ responseHeaders.add("Location", SmokeTest.midSizedFilename);
+ }
+ t.sendResponseHeaders(301, -1);
+ t.close();
+ }
- int size() {
- return size;
- }
+ int count() {
+ return count;
+ }
- // wait a sec before calling this
- boolean failed() {
- synchronized(lock) {
- return failed;
+ void reset() {
+ count = 0;
}
}
- @Override
- public void run () {
- int nbytes = 0;
- Socket s = null;
-
- synchronized(lock) {
- try {
- s = ss.accept();
+ static class RedirectErrorHandler implements HttpHandler {
+ private final String root;
+ private volatile int count = 1;
- InputStream is = s.getInputStream();
- OutputStream os = s.getOutputStream();
- os.write("HTTP/1.1 201 OK\r\nContent-length: 0\r\n\r\n".getBytes());
- int n;
- byte[] buf = new byte[8000];
- while ((n=is.read(buf)) != -1) {
- nbytes += n;
- }
- } catch (IOException e) {
- System.out.println ("read " + nbytes);
- System.out.println ("size " + size);
- failed = nbytes >= size;
- } finally {
- try {
- ss.close();
- if (s != null)
- s.close();
- } catch (IOException e) {}
- }
+ RedirectErrorHandler(String root) {
+ this.root = root;
}
- }
-}
-
-class RedirectHandler implements HttpHandler {
- String root;
- volatile int count = 0;
- RedirectHandler(String root) {
- this.root = root;
- }
+ synchronized int count() {
+ return count;
+ }
- @Override
- public synchronized void handle(HttpExchange t)
- throws IOException
- {
- byte[] buf = new byte[2048];
- try (InputStream is = t.getRequestBody()) {
- while (is.read(buf) != -1) ;
+ synchronized void increment() {
+ count++;
}
- Headers responseHeaders = t.getResponseHeaders();
-
- if (count++ < 1) {
- responseHeaders.add("Location", root + "/foo/" + count);
- } else {
- responseHeaders.add("Location", SmokeTest.midSizedFilename);
- }
- t.sendResponseHeaders(301, -1);
- t.close();
- }
-
- int count() {
- return count;
- }
+ @Override
+ public synchronized void handle(HttpExchange t) throws IOException {
+ try (InputStream is = t.getRequestBody()) {
+ is.readAllBytes();
+ }
- void reset() {
- count = 0;
- }
-}
-
-class RedirectErrorHandler implements HttpHandler {
- String root;
- volatile int count = 1;
-
- RedirectErrorHandler(String root) {
- this.root = root;
- }
-
- synchronized int count() {
- return count;
+ Headers map = t.getResponseHeaders();
+ String redirect = root + "/foo/" + Integer.toString(count);
+ increment();
+ map.add("Location", redirect);
+ t.sendResponseHeaders(301, -1);
+ t.close();
+ }
}
- synchronized void increment() {
- count++;
- }
+ static class DelayHandler implements HttpHandler {
+
+ CyclicBarrier bar1 = new CyclicBarrier(2);
+ CyclicBarrier bar2 = new CyclicBarrier(2);
+ CyclicBarrier bar3 = new CyclicBarrier(2);
- @Override
- public synchronized void handle (HttpExchange t)
- throws IOException
- {
- byte[] buf = new byte[2048];
- try (InputStream is = t.getRequestBody()) {
- while (is.read(buf) != -1) ;
+ CyclicBarrier barrier1() {
+ return bar1;
+ }
+
+ CyclicBarrier barrier2() {
+ return bar2;
}
- Headers map = t.getResponseHeaders();
- String redirect = root + "/foo/" + Integer.toString(count);
- increment();
- map.add("Location", redirect);
- t.sendResponseHeaders(301, -1);
- t.close();
+ @Override
+ public synchronized void handle(HttpExchange he) throws IOException {
+ he.getRequestBody().readAllBytes();
+ try {
+ bar1.await();
+ bar2.await();
+ } catch (Exception e) { }
+ he.sendResponseHeaders(200, -1); // will probably fail
+ he.close();
+ }
}
-}
-class Util {
- static byte[] readAll(InputStream is) throws IOException {
- byte[] buf = new byte[1024];
- byte[] result = new byte[0];
+ static class Configurator extends HttpsConfigurator {
+ public Configurator(SSLContext ctx) {
+ super(ctx);
+ }
- while (true) {
- int n = is.read(buf);
- if (n > 0) {
- byte[] b1 = new byte[result.length + n];
- System.arraycopy(result, 0, b1, 0, result.length);
- System.arraycopy(buf, 0, b1, result.length, n);
- result = b1;
- } else if (n == -1) {
- return result;
- }
+ public void configure (HttpsParameters params) {
+ params.setSSLParameters (getSSLContext().getSupportedSSLParameters());
}
}
@@ -858,8 +796,8 @@
File f = File.createTempFile("test", "txt");
f.deleteOnExit();
byte[] buf = new byte[2048];
- for (int i=0; i<buf.length; i++)
- buf[i] = (byte)i;
+ for (int i = 0; i < buf.length; i++)
+ buf[i] = (byte) i;
FileOutputStream fos = new FileOutputStream(f);
while (size > 0) {
@@ -872,33 +810,6 @@
}
}
-class DelayHandler implements HttpHandler {
-
- CyclicBarrier bar1 = new CyclicBarrier(2);
- CyclicBarrier bar2 = new CyclicBarrier(2);
- CyclicBarrier bar3 = new CyclicBarrier(2);
-
- CyclicBarrier barrier1() {
- return bar1;
- }
-
- CyclicBarrier barrier2() {
- return bar2;
- }
-
- @Override
- public synchronized void handle(HttpExchange he) throws IOException {
- byte[] buf = Util.readAll(he.getRequestBody());
- try {
- bar1.await();
- bar2.await();
- } catch (Exception e) {}
- he.sendResponseHeaders(200, -1); // will probably fail
- he.close();
- }
-
-}
-
// check for simple hardcoded sequence and use remote address
// to check.
// First 4 requests executed in sequence (should use same connection/address)
--- a/test/jdk/java/net/httpclient/SplitResponse.java Sun Nov 05 17:05:57 2017 +0000
+++ b/test/jdk/java/net/httpclient/SplitResponse.java Sun Nov 05 17:32:13 2017 +0000
@@ -1,5 +1,5 @@
/*
- * Copyright (c) 2015, 2016, Oracle and/or its affiliates. All rights reserved.
+ * Copyright (c) 2015, 2017, Oracle and/or its affiliates. All rights reserved.
* DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER.
*
* This code is free software; you can redistribute it and/or modify it
@@ -21,38 +21,51 @@
* questions.
*/
-
import java.io.IOException;
-import jdk.incubator.http.HttpClient;
-import jdk.incubator.http.HttpRequest;
-import jdk.incubator.http.HttpResponse;
import java.net.URI;
import java.util.concurrent.CompletableFuture;
import java.util.concurrent.Executor;
import java.util.concurrent.ExecutorService;
+import javax.net.ssl.SSLContext;
+import javax.net.ServerSocketFactory;
+import javax.net.ssl.SSLServerSocketFactory;
+import jdk.incubator.http.HttpClient;
+import jdk.incubator.http.HttpClient.Version;
+import jdk.incubator.http.HttpRequest;
+import jdk.incubator.http.HttpResponse;
+import jdk.testlibrary.SimpleSSLContext;
+import static java.lang.System.out;
+import static java.lang.String.format;
import static jdk.incubator.http.HttpResponse.BodyHandler.asString;
/**
* @test
* @bug 8087112
- * @key intermittent
- * @build Server
- * @run main/othervm -Djava.net.HttpClient.log=all SplitResponse
+ * @library /lib/testlibrary
+ * @build jdk.testlibrary.SimpleSSLContext
+ * @build MockServer
+ * @run main/othervm -Djdk.internal.httpclient.debug=true -Djdk.httpclient.HttpClient.log=all SplitResponse
*/
/**
* Similar test to QuickResponses except that each byte of the response
* is sent in a separate packet, which tests the stability of the implementation
- * for receiving unusual packet sizes.
+ * for receiving unusual packet sizes. Additionally, tests scenarios there
+ * connections that are retrieved from the connection pool may reach EOF before
+ * being reused.
*/
public class SplitResponse {
- static Server server;
+ static String response(String body, boolean serverKeepalive) {
+ StringBuilder sb = new StringBuilder();
+ sb.append("HTTP/1.1 200 OK\r\n");
+ if (!serverKeepalive)
+ sb.append("Connection: Close\r\n");
- static String response(String body) {
- return "HTTP/1.1 200 OK\r\nConnection: Close\r\nContent-length: "
- + Integer.toString(body.length())
- + "\r\n\r\n" + body;
+ sb.append("Content-length: ").append(body.length()).append("\r\n");
+ sb.append("\r\n");
+ sb.append(body);
+ return sb.toString();
}
static final String responses[] = {
@@ -68,59 +81,123 @@
"Excepteur sint occaecat cupidatat non proident."
};
+ final ServerSocketFactory factory;
+ final SSLContext context;
+ final boolean useSSL;
+ SplitResponse(boolean useSSL) throws IOException {
+ this.useSSL = useSSL;
+ context = new SimpleSSLContext().get();
+ SSLContext.setDefault(context);
+ factory = useSSL ? SSLServerSocketFactory.getDefault()
+ : ServerSocketFactory.getDefault();
+ }
+
+ public HttpClient newHttpClient() {
+ HttpClient client;
+ if (useSSL) {
+ client = HttpClient.newBuilder()
+ .sslContext(context)
+ .build();
+ } else {
+ client = HttpClient.newHttpClient();
+ }
+ return client;
+ }
+
public static void main(String[] args) throws Exception {
- server = new Server(0);
+ boolean useSSL = false;
+ if (args != null && args.length == 1) {
+ useSSL = "SSL".equals(args[0]);
+ }
+ SplitResponse sp = new SplitResponse(useSSL);
+
+ for (Version version : Version.values()) {
+ for (boolean serverKeepalive : new boolean[]{ true, false }) {
+ // Note: the mock server doesn't support Keep-Alive, but
+ // pretending that it might exercises code paths in and out of
+ // the connection pool, and retry logic
+ for (boolean async : new boolean[]{ true, false }) {
+ sp.test(version, serverKeepalive, async);
+ }
+ }
+ }
+ }
+
+ // @Test
+ void test(Version version, boolean serverKeepalive, boolean async)
+ throws Exception
+ {
+ out.println(format("*** version %s, serverKeepAlive: %s, async: %s ***",
+ version, serverKeepalive, async));
+ MockServer server = new MockServer(0, factory);
URI uri = new URI(server.getURL());
+ out.println("server is: " + uri);
server.start();
- HttpClient client = HttpClient.newHttpClient();
- HttpRequest request = HttpRequest.newBuilder(uri).build();
+ HttpClient client = newHttpClient();
+ HttpRequest request = HttpRequest.newBuilder(uri).version(version).build();
HttpResponse<String> r;
CompletableFuture<HttpResponse<String>> cf1;
try {
for (int i=0; i<responses.length; i++) {
- cf1 = client.sendAsync(request, asString());
+ out.println("----- iteration " + i + " -----");
String body = responses[i];
+ Thread t = sendSplitResponse(response(body, serverKeepalive), server);
- Server.Connection c = server.activity();
- sendSplitResponse(response(body), c);
- r = cf1.get();
- if (r.statusCode()!= 200)
+ if (async) {
+ out.println("send async: " + request);
+ cf1 = client.sendAsync(request, asString());
+ r = cf1.get();
+ } else { // sync
+ out.println("send sync: " + request);
+ r = client.send(request, asString());
+ }
+
+ if (r.statusCode() != 200)
throw new RuntimeException("Failed");
String rxbody = r.body();
- System.out.println("received " + rxbody);
+ out.println("received " + rxbody);
if (!rxbody.equals(body))
- throw new RuntimeException("Failed");
- c.close();
+ throw new RuntimeException(format("Expected:%s, got:%s", body, rxbody));
+
+ t.join();
+ conn.close();
}
} finally {
- Executor def = client.executor();
- if (def instanceof ExecutorService) {
- ((ExecutorService)def).shutdownNow();
- }
+ server.close();
}
System.out.println("OK");
}
- // send the response one byte at a time with a small delay between bytes
- // to ensure that each byte is read in a separate read
- static void sendSplitResponse(String s, Server.Connection conn) {
+ // required for cleanup
+ volatile MockServer.Connection conn;
+
+ // Sends the response, mostly, one byte at a time with a small delay
+ // between bytes, to encourage that each byte is read in a separate read
+ Thread sendSplitResponse(String s, MockServer server) {
System.out.println("Sending: ");
Thread t = new Thread(() -> {
+ System.out.println("Waiting for server to receive headers");
+ conn = server.activity();
+ System.out.println("Start sending response");
+
try {
int len = s.length();
+ out.println("sending " + s);
for (int i = 0; i < len; i++) {
String onechar = s.substring(i, i + 1);
conn.send(onechar);
- Thread.sleep(30);
+ Thread.sleep(10);
}
- System.out.println("sent");
+ out.println("sent " + s);
} catch (IOException | InterruptedException e) {
+ throw new RuntimeException(e);
}
});
t.setDaemon(true);
t.start();
+ return t;
}
}
--- /dev/null Thu Jan 01 00:00:00 1970 +0000
+++ b/test/jdk/java/net/httpclient/SplitResponseSSL.java Sun Nov 05 17:32:13 2017 +0000
@@ -0,0 +1,36 @@
+/*
+ * Copyright (c) 2017, Oracle and/or its affiliates. All rights reserved.
+ * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER.
+ *
+ * This code is free software; you can redistribute it and/or modify it
+ * under the terms of the GNU General Public License version 2 only, as
+ * published by the Free Software Foundation.
+ *
+ * This code is distributed in the hope that it will be useful, but WITHOUT
+ * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or
+ * FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License
+ * version 2 for more details (a copy is included in the LICENSE file that
+ * accompanied this code).
+ *
+ * You should have received a copy of the GNU General Public License version
+ * 2 along with this work; if not, write to the Free Software Foundation,
+ * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA.
+ *
+ * Please contact Oracle, 500 Oracle Parkway, Redwood Shores, CA 94065 USA
+ * or visit www.oracle.com if you need additional information or have any
+ * questions.
+ */
+
+/**
+ * @test
+ * @bug 8087112
+ * @library /lib/testlibrary
+ * @build jdk.testlibrary.SimpleSSLContext
+ * @build MockServer SplitResponse
+ * @run main/othervm -Djdk.internal.httpclient.debug=true -Djdk.httpclient.HttpClient.log=all SplitResponseSSL SSL
+ */
+public class SplitResponseSSL {
+ public static void main(String[] args) throws Exception {
+ SplitResponse.main(args);
+ }
+}
--- a/test/jdk/java/net/httpclient/TimeoutOrdering.java Sun Nov 05 17:05:57 2017 +0000
+++ b/test/jdk/java/net/httpclient/TimeoutOrdering.java Sun Nov 05 17:32:13 2017 +0000
@@ -135,8 +135,6 @@
if (error)
throw new RuntimeException("Failed. Check output");
- } finally {
- ((ExecutorService) client.executor()).shutdownNow();
}
}
--- a/test/jdk/java/net/httpclient/VersionTest.java Sun Nov 05 17:05:57 2017 +0000
+++ b/test/jdk/java/net/httpclient/VersionTest.java Sun Nov 05 17:32:13 2017 +0000
@@ -42,8 +42,7 @@
import jdk.incubator.http.HttpClient;
import jdk.incubator.http.HttpRequest;
import jdk.incubator.http.HttpResponse;
-import static jdk.incubator.http.HttpRequest.BodyProcessor.fromString;
-import static jdk.incubator.http.HttpResponse.*;
+import static jdk.incubator.http.HttpRequest.BodyPublisher.fromString;
import static jdk.incubator.http.HttpResponse.BodyHandler.asString;
import static jdk.incubator.http.HttpResponse.BodyHandler.discard;
import static jdk.incubator.http.HttpClient.Version.HTTP_1_1;
--- a/test/jdk/java/net/httpclient/http2/BasicTest.java Sun Nov 05 17:05:57 2017 +0000
+++ b/test/jdk/java/net/httpclient/http2/BasicTest.java Sun Nov 05 17:32:13 2017 +0000
@@ -26,7 +26,8 @@
* @bug 8087112
* @library /lib/testlibrary server
* @build jdk.testlibrary.SimpleSSLContext
- * @modules jdk.incubator.httpclient/jdk.incubator.http.internal.common
+ * @modules java.base/sun.net.www.http
+ * jdk.incubator.httpclient/jdk.incubator.http.internal.common
* jdk.incubator.httpclient/jdk.incubator.http.internal.frame
* jdk.incubator.httpclient/jdk.incubator.http.internal.hpack
* @run testng/othervm -Djdk.httpclient.HttpClient.log=ssl,requests,responses,errors BasicTest
@@ -39,8 +40,8 @@
import java.nio.file.*;
import java.util.concurrent.*;
import jdk.testlibrary.SimpleSSLContext;
-import static jdk.incubator.http.HttpRequest.BodyProcessor.fromFile;
-import static jdk.incubator.http.HttpRequest.BodyProcessor.fromString;
+import static jdk.incubator.http.HttpRequest.BodyPublisher.fromFile;
+import static jdk.incubator.http.HttpRequest.BodyPublisher.fromString;
import static jdk.incubator.http.HttpResponse.BodyHandler.asFile;
import static jdk.incubator.http.HttpResponse.BodyHandler.asString;
@@ -51,7 +52,8 @@
static int httpPort, httpsPort;
static Http2TestServer httpServer, httpsServer;
static HttpClient client = null;
- static ExecutorService exec;
+ static ExecutorService clientExec;
+ static ExecutorService serverExec;
static SSLContext sslContext;
static String httpURIString, httpsURIString;
@@ -61,11 +63,11 @@
SimpleSSLContext sslct = new SimpleSSLContext();
sslContext = sslct.get();
client = getClient();
- httpServer = new Http2TestServer(false, 0, exec, sslContext);
+ httpServer = new Http2TestServer(false, 0, serverExec, sslContext);
httpServer.addHandler(new Http2EchoHandler(), "/");
httpPort = httpServer.getAddress().getPort();
- httpsServer = new Http2TestServer(true, 0, exec, sslContext);
+ httpsServer = new Http2TestServer(true, 0, serverExec, sslContext);
httpsServer.addHandler(new Http2EchoHandler(), "/");
httpsPort = httpsServer.getAddress().getPort();
@@ -98,15 +100,16 @@
} finally {
httpServer.stop();
httpsServer.stop();
- exec.shutdownNow();
+ //clientExec.shutdown();
}
}
static HttpClient getClient() {
if (client == null) {
- exec = Executors.newCachedThreadPool();
+ serverExec = Executors.newCachedThreadPool();
+ clientExec = Executors.newCachedThreadPool();
client = HttpClient.newBuilder()
- .executor(exec)
+ .executor(clientExec)
.sslContext(sslContext)
.version(HTTP_2)
.build();
@@ -170,11 +173,11 @@
});
response.join();
compareFiles(src, dest);
- System.err.println("DONE");
+ System.err.println("streamTest: DONE");
}
static void paramsTest() throws Exception {
- Http2TestServer server = new Http2TestServer(true, 0, exec, sslContext);
+ Http2TestServer server = new Http2TestServer(true, 0, serverExec, sslContext);
server.addHandler((t -> {
SSLSession s = t.getSSLSession();
String prot = s.getProtocol();
@@ -196,6 +199,7 @@
throw new RuntimeException("paramsTest failed "
+ Integer.toString(stat));
}
+ System.err.println("paramsTest: DONE");
}
static void simpleTest(boolean secure) throws Exception {
@@ -237,6 +241,6 @@
Thread.sleep(100);
}
CompletableFuture.allOf(responses).join();
- System.err.println("DONE");
+ System.err.println("simpleTest: DONE");
}
}
--- a/test/jdk/java/net/httpclient/http2/ErrorTest.java Sun Nov 05 17:05:57 2017 +0000
+++ b/test/jdk/java/net/httpclient/http2/ErrorTest.java Sun Nov 05 17:32:13 2017 +0000
@@ -26,7 +26,8 @@
* @bug 8157105
* @library /lib/testlibrary server
* @build jdk.testlibrary.SimpleSSLContext
- * @modules jdk.incubator.httpclient/jdk.incubator.http.internal.common
+ * @modules java.base/sun.net.www.http
+ * jdk.incubator.httpclient/jdk.incubator.http.internal.common
* jdk.incubator.httpclient/jdk.incubator.http.internal.frame
* jdk.incubator.httpclient/jdk.incubator.http.internal.hpack
* java.security.jgss
@@ -45,7 +46,7 @@
import java.util.concurrent.ExecutorService;
import jdk.testlibrary.SimpleSSLContext;
import static jdk.incubator.http.HttpClient.Version.HTTP_2;
-import static jdk.incubator.http.HttpRequest.BodyProcessor.fromString;
+import static jdk.incubator.http.HttpRequest.BodyPublisher.fromString;
import static jdk.incubator.http.HttpResponse.BodyHandler.discard;
import org.testng.annotations.Test;
--- a/test/jdk/java/net/httpclient/http2/FixedThreadPoolTest.java Sun Nov 05 17:05:57 2017 +0000
+++ b/test/jdk/java/net/httpclient/http2/FixedThreadPoolTest.java Sun Nov 05 17:32:13 2017 +0000
@@ -26,7 +26,8 @@
* @bug 8087112 8177935
* @library /lib/testlibrary server
* @build jdk.testlibrary.SimpleSSLContext
- * @modules jdk.incubator.httpclient/jdk.incubator.http.internal.common
+ * @modules java.base/sun.net.www.http
+ * jdk.incubator.httpclient/jdk.incubator.http.internal.common
* jdk.incubator.httpclient/jdk.incubator.http.internal.frame
* jdk.incubator.httpclient/jdk.incubator.http.internal.hpack
* @run testng/othervm -Djdk.httpclient.HttpClient.log=ssl,requests,responses,errors FixedThreadPoolTest
@@ -39,8 +40,8 @@
import java.nio.file.*;
import java.util.concurrent.*;
import jdk.testlibrary.SimpleSSLContext;
-import static jdk.incubator.http.HttpRequest.BodyProcessor.fromFile;
-import static jdk.incubator.http.HttpRequest.BodyProcessor.fromString;
+import static jdk.incubator.http.HttpRequest.BodyPublisher.fromFile;
+import static jdk.incubator.http.HttpRequest.BodyPublisher.fromString;
import static jdk.incubator.http.HttpResponse.BodyHandler.asFile;
import static jdk.incubator.http.HttpResponse.BodyHandler.asString;
@@ -104,6 +105,13 @@
static HttpClient getClient() {
if (client == null) {
exec = Executors.newCachedThreadPool();
+ // Executor e1 = Executors.newFixedThreadPool(1);
+ // Executor e = (Runnable r) -> e1.execute(() -> {
+ // System.out.println("[" + Thread.currentThread().getName()
+ // + "] Executing: "
+ // + r.getClass().getName());
+ // r.run();
+ // });
client = HttpClient.newBuilder()
.executor(Executors.newFixedThreadPool(2))
.sslContext(sslContext)
--- /dev/null Thu Jan 01 00:00:00 1970 +0000
+++ b/test/jdk/java/net/httpclient/http2/HpackBinaryTestDriver.java Sun Nov 05 17:32:13 2017 +0000
@@ -0,0 +1,34 @@
+/*
+ * Copyright (c) 2017, Oracle and/or its affiliates. All rights reserved.
+ * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER.
+ *
+ * This code is free software; you can redistribute it and/or modify it
+ * under the terms of the GNU General Public License version 2 only, as
+ * published by the Free Software Foundation.
+ *
+ * This code is distributed in the hope that it will be useful, but WITHOUT
+ * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or
+ * FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License
+ * version 2 for more details (a copy is included in the LICENSE file that
+ * accompanied this code).
+ *
+ * You should have received a copy of the GNU General Public License version
+ * 2 along with this work; if not, write to the Free Software Foundation,
+ * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA.
+ *
+ * Please contact Oracle, 500 Oracle Parkway, Redwood Shores, CA 94065 USA
+ * or visit www.oracle.com if you need additional information or have any
+ * questions.
+ */
+
+/*
+ * @test
+ * @bug 8153353
+ * @modules jdk.incubator.httpclient/jdk.incubator.http.internal.hpack
+ * @key randomness
+ * @compile/module=jdk.incubator.httpclient jdk/incubator/http/internal/hpack/SpecHelper.java
+ * @compile/module=jdk.incubator.httpclient jdk/incubator/http/internal/hpack/TestHelper.java
+ * @compile/module=jdk.incubator.httpclient jdk/incubator/http/internal/hpack/BuffersTestingKit.java
+ * @run testng/othervm jdk.incubator.httpclient/jdk.incubator.http.internal.hpack.BinaryPrimitivesTest
+ */
+public class HpackBinaryTestDriver { }
--- a/test/jdk/java/net/httpclient/http2/HpackDriver.java Sun Nov 05 17:05:57 2017 +0000
+++ b/test/jdk/java/net/httpclient/http2/HpackDriver.java Sun Nov 05 17:32:13 2017 +0000
@@ -1,5 +1,5 @@
/*
- * Copyright (c) 2016, Oracle and/or its affiliates. All rights reserved.
+ * Copyright (c) 2016, 2017, Oracle and/or its affiliates. All rights reserved.
* DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER.
*
* This code is free software; you can redistribute it and/or modify it
@@ -29,7 +29,6 @@
* @compile/module=jdk.incubator.httpclient jdk/incubator/http/internal/hpack/SpecHelper.java
* @compile/module=jdk.incubator.httpclient jdk/incubator/http/internal/hpack/TestHelper.java
* @compile/module=jdk.incubator.httpclient jdk/incubator/http/internal/hpack/BuffersTestingKit.java
- * @run testng/othervm jdk.incubator.httpclient/jdk.incubator.http.internal.hpack.BinaryPrimitivesTest
* @run testng/othervm jdk.incubator.httpclient/jdk.incubator.http.internal.hpack.CircularBufferTest
* @run testng/othervm jdk.incubator.httpclient/jdk.incubator.http.internal.hpack.DecoderTest
* @run testng/othervm jdk.incubator.httpclient/jdk.incubator.http.internal.hpack.EncoderTest
--- a/test/jdk/java/net/httpclient/http2/NoBody.java Sun Nov 05 17:05:57 2017 +0000
+++ b/test/jdk/java/net/httpclient/http2/NoBody.java Sun Nov 05 17:32:13 2017 +0000
@@ -26,24 +26,23 @@
* @bug 8161157
* @library /lib/testlibrary server
* @build jdk.testlibrary.SimpleSSLContext
- * @modules jdk.incubator.httpclient/jdk.incubator.http.internal.common
+ * @modules java.base/sun.net.www.http
+ * jdk.incubator.httpclient/jdk.incubator.http.internal.common
* jdk.incubator.httpclient/jdk.incubator.http.internal.frame
* jdk.incubator.httpclient/jdk.incubator.http.internal.hpack
* @run testng/othervm -Djdk.httpclient.HttpClient.log=ssl,frames,errors NoBody
*/
-import java.io.IOException;
import java.net.URI;
import jdk.incubator.http.HttpClient;
import jdk.incubator.http.HttpRequest;
import jdk.incubator.http.HttpResponse;
import javax.net.ssl.SSLContext;
-import javax.net.ssl.SSLParameters;
import java.util.concurrent.Executors;
import java.util.concurrent.ExecutorService;
import jdk.testlibrary.SimpleSSLContext;
import static jdk.incubator.http.HttpClient.Version.HTTP_2;
-import static jdk.incubator.http.HttpRequest.BodyProcessor.fromString;
+import static jdk.incubator.http.HttpRequest.BodyPublisher.fromString;
import static jdk.incubator.http.HttpResponse.BodyHandler.asString;
import org.testng.annotations.Test;
--- a/test/jdk/java/net/httpclient/http2/ProxyTest2.java Sun Nov 05 17:05:57 2017 +0000
+++ b/test/jdk/java/net/httpclient/http2/ProxyTest2.java Sun Nov 05 17:32:13 2017 +0000
@@ -62,7 +62,8 @@
* tunnelling through an HTTP/1.1 proxy.
* @modules jdk.incubator.httpclient
* @library /lib/testlibrary server
- * @modules jdk.incubator.httpclient/jdk.incubator.http.internal.common
+ * @modules java.base/sun.net.www.http
+ * jdk.incubator.httpclient/jdk.incubator.http.internal.common
* jdk.incubator.httpclient/jdk.incubator.http.internal.frame
* jdk.incubator.httpclient/jdk.incubator.http.internal.hpack
* @build jdk.testlibrary.SimpleSSLContext ProxyTest2
--- a/test/jdk/java/net/httpclient/http2/RedirectTest.java Sun Nov 05 17:05:57 2017 +0000
+++ b/test/jdk/java/net/httpclient/http2/RedirectTest.java Sun Nov 05 17:32:13 2017 +0000
@@ -24,34 +24,30 @@
/*
* @test
* @bug 8156514
- * @key intermittent
* @library /lib/testlibrary server
* @build jdk.testlibrary.SimpleSSLContext
- * @modules jdk.incubator.httpclient/jdk.incubator.http.internal.common
- * @modules jdk.incubator.httpclient/jdk.incubator.http.internal.frame
- * @modules jdk.incubator.httpclient/jdk.incubator.http.internal.hpack
+ * @modules java.base/sun.net.www.http
+ * jdk.incubator.httpclient/jdk.incubator.http.internal.common
+ * jdk.incubator.httpclient/jdk.incubator.http.internal.frame
+ * jdk.incubator.httpclient/jdk.incubator.http.internal.hpack
* @run testng/othervm -Djdk.httpclient.HttpClient.log=frames,ssl,requests,responses,errors RedirectTest
*/
import java.net.*;
import jdk.incubator.http.*;
-import static jdk.incubator.http.HttpClient.Version.HTTP_2;
-import java.nio.file.*;
import java.util.concurrent.*;
import java.util.function.*;
import java.util.Arrays;
import java.util.Iterator;
-import static jdk.incubator.http.HttpRequest.BodyProcessor.fromString;
+import org.testng.annotations.Test;
+import static jdk.incubator.http.HttpClient.Version.HTTP_2;
+import static jdk.incubator.http.HttpRequest.BodyPublisher.fromString;
import static jdk.incubator.http.HttpResponse.BodyHandler.asString;
-import org.testng.annotations.Test;
-
-@Test
public class RedirectTest {
static int httpPort, altPort;
static Http2TestServer httpServer, altServer;
static HttpClient client;
- static ExecutorService exec;
static String httpURIString, altURIString1, altURIString2;
@@ -64,21 +60,21 @@
static void initialize() throws Exception {
try {
client = getClient();
- httpServer = new Http2TestServer(false, 0, exec, null);
+ httpServer = new Http2TestServer(false, 0, null, null);
httpPort = httpServer.getAddress().getPort();
- altServer = new Http2TestServer(false, 0, exec, null);
+ altServer = new Http2TestServer(false, 0, null, null);
altPort = altServer.getAddress().getPort();
- // urls are accessed in sequence below
- // first two on different servers. Third on same server
- // as second. So, the client should use the same http connection
+ // urls are accessed in sequence below. The first two are on
+ // different servers. Third on same server as second. So, the
+ // client should use the same http connection.
httpURIString = "http://127.0.0.1:" + httpPort + "/foo/";
altURIString1 = "http://127.0.0.1:" + altPort + "/redir";
altURIString2 = "http://127.0.0.1:" + altPort + "/redir/again";
- httpServer.addHandler(new RedirectHandler(sup(altURIString1)), "/foo");
- altServer.addHandler(new RedirectHandler(sup(altURIString2)), "/redir");
+ httpServer.addHandler(new Http2RedirectHandler(sup(altURIString1)), "/foo");
+ altServer.addHandler(new Http2RedirectHandler(sup(altURIString2)), "/redir");
altServer.addHandler(new Http2EchoHandler(), "/redir/again");
httpServer.start();
@@ -90,7 +86,7 @@
}
}
- @Test(timeOut=3000000)
+ @Test
public static void test() throws Exception {
try {
initialize();
@@ -101,15 +97,12 @@
} finally {
httpServer.stop();
altServer.stop();
- exec.shutdownNow();
}
}
static HttpClient getClient() {
if (client == null) {
- exec = Executors.newCachedThreadPool();
client = HttpClient.newBuilder()
- .executor(exec)
.followRedirects(HttpClient.Redirect.ALWAYS)
.version(HTTP_2)
.build();
@@ -137,18 +130,8 @@
}
}
- static Void compareFiles(Path path1, Path path2) {
- return TestUtil.compareFiles(path1, path2);
- }
-
- static Path tempFile() {
- return TestUtil.tempFile();
- }
-
static final String SIMPLE_STRING = "Hello world Goodbye world";
- static final int FILESIZE = 64 * 1024 + 200;
-
static void simpleTest() throws Exception {
URI uri = getURI();
System.err.println("Request to " + uri);
--- a/test/jdk/java/net/httpclient/http2/ServerPush.java Sun Nov 05 17:05:57 2017 +0000
+++ b/test/jdk/java/net/httpclient/http2/ServerPush.java Sun Nov 05 17:32:13 2017 +0000
@@ -26,10 +26,11 @@
* @bug 8087112 8159814
* @library /lib/testlibrary server
* @build jdk.testlibrary.SimpleSSLContext
- * @modules jdk.incubator.httpclient/jdk.incubator.http.internal.common
+ * @modules java.base/sun.net.www.http
+ * jdk.incubator.httpclient/jdk.incubator.http.internal.common
* jdk.incubator.httpclient/jdk.incubator.http.internal.frame
* jdk.incubator.httpclient/jdk.incubator.http.internal.hpack
- * @run testng/othervm -Djdk.httpclient.HttpClient.log=errors,requests,responses ServerPush
+ * @run testng/othervm -Djdk.internal.httpclient.hpack.debug=true -Djdk.internal.httpclient.debug=true -Djdk.httpclient.HttpClient.log=errors,requests,responses ServerPush
*/
import java.io.*;
@@ -37,7 +38,7 @@
import java.nio.file.*;
import java.nio.file.attribute.*;
import jdk.incubator.http.*;
-import jdk.incubator.http.HttpResponse.MultiProcessor;
+import jdk.incubator.http.HttpResponse.MultiSubscriber;
import jdk.incubator.http.HttpResponse.BodyHandler;
import java.util.*;
import java.util.concurrent.*;
@@ -72,7 +73,7 @@
CompletableFuture<MultiMapResult<Path>> cf =
HttpClient.newBuilder().version(HttpClient.Version.HTTP_2)
.executor(e).build().sendAsync(
- request, MultiProcessor.asMap((req) -> {
+ request, MultiSubscriber.asMap((req) -> {
URI u = req.uri();
Path path = Paths.get(dir.toString(), u.getPath());
try {
--- a/test/jdk/java/net/httpclient/http2/TLSConnection.java Sun Nov 05 17:05:57 2017 +0000
+++ b/test/jdk/java/net/httpclient/http2/TLSConnection.java Sun Nov 05 17:32:13 2017 +0000
@@ -29,19 +29,19 @@
import java.net.URISyntaxException;
import jdk.incubator.http.HttpClient;
import jdk.incubator.http.HttpRequest;
-import jdk.incubator.http.HttpResponse;
+
import javax.net.ssl.SSLParameters;
import javax.net.ssl.SSLSession;
-import static jdk.incubator.http.HttpRequest.BodyProcessor.fromString;
+import static jdk.incubator.http.HttpRequest.BodyPublisher.fromString;
import static jdk.incubator.http.HttpResponse.BodyHandler.asString;
/*
* @test
* @bug 8150769 8157107
- * @key intermittent
* @library server
* @summary Checks that SSL parameters can be set for HTTP/2 connection
- * @modules jdk.incubator.httpclient/jdk.incubator.http.internal.common
+ * @modules java.base/sun.net.www.http
+ * jdk.incubator.httpclient/jdk.incubator.http.internal.common
* jdk.incubator.httpclient/jdk.incubator.http.internal.frame
* jdk.incubator.httpclient/jdk.incubator.http.internal.hpack
* @run main/othervm TLSConnection
--- a/test/jdk/java/net/httpclient/http2/Timeout.java Sun Nov 05 17:05:57 2017 +0000
+++ b/test/jdk/java/net/httpclient/http2/Timeout.java Sun Nov 05 17:32:13 2017 +0000
@@ -29,13 +29,12 @@
import jdk.incubator.http.HttpResponse;
import jdk.incubator.http.HttpTimeoutException;
import java.time.Duration;
-import java.util.concurrent.TimeUnit;
import java.util.concurrent.CompletionException;
import javax.net.ssl.SSLServerSocket;
import javax.net.ssl.SSLParameters;
import javax.net.ssl.SSLServerSocketFactory;
import javax.net.ssl.SSLSocket;
-import static jdk.incubator.http.HttpRequest.BodyProcessor.fromString;
+import static jdk.incubator.http.HttpRequest.BodyPublisher.fromString;
import static jdk.incubator.http.HttpResponse.BodyHandler.asString;
/*
--- a/test/jdk/java/net/httpclient/http2/jdk.incubator.httpclient/jdk/incubator/http/internal/hpack/BinaryPrimitivesTest.java Sun Nov 05 17:05:57 2017 +0000
+++ b/test/jdk/java/net/httpclient/http2/jdk.incubator.httpclient/jdk/incubator/http/internal/hpack/BinaryPrimitivesTest.java Sun Nov 05 17:32:13 2017 +0000
@@ -1,5 +1,5 @@
/*
- * Copyright (c) 2014, 2016, Oracle and/or its affiliates. All rights reserved.
+ * Copyright (c) 2014, 2017, Oracle and/or its affiliates. All rights reserved.
* DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER.
*
* This code is free software; you can redistribute it and/or modify it
@@ -24,6 +24,8 @@
import org.testng.annotations.Test;
+import java.io.IOException;
+import java.io.UncheckedIOException;
import java.nio.ByteBuffer;
import java.nio.CharBuffer;
import java.nio.charset.StandardCharsets;
@@ -80,7 +82,7 @@
// for all x: readInteger(writeInteger(x)) == x
//
@Test
- public void integerIdentity() {
+ public void integerIdentity() throws IOException {
final int MAX_VALUE = 1 << 22;
int totalCases = 0;
int maxFilling = 0;
@@ -119,7 +121,11 @@
Iterable<? extends ByteBuffer> buf = relocateBuffers(injectEmptyBuffers(buffers));
r.configure(N);
for (ByteBuffer b : buf) {
- r.read(b);
+ try {
+ r.read(b);
+ } catch (IOException e) {
+ throw new UncheckedIOException(e);
+ }
}
assertEquals(r.get(), expected);
r.reset();
@@ -155,7 +161,11 @@
if (!written) {
fail("please increase bb size");
}
- r.configure(N).read(concat(buf));
+ try {
+ r.configure(N).read(concat(buf));
+ } catch (IOException e) {
+ throw new UncheckedIOException(e);
+ }
// TODO: check payload here
assertEquals(r.get(), expected);
w.reset();
@@ -172,7 +182,7 @@
// for all x: readString(writeString(x)) == x
//
@Test
- public void stringIdentity() {
+ public void stringIdentity() throws IOException {
final int MAX_STRING_LENGTH = 4096;
ByteBuffer bytes = ByteBuffer.allocate(MAX_STRING_LENGTH + 6); // it takes 6 bytes to encode string length of Integer.MAX_VALUE
CharBuffer chars = CharBuffer.allocate(MAX_STRING_LENGTH);
@@ -241,7 +251,11 @@
if (!written) {
fail("please increase 'bytes' size");
}
- reader.read(concat(buffers), chars);
+ try {
+ reader.read(concat(buffers), chars);
+ } catch (IOException e) {
+ throw new UncheckedIOException(e);
+ }
chars.flip();
assertEquals(chars.toString(), expected);
reader.reset();
@@ -279,7 +293,11 @@
forEachSplit(bytes, (buffers) -> {
for (ByteBuffer buf : buffers) {
int p0 = buf.position();
- reader.read(buf, chars);
+ try {
+ reader.read(buf, chars);
+ } catch (IOException e) {
+ throw new UncheckedIOException(e);
+ }
buf.position(p0);
}
chars.flip();
@@ -333,7 +351,11 @@
private static void verifyRead(byte[] data, int expected, int N) {
ByteBuffer buf = ByteBuffer.wrap(data, 0, data.length);
IntegerReader reader = new IntegerReader();
- reader.configure(N).read(buf);
+ try {
+ reader.configure(N).read(buf);
+ } catch (IOException e) {
+ throw new UncheckedIOException(e);
+ }
assertEquals(expected, reader.get());
}
--- a/test/jdk/java/net/httpclient/http2/jdk.incubator.httpclient/jdk/incubator/http/internal/hpack/DecoderTest.java Sun Nov 05 17:05:57 2017 +0000
+++ b/test/jdk/java/net/httpclient/http2/jdk.incubator.httpclient/jdk/incubator/http/internal/hpack/DecoderTest.java Sun Nov 05 17:32:13 2017 +0000
@@ -1,5 +1,5 @@
/*
- * Copyright (c) 2015, 2016, Oracle and/or its affiliates. All rights reserved.
+ * Copyright (c) 2015, 2017, Oracle and/or its affiliates. All rights reserved.
* DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER.
*
* This code is free software; you can redistribute it and/or modify it
@@ -24,8 +24,8 @@
import org.testng.annotations.Test;
+import java.io.IOException;
import java.io.UncheckedIOException;
-import java.net.ProtocolException;
import java.nio.ByteBuffer;
import java.util.Iterator;
import java.util.LinkedList;
@@ -400,7 +400,7 @@
// This test is missing in the spec
//
@Test
- public void sizeUpdate() {
+ public void sizeUpdate() throws IOException {
Decoder d = new Decoder(4096);
assertEquals(d.getTable().maxSize(), 4096);
d.decode(ByteBuffer.wrap(new byte[]{0b00111110}), true, nopCallback()); // newSize = 30
@@ -421,20 +421,14 @@
b.flip();
{
Decoder d = new Decoder(4096);
- UncheckedIOException ex = assertVoidThrows(UncheckedIOException.class,
+ assertVoidThrows(IOException.class,
() -> d.decode(b, true, (name, value) -> { }));
-
- assertNotNull(ex.getCause());
- assertEquals(ex.getCause().getClass(), ProtocolException.class);
}
b.flip();
{
Decoder d = new Decoder(4096);
- UncheckedIOException ex = assertVoidThrows(UncheckedIOException.class,
+ assertVoidThrows(IOException.class,
() -> d.decode(b, false, (name, value) -> { }));
-
- assertNotNull(ex.getCause());
- assertEquals(ex.getCause().getClass(), ProtocolException.class);
}
}
@@ -445,10 +439,8 @@
(byte) 0b11111111, // indexed
(byte) 0b10011010 // 25 + ...
});
- UncheckedIOException e = assertVoidThrows(UncheckedIOException.class,
+ IOException e = assertVoidThrows(IOException.class,
() -> d.decode(data, true, nopCallback()));
- assertNotNull(e.getCause());
- assertEquals(e.getCause().getClass(), ProtocolException.class);
assertExceptionMessageContains(e, "Unexpected end of header block");
}
@@ -471,10 +463,10 @@
(byte) 0b00000111
});
- IllegalArgumentException e = assertVoidThrows(IllegalArgumentException.class,
+ IOException e = assertVoidThrows(IOException.class,
() -> d.decode(data, true, nopCallback()));
- assertExceptionMessageContains(e, "index=2147483647");
+ assertExceptionMessageContains(e.getCause(), "index=2147483647");
}
@Test
@@ -490,7 +482,7 @@
(byte) 0b00000111
});
- IllegalArgumentException e = assertVoidThrows(IllegalArgumentException.class,
+ IOException e = assertVoidThrows(IOException.class,
() -> d.decode(data, true, nopCallback()));
assertExceptionMessageContains(e, "Integer overflow");
@@ -507,10 +499,8 @@
0b00000000, // but only 3 octets available...
0b00000000 // /
});
- UncheckedIOException e = assertVoidThrows(UncheckedIOException.class,
+ IOException e = assertVoidThrows(IOException.class,
() -> d.decode(data, true, nopCallback()));
- assertNotNull(e.getCause());
- assertEquals(e.getCause().getClass(), ProtocolException.class);
assertExceptionMessageContains(e, "Unexpected end of header block");
}
@@ -527,10 +517,8 @@
0b00000000, // /
0b00000000 // /
});
- UncheckedIOException e = assertVoidThrows(UncheckedIOException.class,
+ IOException e = assertVoidThrows(IOException.class,
() -> d.decode(data, true, nopCallback()));
- assertNotNull(e.getCause());
- assertEquals(e.getCause().getClass(), ProtocolException.class);
assertExceptionMessageContains(e, "Unexpected end of header block");
}
@@ -547,7 +535,7 @@
0b00011001, 0b01001101, (byte) 0b11111111,
(byte) 0b11111111, (byte) 0b11111111, (byte) 0b11111100
});
- IllegalArgumentException e = assertVoidThrows(IllegalArgumentException.class,
+ IOException e = assertVoidThrows(IOException.class,
() -> d.decode(data, true, nopCallback()));
assertExceptionMessageContains(e, "Encountered EOS");
@@ -566,7 +554,7 @@
0b00011001, 0b01001101, (byte) 0b11111111
// len("aei") + len(padding) = (5 + 5 + 5) + (9)
});
- IllegalArgumentException e = assertVoidThrows(IllegalArgumentException.class,
+ IOException e = assertVoidThrows(IOException.class,
() -> d.decode(data, true, nopCallback()));
assertExceptionMessageContains(e, "Padding is too long", "len=9");
@@ -597,7 +585,7 @@
(byte) 0b10000011, // huffman=true, length=3
0b00011001, 0b01111010, (byte) 0b11111110
});
- IllegalArgumentException e = assertVoidThrows(IllegalArgumentException.class,
+ IOException e = assertVoidThrows(IOException.class,
() -> d.decode(data, true, nopCallback()));
assertExceptionMessageContains(e, "Not a EOS prefix");
@@ -648,13 +636,17 @@
Decoder d = supplier.get();
do {
ByteBuffer n = i.next();
- d.decode(n, !i.hasNext(), (name, value) -> {
- if (value == null) {
- actual.add(name.toString());
- } else {
- actual.add(name + ": " + value);
- }
- });
+ try {
+ d.decode(n, !i.hasNext(), (name, value) -> {
+ if (value == null) {
+ actual.add(name.toString());
+ } else {
+ actual.add(name + ": " + value);
+ }
+ });
+ } catch (IOException e) {
+ throw new UncheckedIOException(e);
+ }
} while (i.hasNext());
assertEquals(d.getTable().getStateString(), expectedHeaderTable);
assertEquals(actual.stream().collect(Collectors.joining("\n")), expectedHeaderList);
@@ -671,13 +663,17 @@
ByteBuffer source = SpecHelper.toBytes(hexdump);
List<String> actual = new LinkedList<>();
- d.decode(source, true, (name, value) -> {
- if (value == null) {
- actual.add(name.toString());
- } else {
- actual.add(name + ": " + value);
- }
- });
+ try {
+ d.decode(source, true, (name, value) -> {
+ if (value == null) {
+ actual.add(name.toString());
+ } else {
+ actual.add(name + ": " + value);
+ }
+ });
+ } catch (IOException e) {
+ throw new UncheckedIOException(e);
+ }
assertEquals(d.getTable().getStateString(), expectedHeaderTable);
assertEquals(actual.stream().collect(Collectors.joining("\n")), expectedHeaderList);
--- a/test/jdk/java/net/httpclient/http2/jdk.incubator.httpclient/jdk/incubator/http/internal/hpack/EncoderTest.java Sun Nov 05 17:05:57 2017 +0000
+++ b/test/jdk/java/net/httpclient/http2/jdk.incubator.httpclient/jdk/incubator/http/internal/hpack/EncoderTest.java Sun Nov 05 17:32:13 2017 +0000
@@ -1,5 +1,5 @@
/*
- * Copyright (c) 2014, 2016, Oracle and/or its affiliates. All rights reserved.
+ * Copyright (c) 2014, 2017, Oracle and/or its affiliates. All rights reserved.
* DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER.
*
* This code is free software; you can redistribute it and/or modify it
@@ -24,6 +24,7 @@
import org.testng.annotations.Test;
+import java.io.IOException;
import java.nio.Buffer;
import java.nio.ByteBuffer;
import java.util.ArrayList;
@@ -520,7 +521,7 @@
}
@Test
- public void initialSizeUpdateDefaultEncoder() {
+ public void initialSizeUpdateDefaultEncoder() throws IOException {
Function<Integer, Encoder> e = Encoder::new;
testSizeUpdate(e, 1024, asList(), asList(0));
testSizeUpdate(e, 1024, asList(1024), asList(0));
@@ -531,7 +532,7 @@
}
@Test
- public void initialSizeUpdateCustomEncoder() {
+ public void initialSizeUpdateCustomEncoder() throws IOException {
Function<Integer, Encoder> e = EncoderTest::newCustomEncoder;
testSizeUpdate(e, 1024, asList(), asList(1024));
testSizeUpdate(e, 1024, asList(1024), asList(1024));
@@ -542,7 +543,7 @@
}
@Test
- public void seriesOfSizeUpdatesDefaultEncoder() {
+ public void seriesOfSizeUpdatesDefaultEncoder() throws IOException {
Function<Integer, Encoder> e = c -> {
Encoder encoder = new Encoder(c);
drainInitialUpdate(encoder);
@@ -563,7 +564,7 @@
// https://tools.ietf.org/html/rfc7541#section-4.2
//
@Test
- public void seriesOfSizeUpdatesCustomEncoder() {
+ public void seriesOfSizeUpdatesCustomEncoder() throws IOException {
Function<Integer, Encoder> e = c -> {
Encoder encoder = newCustomEncoder(c);
drainInitialUpdate(encoder);
@@ -638,7 +639,7 @@
private void testSizeUpdate(Function<Integer, Encoder> encoder,
int initialSize,
List<Integer> updates,
- List<Integer> expected) {
+ List<Integer> expected) throws IOException {
Encoder e = encoder.apply(initialSize);
updates.forEach(e::setMaxCapacity);
ByteBuffer b = ByteBuffer.allocate(64);
--- a/test/jdk/java/net/httpclient/http2/jdk.incubator.httpclient/jdk/incubator/http/internal/hpack/HeaderTableTest.java Sun Nov 05 17:05:57 2017 +0000
+++ b/test/jdk/java/net/httpclient/http2/jdk.incubator.httpclient/jdk/incubator/http/internal/hpack/HeaderTableTest.java Sun Nov 05 17:32:13 2017 +0000
@@ -1,5 +1,5 @@
/*
- * Copyright (c) 2014, 2016, Oracle and/or its affiliates. All rights reserved.
+ * Copyright (c) 2014, 2017, Oracle and/or its affiliates. All rights reserved.
* DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER.
*
* This code is free software; you can redistribute it and/or modify it
@@ -116,7 +116,7 @@
@Test
public void staticData() {
- HeaderTable table = new HeaderTable(0);
+ HeaderTable table = new HeaderTable(0, HPACK.getLogger());
Map<Integer, HeaderField> staticHeaderFields = createStaticEntries();
Map<String, Integer> minimalIndexes = new HashMap<>();
@@ -159,7 +159,7 @@
@Test
public void constructorSetsMaxSize() {
int size = rnd.nextInt(64);
- HeaderTable t = new HeaderTable(size);
+ HeaderTable t = new HeaderTable(size, HPACK.getLogger());
assertEquals(t.size(), 0);
assertEquals(t.maxSize(), size);
}
@@ -169,13 +169,13 @@
int maxSize = -(rnd.nextInt(100) + 1); // [-100, -1]
IllegalArgumentException e =
assertVoidThrows(IllegalArgumentException.class,
- () -> new HeaderTable(0).setMaxSize(maxSize));
+ () -> new HeaderTable(0, HPACK.getLogger()).setMaxSize(maxSize));
assertExceptionMessageContains(e, "maxSize");
}
@Test
public void zeroMaximumSize() {
- HeaderTable table = new HeaderTable(0);
+ HeaderTable table = new HeaderTable(0, HPACK.getLogger());
table.setMaxSize(0);
assertEquals(table.maxSize(), 0);
}
@@ -183,41 +183,41 @@
@Test
public void negativeIndex() {
int idx = -(rnd.nextInt(256) + 1); // [-256, -1]
- IllegalArgumentException e =
- assertVoidThrows(IllegalArgumentException.class,
- () -> new HeaderTable(0).get(idx));
+ IndexOutOfBoundsException e =
+ assertVoidThrows(IndexOutOfBoundsException.class,
+ () -> new HeaderTable(0, HPACK.getLogger()).get(idx));
assertExceptionMessageContains(e, "index");
}
@Test
public void zeroIndex() {
- IllegalArgumentException e =
- assertThrows(IllegalArgumentException.class,
- () -> new HeaderTable(0).get(0));
+ IndexOutOfBoundsException e =
+ assertThrows(IndexOutOfBoundsException.class,
+ () -> new HeaderTable(0, HPACK.getLogger()).get(0));
assertExceptionMessageContains(e, "index");
}
@Test
public void length() {
- HeaderTable table = new HeaderTable(0);
+ HeaderTable table = new HeaderTable(0, HPACK.getLogger());
assertEquals(table.length(), STATIC_TABLE_LENGTH);
}
@Test
public void indexOutsideStaticRange() {
- HeaderTable table = new HeaderTable(0);
+ HeaderTable table = new HeaderTable(0, HPACK.getLogger());
int idx = table.length() + (rnd.nextInt(256) + 1);
- IllegalArgumentException e =
- assertThrows(IllegalArgumentException.class,
+ IndexOutOfBoundsException e =
+ assertThrows(IndexOutOfBoundsException.class,
() -> table.get(idx));
assertExceptionMessageContains(e, "index");
}
@Test
public void entryPutAfterStaticArea() {
- HeaderTable table = new HeaderTable(256);
+ HeaderTable table = new HeaderTable(256, HPACK.getLogger());
int idx = table.length() + 1;
- assertThrows(IllegalArgumentException.class, () -> table.get(idx));
+ assertThrows(IndexOutOfBoundsException.class, () -> table.get(idx));
byte[] bytes = new byte[32];
rnd.nextBytes(bytes);
@@ -232,13 +232,13 @@
@Test
public void staticTableHasZeroSize() {
- HeaderTable table = new HeaderTable(0);
+ HeaderTable table = new HeaderTable(0, HPACK.getLogger());
assertEquals(0, table.size());
}
@Test
public void lowerIndexPriority() {
- HeaderTable table = new HeaderTable(256);
+ HeaderTable table = new HeaderTable(256, HPACK.getLogger());
int oldLength = table.length();
table.put("bender", "rodriguez");
table.put("bender", "rodriguez");
@@ -251,7 +251,7 @@
@Test
public void lowerIndexPriority2() {
- HeaderTable table = new HeaderTable(256);
+ HeaderTable table = new HeaderTable(256, HPACK.getLogger());
int oldLength = table.length();
int idx = rnd.nextInt(oldLength) + 1;
HeaderField f = table.get(idx);
@@ -267,7 +267,7 @@
@Test
public void fifo() {
- HeaderTable t = new HeaderTable(Integer.MAX_VALUE);
+ HeaderTable t = new HeaderTable(Integer.MAX_VALUE, HPACK.getLogger());
// Let's add a series of header fields
int NUM_HEADERS = 32;
for (int i = 1; i <= NUM_HEADERS; i++) {
@@ -293,7 +293,7 @@
@Test
public void indexOf() {
- HeaderTable t = new HeaderTable(Integer.MAX_VALUE);
+ HeaderTable t = new HeaderTable(Integer.MAX_VALUE, HPACK.getLogger());
// Let's put a series of header fields
int NUM_HEADERS = 32;
for (int i = 1; i <= NUM_HEADERS; i++) {
@@ -333,11 +333,13 @@
}
private void testToString0() {
- HeaderTable table = new HeaderTable(0);
+ HeaderTable table = new HeaderTable(0, HPACK.getLogger());
{
- table.setMaxSize(2048);
- String expected =
- format("entries: %d; used %s/%s (%.1f%%)", 0, 0, 2048, 0.0);
+ int maxSize = 2048;
+ table.setMaxSize(maxSize);
+ String expected = format(
+ "dynamic length: %s, full length: %s, used space: %s/%s (%.1f%%)",
+ 0, STATIC_TABLE_LENGTH, 0, maxSize, 0.0);
assertEquals(expected, table.toString());
}
@@ -353,8 +355,9 @@
int used = name.length() + value.length() + 32;
double ratio = used * 100.0 / size;
- String expected =
- format("entries: 1; used %s/%s (%.1f%%)", used, size, ratio);
+ String expected = format(
+ "dynamic length: %s, full length: %s, used space: %s/%s (%.1f%%)",
+ 1, STATIC_TABLE_LENGTH + 1, used, size, ratio);
assertEquals(expected, s);
}
@@ -364,14 +367,15 @@
table.put(":status", "");
String s = table.toString();
String expected =
- format("entries: %d; used %s/%s (%.1f%%)", 2, 78, 78, 100.0);
+ format("dynamic length: %s, full length: %s, used space: %s/%s (%.1f%%)",
+ 2, STATIC_TABLE_LENGTH + 2, 78, 78, 100.0);
assertEquals(expected, s);
}
}
@Test
public void stateString() {
- HeaderTable table = new HeaderTable(256);
+ HeaderTable table = new HeaderTable(256, HPACK.getLogger());
table.put("custom-key", "custom-header");
// @formatter:off
assertEquals("[ 1] (s = 55) custom-key: custom-header\n" +
--- a/test/jdk/java/net/httpclient/http2/jdk.incubator.httpclient/jdk/incubator/http/internal/hpack/HuffmanTest.java Sun Nov 05 17:05:57 2017 +0000
+++ b/test/jdk/java/net/httpclient/http2/jdk.incubator.httpclient/jdk/incubator/http/internal/hpack/HuffmanTest.java Sun Nov 05 17:32:13 2017 +0000
@@ -1,5 +1,5 @@
/*
- * Copyright (c) 2015, 2016, Oracle and/or its affiliates. All rights reserved.
+ * Copyright (c) 2015, 2017, Oracle and/or its affiliates. All rights reserved.
* DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER.
*
* This code is free software; you can redistribute it and/or modify it
@@ -24,6 +24,8 @@
import org.testng.annotations.Test;
+import java.io.IOException;
+import java.io.UncheckedIOException;
import java.nio.ByteBuffer;
import java.util.Stack;
import java.util.regex.Matcher;
@@ -302,7 +304,7 @@
// @formatter:on
@Test
- public void read_table() {
+ public void read_table() throws IOException {
Pattern line = Pattern.compile(
"\\(\\s*(?<ascii>\\d+)\\s*\\)\\s*(?<binary>(\\|(0|1)+)+)\\s*" +
"(?<hex>[0-9a-zA-Z]+)\\s*\\[\\s*(?<len>\\d+)\\s*\\]");
@@ -555,7 +557,11 @@
private static void read(String hexdump, String decoded) {
ByteBuffer source = SpecHelper.toBytes(hexdump);
Appendable actual = new StringBuilder();
- new Huffman.Reader().read(source, actual, true);
+ try {
+ new Huffman.Reader().read(source, actual, true);
+ } catch (IOException e) {
+ throw new UncheckedIOException(e);
+ }
assertEquals(actual.toString(), decoded);
}
--- a/test/jdk/java/net/httpclient/http2/server/BodyInputStream.java Sun Nov 05 17:05:57 2017 +0000
+++ b/test/jdk/java/net/httpclient/http2/server/BodyInputStream.java Sun Nov 05 17:32:13 2017 +0000
@@ -88,7 +88,7 @@
return null;
}
ByteBufferReference[] data = df.getData();
- int len = Utils.remaining(data);
+ long len = Utils.remaining(data);
if ((len == 0) && eof) {
return null;
}
--- /dev/null Thu Jan 01 00:00:00 1970 +0000
+++ b/test/jdk/java/net/httpclient/http2/server/Http2RedirectHandler.java Sun Nov 05 17:32:13 2017 +0000
@@ -0,0 +1,50 @@
+/*
+ * Copyright (c) 2016, 2017, Oracle and/or its affiliates. All rights reserved.
+ * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER.
+ *
+ * This code is free software; you can redistribute it and/or modify it
+ * under the terms of the GNU General Public License version 2 only, as
+ * published by the Free Software Foundation.
+ *
+ * This code is distributed in the hope that it will be useful, but WITHOUT
+ * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or
+ * FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License
+ * version 2 for more details (a copy is included in the LICENSE file that
+ * accompanied this code).
+ *
+ * You should have received a copy of the GNU General Public License version
+ * 2 along with this work; if not, write to the Free Software Foundation,
+ * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA.
+ *
+ * Please contact Oracle, 500 Oracle Parkway, Redwood Shores, CA 94065 USA
+ * or visit www.oracle.com if you need additional information or have any
+ * questions.
+ */
+
+import java.io.IOException;
+import java.io.InputStream;
+import java.util.function.Supplier;
+import jdk.incubator.http.internal.common.HttpHeadersImpl;
+
+public class Http2RedirectHandler implements Http2Handler {
+
+ final Supplier<String> supplier;
+
+ public Http2RedirectHandler(Supplier<String> redirectSupplier) {
+ supplier = redirectSupplier;
+ }
+
+ @Override
+ public void handle(Http2TestExchange t) throws IOException {
+ try (InputStream is = t.getRequestBody()) {
+ is.readAllBytes();
+ String location = supplier.get();
+ System.err.println("RedirectHandler received request to " + t.getRequestURI());
+ System.err.println("Redirecting to: " + location);
+ HttpHeadersImpl map1 = t.getResponseHeaders();
+ map1.addHeader("Location", location);
+ t.sendResponseHeaders(301, 0);
+ t.close();
+ }
+ }
+}
--- a/test/jdk/java/net/httpclient/http2/server/Http2TestServerConnection.java Sun Nov 05 17:05:57 2017 +0000
+++ b/test/jdk/java/net/httpclient/http2/server/Http2TestServerConnection.java Sun Nov 05 17:32:13 2017 +0000
@@ -1,5 +1,5 @@
/*
- * Copyright (c) 2015, 2016, Oracle and/or its affiliates. All rights reserved.
+ * Copyright (c) 2015, 2017, Oracle and/or its affiliates. All rights reserved.
* DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER.
*
* This code is free software; you can redistribute it and/or modify it
@@ -38,17 +38,26 @@
import java.util.*;
import java.util.concurrent.ExecutorService;
import java.util.function.Consumer;
-
import jdk.incubator.http.internal.common.ByteBufferReference;
-import jdk.incubator.http.internal.frame.FramesDecoder;
-
import jdk.incubator.http.internal.common.BufferHandler;
import jdk.incubator.http.internal.common.HttpHeadersImpl;
import jdk.incubator.http.internal.common.Queue;
-import jdk.incubator.http.internal.frame.*;
+import jdk.incubator.http.internal.frame.DataFrame;
+import jdk.incubator.http.internal.frame.FramesDecoder;
+import jdk.incubator.http.internal.frame.FramesEncoder;
+import jdk.incubator.http.internal.frame.GoAwayFrame;
+import jdk.incubator.http.internal.frame.HeaderFrame;
+import jdk.incubator.http.internal.frame.HeadersFrame;
+import jdk.incubator.http.internal.frame.Http2Frame;
+import jdk.incubator.http.internal.frame.PushPromiseFrame;
+import jdk.incubator.http.internal.frame.ResetFrame;
+import jdk.incubator.http.internal.frame.SettingsFrame;
+import jdk.incubator.http.internal.frame.WindowUpdateFrame;
import jdk.incubator.http.internal.hpack.Decoder;
import jdk.incubator.http.internal.hpack.DecodingCallback;
import jdk.incubator.http.internal.hpack.Encoder;
+import sun.net.www.http.ChunkedInputStream;
+import sun.net.www.http.HttpClient;
import static jdk.incubator.http.internal.frame.SettingsFrame.HEADER_TABLE_SIZE;
/**
@@ -88,6 +97,7 @@
this.streams = Collections.synchronizedMap(new HashMap<>());
this.outputQ = new Queue<>();
this.socket = socket;
+ this.socket.setTcpNoDelay(true);
this.serverSettings = SettingsFrame.getDefaultSettings();
this.exec = server.exec;
this.secure = server.secure;
@@ -252,6 +262,20 @@
int start = buf.arrayOffset() + buf.position();
c += buf.remaining();
os.write(ba, start, buf.remaining());
+
+// System.out.println("writing byte at a time");
+// while (buf.hasRemaining()) {
+// byte b = buf.get();
+// os.write(b);
+// os.flush();
+// try {
+// Thread.sleep(1);
+// } catch(InterruptedException e) {
+// UncheckedIOException uie = new UncheckedIOException(new IOException(""));
+// uie.addSuppressed(e);
+// throw uie;
+// }
+// }
}
os.flush();
//System.err.printf("TestServer: wrote %d bytes\n", c);
@@ -276,9 +300,11 @@
frame.streamid(0);
outputQ.put(frame);
return;
+ } else if (f instanceof GoAwayFrame) {
+ System.err.println("Closing: "+ f.toString());
+ close();
}
- //System.err.println("TestServer: Received ---> " + f.toString());
- throw new UnsupportedOperationException("Not supported yet.");
+ throw new UnsupportedOperationException("Not supported yet: " + f.toString());
}
void sendWindowUpdates(int len, int streamid) throws IOException {
@@ -290,7 +316,7 @@
outputQ.put(wup);
}
- HttpHeadersImpl decodeHeaders(List<HeaderFrame> frames) {
+ HttpHeadersImpl decodeHeaders(List<HeaderFrame> frames) throws IOException {
HttpHeadersImpl headers = new HttpHeadersImpl();
DecodingCallback cb = (name, value) -> {
@@ -428,7 +454,7 @@
System.err.printf("TestServer: %s %s\n", method, path);
HttpHeadersImpl rspheaders = new HttpHeadersImpl();
int winsize = clientSettings.getParameter(
- SettingsFrame.INITIAL_WINDOW_SIZE);
+ SettingsFrame.INITIAL_WINDOW_SIZE);
//System.err.println ("Stream window size = " + winsize);
final InputStream bis;
@@ -497,7 +523,7 @@
} else {
if (q == null && !pushStreams.contains(stream)) {
System.err.printf("Non Headers frame received with"+
- " non existing stream (%d) ", frame.streamid());
+ " non existing stream (%d) ", frame.streamid());
System.err.println(frame);
continue;
}
@@ -721,13 +747,25 @@
String readHttp1Request() throws IOException {
String headers = readUntil(CRLF + CRLF);
int clen = getContentLength(headers);
- // read the content.
- byte[] buf = new byte[clen];
- is.readNBytes(buf, 0, clen);
+ byte[] buf;
+ if (clen >= 0) {
+ // HTTP/1.1 fixed length content ( may be 0 ), read it
+ buf = new byte[clen];
+ is.readNBytes(buf, 0, clen);
+ } else {
+ // HTTP/1.1 chunked data, read it
+ buf = readChunkedInputStream(is);
+ }
String body = new String(buf, StandardCharsets.US_ASCII);
return headers + body;
}
+ // This is a quick hack to get a chunked input stream reader.
+ private static byte[] readChunkedInputStream(InputStream is) throws IOException {
+ ChunkedInputStream cis = new ChunkedInputStream(is, new HttpClient() {}, null);
+ return cis.readAllBytes();
+ }
+
void sendHttp1Response(int code, String msg, String... headers) throws IOException {
StringBuilder sb = new StringBuilder();
sb.append("HTTP/1.1 ")
@@ -795,13 +833,13 @@
* @param amount
*/
synchronized void obtainConnectionWindow(int amount) throws InterruptedException {
- while (amount > 0) {
- int n = Math.min(amount, sendWindow);
- amount -= n;
- sendWindow -= n;
- if (amount > 0)
- wait();
- }
+ while (amount > 0) {
+ int n = Math.min(amount, sendWindow);
+ amount -= n;
+ sendWindow -= n;
+ if (amount > 0)
+ wait();
+ }
}
synchronized void updateConnectionWindow(int amount) {
@@ -823,9 +861,9 @@
}
static class NullInputStream extends InputStream {
- static final NullInputStream INSTANCE = new NullInputStream();
- private NullInputStream() {}
- public int read() { return -1; }
- public int available() { return 0; }
- }
+ static final NullInputStream INSTANCE = new NullInputStream();
+ private NullInputStream() {}
+ public int read() { return -1; }
+ public int available() { return 0; }
+ }
}
--- a/test/jdk/java/net/httpclient/http2/server/RedirectHandler.java Sun Nov 05 17:05:57 2017 +0000
+++ /dev/null Thu Jan 01 00:00:00 1970 +0000
@@ -1,66 +0,0 @@
-/*
- * Copyright (c) 2016, Oracle and/or its affiliates. All rights reserved.
- * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER.
- *
- * This code is free software; you can redistribute it and/or modify it
- * under the terms of the GNU General Public License version 2 only, as
- * published by the Free Software Foundation.
- *
- * This code is distributed in the hope that it will be useful, but WITHOUT
- * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or
- * FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License
- * version 2 for more details (a copy is included in the LICENSE file that
- * accompanied this code).
- *
- * You should have received a copy of the GNU General Public License version
- * 2 along with this work; if not, write to the Free Software Foundation,
- * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA.
- *
- * Please contact Oracle, 500 Oracle Parkway, Redwood Shores, CA 94065 USA
- * or visit www.oracle.com if you need additional information or have any
- * questions.
- */
-
-import java.io.*;
-import java.util.function.Supplier;
-import jdk.incubator.http.internal.common.HttpHeadersImpl;
-import static java.nio.charset.StandardCharsets.ISO_8859_1;
-
-public class RedirectHandler implements Http2Handler {
-
- final Supplier<String> supplier;
-
- public RedirectHandler(Supplier<String> redirectSupplier) {
- supplier = redirectSupplier;
- }
-
- static String consume(InputStream is) throws IOException {
- byte[] b = new byte[1024];
- int i;
- StringBuilder sb = new StringBuilder();
-
- while ((i=is.read(b)) != -1) {
- sb.append(new String(b, 0, i, ISO_8859_1));
- }
- is.close();
- return sb.toString();
- }
-
- @Override
- public void handle(Http2TestExchange t) throws IOException {
- try {
- consume(t.getRequestBody());
- String location = supplier.get();
- System.err.println("RedirectHandler received request to " + t.getRequestURI());
- System.err.println("Redirecting to: " + location);
- HttpHeadersImpl map1 = t.getResponseHeaders();
- map1.addHeader("Location", location);
- t.sendResponseHeaders(301, 0);
- // return the number of bytes received (no echo)
- t.close();
- } catch (Throwable e) {
- e.printStackTrace();
- throw new IOException(e);
- }
- }
-}
--- a/test/jdk/java/net/httpclient/security/Driver.java Sun Nov 05 17:05:57 2017 +0000
+++ b/test/jdk/java/net/httpclient/security/Driver.java Sun Nov 05 17:32:13 2017 +0000
@@ -34,7 +34,7 @@
* @compile ../ProxyServer.java
* @build Security
*
- * @run driver/timeout=90 Driver
+ * @run main/othervm Driver
*/
/**
@@ -142,11 +142,15 @@
.redirectErrorStream(true);
String cmdLine = cmd.stream().collect(Collectors.joining(" "));
+ long start = System.currentTimeMillis();
Process child = processBuilder.start();
Logger log = new Logger(cmdLine, child, testClasses);
log.start();
retval = child.waitFor();
- System.out.println("retval = " + retval);
+ long elapsed = System.currentTimeMillis() - start;
+ System.out.println("Security " + testnum
+ + ": retval = " + retval
+ + ", duration=" + elapsed+" ms");
}
if (retval != 0) {
Thread.sleep(2000);
--- a/test/jdk/java/net/httpclient/security/Security.java Sun Nov 05 17:05:57 2017 +0000
+++ b/test/jdk/java/net/httpclient/security/Security.java Sun Nov 05 17:32:13 2017 +0000
@@ -82,7 +82,6 @@
import java.util.concurrent.CompletionException;
import java.util.concurrent.CompletionStage;
import java.util.concurrent.ExecutionException;
-import java.util.concurrent.Executor;
import java.util.concurrent.Executors;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Flow;
@@ -297,15 +296,15 @@
CompletableFuture<HttpResponse<String>> cf =
client.sendAsync(request, new HttpResponse.BodyHandler<String>() {
@Override
- public HttpResponse.BodyProcessor<String> apply(int status, HttpHeaders responseHeaders) {
- final HttpResponse.BodyProcessor<String> stproc = sth.apply(status, responseHeaders);
- return new HttpResponse.BodyProcessor<String>() {
+ public HttpResponse.BodySubscriber<String> apply(int status, HttpHeaders responseHeaders) {
+ final HttpResponse.BodySubscriber<String> stproc = sth.apply(status, responseHeaders);
+ return new HttpResponse.BodySubscriber<String>() {
@Override
public CompletionStage<String> getBody() {
return stproc.getBody();
}
@Override
- public void onNext(ByteBuffer item) {
+ public void onNext(List<ByteBuffer> item) {
SecurityManager sm = System.getSecurityManager();
// should succeed.
sm.checkPermission(new RuntimePermission("foobar"));
@@ -337,6 +336,9 @@
Throwable t = e.getCause();
if (t instanceof SecurityException)
throw (SecurityException)t;
+ else if ((t instanceof IOException)
+ && (t.getCause() instanceof SecurityException))
+ throw ((SecurityException)t.getCause());
else
throw new RuntimeException(t);
}
@@ -419,12 +421,6 @@
} finally {
s1.stop(0);
executor.shutdownNow();
- for (HttpClient client : clients) {
- Executor e = client.executor();
- if (e instanceof ExecutorService) {
- ((ExecutorService)e).shutdownNow();
- }
- }
}
}
--- /dev/null Thu Jan 01 00:00:00 1970 +0000
+++ b/test/jdk/java/net/httpclient/security/filePerms/FileProcessorPermissionTest.java Sun Nov 05 17:32:13 2017 +0000
@@ -0,0 +1,138 @@
+/*
+ * Copyright (c) 2017, Oracle and/or its affiliates. All rights reserved.
+ * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER.
+ *
+ * This code is free software; you can redistribute it and/or modify it
+ * under the terms of the GNU General Public License version 2 only, as
+ * published by the Free Software Foundation.
+ *
+ * This code is distributed in the hope that it will be useful, but WITHOUT
+ * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or
+ * FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License
+ * version 2 for more details (a copy is included in the LICENSE file that
+ * accompanied this code).
+ *
+ * You should have received a copy of the GNU General Public License version
+ * 2 along with this work; if not, write to the Free Software Foundation,
+ * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA.
+ *
+ * Please contact Oracle, 500 Oracle Parkway, Redwood Shores, CA 94065 USA
+ * or visit www.oracle.com if you need additional information or have any
+ * questions.
+ */
+
+/*
+ * @test
+ * @summary Basic checks for SecurityException from body processors APIs
+ * @run testng/othervm/java.security.policy=httpclient.policy FileProcessorPermissionTest
+ */
+
+import java.io.FilePermission;
+import java.nio.file.Path;
+import java.nio.file.Paths;
+import java.security.AccessControlContext;
+import java.security.AccessController;
+import java.security.Permission;
+import java.security.Permissions;
+import java.security.PrivilegedActionException;
+import java.security.PrivilegedExceptionAction;
+import java.security.ProtectionDomain;
+import java.util.List;
+import jdk.incubator.http.HttpRequest;
+import jdk.incubator.http.HttpResponse;
+import org.testng.annotations.Test;
+import static java.nio.file.StandardOpenOption.*;
+import static org.testng.Assert.*;
+
+public class FileProcessorPermissionTest {
+
+ static final String testSrc = System.getProperty("test.src", ".");
+ static final Path fromFilePath = Paths.get(testSrc, "FileProcessorPermissionTest.java");
+ static final Path asFilePath = Paths.get(testSrc, "asFile.txt");
+ static final Path CWD = Paths.get(".");
+ static final Class<SecurityException> SE = SecurityException.class;
+
+ static AccessControlContext withPermissions(Permission... perms) {
+ Permissions p = new Permissions();
+ for (Permission perm : perms) {
+ p.add(perm);
+ }
+ ProtectionDomain pd = new ProtectionDomain(null, p);
+ return new AccessControlContext(new ProtectionDomain[]{ pd });
+ }
+
+ static AccessControlContext noPermissions() {
+ return withPermissions(/*empty*/);
+ }
+
+ @Test
+ public void test() throws Exception {
+ List<PrivilegedExceptionAction<?>> list = List.of(
+ () -> HttpRequest.BodyPublisher.fromFile(fromFilePath),
+
+ () -> HttpResponse.BodyHandler.asFile(asFilePath),
+ () -> HttpResponse.BodyHandler.asFile(asFilePath, CREATE),
+ () -> HttpResponse.BodyHandler.asFile(asFilePath, CREATE, WRITE),
+ () -> HttpResponse.BodyHandler.asFile(asFilePath, CREATE, WRITE, READ),
+ () -> HttpResponse.BodyHandler.asFile(asFilePath, CREATE, WRITE, READ, DELETE_ON_CLOSE),
+
+ () -> HttpResponse.BodyHandler.asFileDownload(CWD),
+ () -> HttpResponse.BodyHandler.asFileDownload(CWD, CREATE),
+ () -> HttpResponse.BodyHandler.asFileDownload(CWD, CREATE, WRITE),
+ () -> HttpResponse.BodyHandler.asFileDownload(CWD, CREATE, WRITE, READ),
+ () -> HttpResponse.BodyHandler.asFileDownload(CWD, CREATE, WRITE, READ, DELETE_ON_CLOSE),
+
+ // TODO: what do these even mean by themselves, maybe ok means nothing?
+ () -> HttpResponse.BodyHandler.asFile(asFilePath, DELETE_ON_CLOSE),
+ () -> HttpResponse.BodyHandler.asFile(asFilePath, READ)
+ );
+
+ // sanity, just run http ( no security manager )
+ System.setSecurityManager(null);
+ try {
+ for (PrivilegedExceptionAction pa : list) {
+ AccessController.doPrivileged(pa);
+ }
+ } finally {
+ System.setSecurityManager(new SecurityManager());
+ }
+
+ // Run with all permissions, i.e. no further restrictions than test's AllPermission
+ for (PrivilegedExceptionAction pa : list) {
+ try {
+ assert System.getSecurityManager() != null;
+ AccessController.doPrivileged(pa, null, new Permission[] { });
+ } catch (PrivilegedActionException pae) {
+ fail("UNEXPECTED Exception:" + pae);
+ pae.printStackTrace();
+ }
+ }
+
+ // Run with limited permissions, i.e. just what is required
+ AccessControlContext minimalACC = withPermissions(
+ new FilePermission(fromFilePath.toString() , "read"),
+ new FilePermission(asFilePath.toString(), "read,write,delete"),
+ new FilePermission(CWD.toString(), "read,write,delete")
+ );
+ for (PrivilegedExceptionAction pa : list) {
+ try {
+ assert System.getSecurityManager() != null;
+ AccessController.doPrivileged(pa, minimalACC);
+ } catch (PrivilegedActionException pae) {
+ fail("UNEXPECTED Exception:" + pae);
+ pae.printStackTrace();
+ }
+ }
+
+ // Run with NO permissions, i.e. expect SecurityException
+ for (PrivilegedExceptionAction pa : list) {
+ try {
+ assert System.getSecurityManager() != null;
+ AccessController.doPrivileged(pa, noPermissions());
+ fail("EXPECTED SecurityException");
+ } catch (SecurityException expected) {
+ System.out.println("Caught expected SE:" + expected);
+ }
+ }
+ }
+}
--- /dev/null Thu Jan 01 00:00:00 1970 +0000
+++ b/test/jdk/java/net/httpclient/security/filePerms/httpclient.policy Sun Nov 05 17:32:13 2017 +0000
@@ -0,0 +1,46 @@
+grant codeBase "jrt:/jdk.incubator.httpclient" {
+ permission java.lang.RuntimePermission "accessClassInPackage.sun.net";
+ permission java.lang.RuntimePermission "accessClassInPackage.sun.net.util";
+ permission java.lang.RuntimePermission "accessClassInPackage.sun.net.www";
+ permission java.lang.RuntimePermission "accessClassInPackage.jdk.internal.misc";
+
+ // ## why is SP not good enough. Check API @throws signatures and impl
+ permission java.net.SocketPermission "*","connect,resolve";
+ permission java.net.URLPermission "http:*","*:*";
+ permission java.net.URLPermission "https:*","*:*";
+ permission java.net.URLPermission "ws:*","*:*";
+ permission java.net.URLPermission "wss:*","*:*";
+ permission java.net.URLPermission "socket:*","CONNECT"; // proxy
+
+ // For request/response body processors, fromFile, asFile
+ permission java.io.FilePermission "<<ALL FILES>>","read,write,delete";
+
+ // ## look at the different property names!
+ permission java.util.PropertyPermission "jdk.httpclient.HttpClient.log","read"; // name!
+ permission java.util.PropertyPermission "jdk.httpclient.auth.retrylimit","read";
+ permission java.util.PropertyPermission "jdk.httpclient.connectionWindowSize","read";
+ permission java.util.PropertyPermission "jdk.httpclient.enablepush","read";
+ permission java.util.PropertyPermission "jdk.httpclient.hpack.maxheadertablesize","read";
+ permission java.util.PropertyPermission "jdk.httpclient.keepalive.timeout","read";
+ permission java.util.PropertyPermission "jdk.httpclient.maxframesize","read";
+ permission java.util.PropertyPermission "jdk.httpclient.maxstreams","read";
+ permission java.util.PropertyPermission "jdk.httpclient.redirects.retrylimit","read";
+ permission java.util.PropertyPermission "jdk.httpclient.windowsize","read";
+ permission java.util.PropertyPermission "jdk.httpclient.bufsize","read";
+ permission java.util.PropertyPermission "jdk.httpclient.internal.selector.timeout","read";
+ permission java.util.PropertyPermission "jdk.internal.httpclient.debug","read";
+ permission java.util.PropertyPermission "jdk.internal.httpclient.hpack.debug","read";
+ permission java.util.PropertyPermission "jdk.internal.httpclient.hpack.log.level","read";
+
+ // ## these permissions do not appear in the NetPermission spec!!! JDK bug?
+ permission java.net.NetPermission "getSSLContext";
+ permission java.net.NetPermission "setSSLContext";
+
+ permission java.security.SecurityPermission "createAccessControlContext";
+};
+
+// bootstrap to get the test going, it will do its own restrictions
+grant codeBase "file:${test.classes}/*" {
+ permission java.security.AllPermission;
+};
+
--- /dev/null Thu Jan 01 00:00:00 1970 +0000
+++ b/test/jdk/java/net/httpclient/whitebox/ConnectionPoolTestDriver.java Sun Nov 05 17:32:13 2017 +0000
@@ -0,0 +1,32 @@
+/*
+ * Copyright (c) 2017, Oracle and/or its affiliates. All rights reserved.
+ * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER.
+ *
+ * This code is free software; you can redistribute it and/or modify it
+ * under the terms of the GNU General Public License version 2 only, as
+ * published by the Free Software Foundation.
+ *
+ * This code is distributed in the hope that it will be useful, but WITHOUT
+ * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or
+ * FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License
+ * version 2 for more details (a copy is included in the LICENSE file that
+ * accompanied this code).
+ *
+ * You should have received a copy of the GNU General Public License version
+ * 2 along with this work; if not, write to the Free Software Foundation,
+ * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA.
+ *
+ * Please contact Oracle, 500 Oracle Parkway, Redwood Shores, CA 94065 USA
+ * or visit www.oracle.com if you need additional information or have any
+ * questions.
+ */
+
+/*
+ * @test
+ * @bug 8187044 8187111
+ * @summary Verifies that the ConnectionPool correctly handle
+ * connection deadlines and purges the right connections
+ * from the cache.
+ * @modules jdk.incubator.httpclient java.management
+ * @run main/othervm --add-reads jdk.incubator.httpclient=java.management jdk.incubator.httpclient/jdk.incubator.http.ConnectionPoolTest
+ */
--- /dev/null Thu Jan 01 00:00:00 1970 +0000
+++ b/test/jdk/java/net/httpclient/whitebox/DemandTestDriver.java Sun Nov 05 17:32:13 2017 +0000
@@ -0,0 +1,28 @@
+/*
+ * Copyright (c) 2017, Oracle and/or its affiliates. All rights reserved.
+ * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER.
+ *
+ * This code is free software; you can redistribute it and/or modify it
+ * under the terms of the GNU General Public License version 2 only, as
+ * published by the Free Software Foundation.
+ *
+ * This code is distributed in the hope that it will be useful, but WITHOUT
+ * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or
+ * FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License
+ * version 2 for more details (a copy is included in the LICENSE file that
+ * accompanied this code).
+ *
+ * You should have received a copy of the GNU General Public License version
+ * 2 along with this work; if not, write to the Free Software Foundation,
+ * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA.
+ *
+ * Please contact Oracle, 500 Oracle Parkway, Redwood Shores, CA 94065 USA
+ * or visit www.oracle.com if you need additional information or have any
+ * questions.
+ */
+
+/*
+ * @test
+ * @modules jdk.incubator.httpclient/jdk.incubator.http.internal.common
+ * @run testng jdk.incubator.httpclient/jdk.incubator.http.internal.common.DemandTest
+ */
--- a/test/jdk/java/net/httpclient/whitebox/Driver.java Sun Nov 05 17:05:57 2017 +0000
+++ b/test/jdk/java/net/httpclient/whitebox/Driver.java Sun Nov 05 17:32:13 2017 +0000
@@ -23,10 +23,9 @@
/*
* @test
- * @bug 8151299 8164704 8187044
- * @modules jdk.incubator.httpclient java.management
+ * @bug 8151299 8164704
+ * @modules jdk.incubator.httpclient
* @run testng jdk.incubator.httpclient/jdk.incubator.http.SelectorTest
* @run testng jdk.incubator.httpclient/jdk.incubator.http.RawChannelTest
* @run testng jdk.incubator.httpclient/jdk.incubator.http.ResponseHeadersTest
- * @run main/othervm --add-reads jdk.incubator.httpclient=java.management jdk.incubator.httpclient/jdk.incubator.http.ConnectionPoolTest
*/
--- /dev/null Thu Jan 01 00:00:00 1970 +0000
+++ b/test/jdk/java/net/httpclient/whitebox/FlowTestDriver.java Sun Nov 05 17:32:13 2017 +0000
@@ -0,0 +1,28 @@
+/*
+ * Copyright (c) 2016, 2017, Oracle and/or its affiliates. All rights reserved.
+ * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER.
+ *
+ * This code is free software; you can redistribute it and/or modify it
+ * under the terms of the GNU General Public License version 2 only, as
+ * published by the Free Software Foundation.
+ *
+ * This code is distributed in the hope that it will be useful, but WITHOUT
+ * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or
+ * FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License
+ * version 2 for more details (a copy is included in the LICENSE file that
+ * accompanied this code).
+ *
+ * You should have received a copy of the GNU General Public License version
+ * 2 along with this work; if not, write to the Free Software Foundation,
+ * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA.
+ *
+ * Please contact Oracle, 500 Oracle Parkway, Redwood Shores, CA 94065 USA
+ * or visit www.oracle.com if you need additional information or have any
+ * questions.
+ */
+
+/*
+ * @test
+ * @modules jdk.incubator.httpclient
+ * @run testng jdk.incubator.httpclient/jdk.incubator.http.FlowTest
+ */
--- /dev/null Thu Jan 01 00:00:00 1970 +0000
+++ b/test/jdk/java/net/httpclient/whitebox/Http1HeaderParserTestDriver.java Sun Nov 05 17:32:13 2017 +0000
@@ -0,0 +1,28 @@
+/*
+ * Copyright (c) 2017, Oracle and/or its affiliates. All rights reserved.
+ * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER.
+ *
+ * This code is free software; you can redistribute it and/or modify it
+ * under the terms of the GNU General Public License version 2 only, as
+ * published by the Free Software Foundation.
+ *
+ * This code is distributed in the hope that it will be useful, but WITHOUT
+ * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or
+ * FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License
+ * version 2 for more details (a copy is included in the LICENSE file that
+ * accompanied this code).
+ *
+ * You should have received a copy of the GNU General Public License version
+ * 2 along with this work; if not, write to the Free Software Foundation,
+ * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA.
+ *
+ * Please contact Oracle, 500 Oracle Parkway, Redwood Shores, CA 94065 USA
+ * or visit www.oracle.com if you need additional information or have any
+ * questions.
+ */
+
+/*
+ * @test
+ * @modules jdk.incubator.httpclient
+ * @run testng jdk.incubator.httpclient/jdk.incubator.http.Http1HeaderParserTest
+ */
--- /dev/null Thu Jan 01 00:00:00 1970 +0000
+++ b/test/jdk/java/net/httpclient/whitebox/SSLTubeTestDriver.java Sun Nov 05 17:32:13 2017 +0000
@@ -0,0 +1,30 @@
+/*
+ * Copyright (c) 2017, Oracle and/or its affiliates. All rights reserved.
+ * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER.
+ *
+ * This code is free software; you can redistribute it and/or modify it
+ * under the terms of the GNU General Public License version 2 only, as
+ * published by the Free Software Foundation.
+ *
+ * This code is distributed in the hope that it will be useful, but WITHOUT
+ * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or
+ * FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License
+ * version 2 for more details (a copy is included in the LICENSE file that
+ * accompanied this code).
+ *
+ * You should have received a copy of the GNU General Public License version
+ * 2 along with this work; if not, write to the Free Software Foundation,
+ * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA.
+ *
+ * Please contact Oracle, 500 Oracle Parkway, Redwood Shores, CA 94065 USA
+ * or visit www.oracle.com if you need additional information or have any
+ * questions.
+ */
+
+/*
+ * @test
+ * @modules jdk.incubator.httpclient
+ * @ignore
+ */
+
+ // FIXME * @run testng jdk.incubator.httpclient/jdk.incubator.http.SSLTubeTest
--- /dev/null Thu Jan 01 00:00:00 1970 +0000
+++ b/test/jdk/java/net/httpclient/whitebox/WrapperTestDriver.java Sun Nov 05 17:32:13 2017 +0000
@@ -0,0 +1,28 @@
+/*
+ * Copyright (c) 2016, 2017, Oracle and/or its affiliates. All rights reserved.
+ * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER.
+ *
+ * This code is free software; you can redistribute it and/or modify it
+ * under the terms of the GNU General Public License version 2 only, as
+ * published by the Free Software Foundation.
+ *
+ * This code is distributed in the hope that it will be useful, but WITHOUT
+ * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or
+ * FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License
+ * version 2 for more details (a copy is included in the LICENSE file that
+ * accompanied this code).
+ *
+ * You should have received a copy of the GNU General Public License version
+ * 2 along with this work; if not, write to the Free Software Foundation,
+ * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA.
+ *
+ * Please contact Oracle, 500 Oracle Parkway, Redwood Shores, CA 94065 USA
+ * or visit www.oracle.com if you need additional information or have any
+ * questions.
+ */
+
+/*
+ * @test
+ * @modules jdk.incubator.httpclient
+ * @run testng jdk.incubator.httpclient/jdk.incubator.http.WrapperTest
+ */
--- a/test/jdk/java/net/httpclient/whitebox/jdk.incubator.httpclient/jdk/incubator/http/ConnectionPoolTest.java Sun Nov 05 17:05:57 2017 +0000
+++ b/test/jdk/java/net/httpclient/whitebox/jdk.incubator.httpclient/jdk/incubator/http/ConnectionPoolTest.java Sun Nov 05 17:32:13 2017 +0000
@@ -25,27 +25,31 @@
import java.io.IOException;
import java.lang.management.ManagementFactory;
-import java.lang.ref.Reference;
-import java.lang.ref.ReferenceQueue;
-import java.lang.ref.WeakReference;
import java.net.Authenticator;
import java.net.CookieManager;
import java.net.InetSocketAddress;
import java.net.ProxySelector;
import java.nio.ByteBuffer;
import java.nio.channels.SocketChannel;
+import java.util.List;
import java.util.Optional;
+import java.util.Random;
import java.util.concurrent.CompletableFuture;
import java.util.concurrent.Executor;
+import java.util.concurrent.Flow;
+import java.util.stream.IntStream;
+import java.time.Instant;
+import java.time.temporal.ChronoUnit;
import javax.net.ssl.SSLContext;
import javax.net.ssl.SSLParameters;
import jdk.incubator.http.internal.common.ByteBufferReference;
+import jdk.incubator.http.internal.common.FlowTube;
/**
- * @summary Verifies that the ConnectionPool won't prevent an HttpClient
- * from being GC'ed. Verifies that the ConnectionPool has at most
- * one CacheCleaner thread running.
- * @bug 8187044
+ * @summary Verifies that the ConnectionPool correctly handle
+ * connection deadlines and purges the right connections
+ * from the cache.
+ * @bug 8187044 8187111
* @author danielfuchs
*/
public class ConnectionPoolTest {
@@ -69,96 +73,95 @@
HttpClient client = new HttpClientStub(pool);
InetSocketAddress proxy = InetSocketAddress.createUnresolved("bar", 80);
System.out.println("Adding 10 connections to pool");
- for (int i=0; i<10; i++) {
- InetSocketAddress addr = InetSocketAddress.createUnresolved("foo"+i, 80);
- HttpConnection c1 = new HttpConnectionStub(client, addr, proxy, true);
- pool.returnToPool(c1);
+ Random random = new Random();
+
+ final int count = 20;
+ Instant now = Instant.now().truncatedTo(ChronoUnit.SECONDS);
+ int[] keepAlives = new int[count];
+ HttpConnectionStub[] connections = new HttpConnectionStub[count];
+ long purge = pool.purgeExpiredConnectionsAndReturnNextDeadline(now);
+ long expected = 0;
+ if (purge != expected) {
+ throw new RuntimeException("Bad purge delay: " + purge
+ + ", expected " + expected);
}
- while (getActiveCleaners() == 0) {
- System.out.println("Waiting for cleaner to start");
- Thread.sleep(10);
- }
- System.out.println("Active CacheCleaners: " + getActiveCleaners());
- if (getActiveCleaners() > 1) {
- throw new RuntimeException("Too many CacheCleaner active: "
- + getActiveCleaners());
- }
- System.out.println("Removing 9 connections from pool");
- for (int i=0; i<9; i++) {
+ expected = Long.MAX_VALUE;
+ for (int i=0; i<count; i++) {
InetSocketAddress addr = InetSocketAddress.createUnresolved("foo"+i, 80);
- HttpConnection c2 = pool.getConnection(true, addr, proxy);
- if (c2 == null) {
- throw new RuntimeException("connection not found for " + addr);
- }
- }
- System.out.println("Active CacheCleaners: " + getActiveCleaners());
- if (getActiveCleaners() != 1) {
- throw new RuntimeException("Wrong number of CacheCleaner active: "
- + getActiveCleaners());
- }
- System.out.println("Removing last connection from pool");
- for (int i=9; i<10; i++) {
- InetSocketAddress addr = InetSocketAddress.createUnresolved("foo"+i, 80);
- HttpConnection c2 = pool.getConnection(true, addr, proxy);
- if (c2 == null) {
- throw new RuntimeException("connection not found for " + addr);
+ keepAlives[i] = random.nextInt(10) * 10 + 10;
+ connections[i] = new HttpConnectionStub(client, addr, proxy, true);
+ System.out.println("Adding connection: " + now
+ + " keepAlive: " + keepAlives[i]
+ + " /" + connections[i]);
+ pool.returnToPool(connections[i], now, keepAlives[i]);
+ expected = Math.min(expected, keepAlives[i] * 1000);
+ purge = pool.purgeExpiredConnectionsAndReturnNextDeadline(now);
+ if (purge != expected) {
+ throw new RuntimeException("Bad purge delay: " + purge
+ + ", expected " + expected);
}
}
- System.out.println("Active CacheCleaners: " + getActiveCleaners()
- + " (may be 0 or may still be 1)");
- if (getActiveCleaners() > 1) {
- throw new RuntimeException("Too many CacheCleaner active: "
- + getActiveCleaners());
+ int min = IntStream.of(keepAlives).min().getAsInt();
+ int max = IntStream.of(keepAlives).max().getAsInt();
+ int mean = (min + max)/2;
+ System.out.println("min=" + min + ", max=" + max + ", mean=" + mean);
+ purge = pool.purgeExpiredConnectionsAndReturnNextDeadline(now);
+ System.out.println("first purge would be in " + purge + " ms");
+ if (Math.abs(purge/1000 - min) > 0) {
+ throw new RuntimeException("expected " + min + " got " + purge/1000);
}
- InetSocketAddress addr = InetSocketAddress.createUnresolved("foo", 80);
- HttpConnection c = new HttpConnectionStub(client, addr, proxy, true);
- System.out.println("Adding/Removing one connection from pool 20 times in a loop");
- for (int i=0; i<20; i++) {
- pool.returnToPool(c);
- HttpConnection c2 = pool.getConnection(true, addr, proxy);
- if (c2 == null) {
- throw new RuntimeException("connection not found for " + addr);
- }
- if (c2 != c) {
- throw new RuntimeException("wrong connection found for " + addr);
- }
- }
- if (getActiveCleaners() > 1) {
- throw new RuntimeException("Too many CacheCleaner active: "
- + getActiveCleaners());
+ long opened = java.util.stream.Stream.of(connections)
+ .filter(HttpConnectionStub::connected).count();
+ if (opened != count) {
+ throw new RuntimeException("Opened: expected "
+ + count + " got " + opened);
}
- ReferenceQueue<HttpClient> queue = new ReferenceQueue<>();
- WeakReference<HttpClient> weak = new WeakReference<>(client, queue);
- System.gc();
- Reference.reachabilityFence(pool);
- client = null; pool = null; c = null;
- while (true) {
- long cleaners = getActiveCleaners();
- System.out.println("Waiting for GC to release stub HttpClient;"
- + " active cache cleaners: " + cleaners);
- System.gc();
- Reference<?> ref = queue.remove(1000);
- if (ref == weak) {
- System.out.println("Stub HttpClient GC'ed");
- break;
- }
- }
- while (getActiveCleaners() > 0) {
- System.out.println("Waiting for CacheCleaner to stop");
- Thread.sleep(1000);
- }
- System.out.println("Active CacheCleaners: "
- + getActiveCleaners());
-
- if (getActiveCleaners() > 0) {
- throw new RuntimeException("Too many CacheCleaner active: "
- + getActiveCleaners());
+ purge = mean * 1000;
+ System.out.println("start purging at " + purge + " ms");
+ Instant next = now;
+ do {
+ System.out.println("next purge is in " + purge + " ms");
+ next = next.plus(purge, ChronoUnit.MILLIS);
+ purge = pool.purgeExpiredConnectionsAndReturnNextDeadline(next);
+ long k = now.until(next, ChronoUnit.SECONDS);
+ System.out.println("now is " + k + "s from start");
+ for (int i=0; i<count; i++) {
+ if (connections[i].connected() != (k < keepAlives[i])) {
+ throw new RuntimeException("Bad connection state for "
+ + i
+ + "\n\t connected=" + connections[i].connected()
+ + "\n\t keepAlive=" + keepAlives[i]
+ + "\n\t elapsed=" + k);
+ }
+ }
+ } while (purge > 0);
+ opened = java.util.stream.Stream.of(connections)
+ .filter(HttpConnectionStub::connected).count();
+ if (opened != 0) {
+ throw new RuntimeException("Closed: expected "
+ + count + " got "
+ + (count-opened));
}
}
+
static <T> T error() {
throw new InternalError("Should not reach here: wrong test assumptions!");
}
+ static class FlowTubeStub implements FlowTube {
+ final HttpConnectionStub conn;
+ FlowTubeStub(HttpConnectionStub conn) { this.conn = conn; }
+ @Override
+ public void onSubscribe(Flow.Subscription subscription) { }
+ @Override public void onError(Throwable error) { error(); }
+ @Override public void onComplete() { error(); }
+ @Override public void onNext(List<ByteBuffer> item) { error();}
+ @Override
+ public void subscribe(Flow.Subscriber<? super List<ByteBuffer>> subscriber) {
+ }
+ @Override public boolean isFinished() { return conn.closed; }
+ }
+
// Emulates an HttpConnection that has a strong reference to its HttpClient.
static class HttpConnectionStub extends HttpConnection {
@@ -172,50 +175,54 @@
this.proxy = proxy;
this.secured = secured;
this.client = client;
+ this.flow = new FlowTubeStub(this);
}
- InetSocketAddress proxy;
- InetSocketAddress address;
- boolean secured;
- ConnectionPool.CacheKey key;
- HttpClient client;
+ final InetSocketAddress proxy;
+ final InetSocketAddress address;
+ final boolean secured;
+ final ConnectionPool.CacheKey key;
+ final HttpClient client;
+ final FlowTubeStub flow;
+ volatile boolean closed;
// All these return something
- @Override boolean connected() {return true;}
+ @Override boolean connected() {return !closed;}
@Override boolean isSecure() {return secured;}
@Override boolean isProxied() {return proxy!=null;}
@Override ConnectionPool.CacheKey cacheKey() {return key;}
- @Override public void close() {}
@Override void shutdownInput() throws IOException {}
@Override void shutdownOutput() throws IOException {}
+ @Override
+ public void close() {
+ closed=true;
+ System.out.println("closed: " + this);
+ }
+ @Override
public String toString() {
return "HttpConnectionStub: " + address + " proxy: " + proxy;
}
// All these throw errors
- @Override
- public void connect() throws IOException, InterruptedException {error();}
+ @Override public HttpPublisher publisher() {return error();}
@Override public CompletableFuture<Void> connectAsync() {return error();}
@Override SocketChannel channel() {return error();}
- @Override void flushAsync() throws IOException {error();}
- @Override
- protected ByteBuffer readImpl() throws IOException {return error();}
- @Override CompletableFuture<Void> whenReceivingResponse() {return error();}
+ @Override public void flushAsync() throws IOException {error();}
@Override
- long write(ByteBuffer[] buffers, int start, int number) throws IOException {
- throw (Error)error();
- }
- @Override
- long write(ByteBuffer buffer) throws IOException {throw (Error)error();}
- @Override
- void writeAsync(ByteBufferReference[] buffers) throws IOException {
+ public void writeAsync(ByteBufferReference[] buffers) throws IOException {
error();
}
@Override
- void writeAsyncUnordered(ByteBufferReference[] buffers)
+ public void writeAsyncUnordered(ByteBufferReference[] buffers)
throws IOException {
error();
}
+ @Override
+ HttpConnection.DetachedConnectionChannel detachChannel() {
+ return error();
+ }
+ @Override
+ FlowTube getConnectionFlow() {return flow;}
}
// Emulates an HttpClient that has a strong reference to its connection pool.
static class HttpClientStub extends HttpClient {
@@ -227,10 +234,10 @@
@Override public HttpClient.Redirect followRedirects() {return error();}
@Override public Optional<ProxySelector> proxy() {return error();}
@Override public SSLContext sslContext() {return error();}
- @Override public Optional<SSLParameters> sslParameters() {return error();}
+ @Override public SSLParameters sslParameters() {return error();}
@Override public Optional<Authenticator> authenticator() {return error();}
@Override public HttpClient.Version version() {return HttpClient.Version.HTTP_1_1;}
- @Override public Executor executor() {return error();}
+ @Override public Optional<Executor> executor() {return error();}
@Override
public <T> HttpResponse<T> send(HttpRequest req,
HttpResponse.BodyHandler<T> responseBodyHandler)
@@ -244,7 +251,7 @@
}
@Override
public <U, T> CompletableFuture<U> sendAsync(HttpRequest req,
- HttpResponse.MultiProcessor<U, T> multiProcessor) {
+ HttpResponse.MultiSubscriber<U, T> multiSubscriber) {
return error();
}
}
--- /dev/null Thu Jan 01 00:00:00 1970 +0000
+++ b/test/jdk/java/net/httpclient/whitebox/jdk.incubator.httpclient/jdk/incubator/http/FlowTest.java Sun Nov 05 17:32:13 2017 +0000
@@ -0,0 +1,506 @@
+/*
+ * Copyright (c) 2017, Oracle and/or its affiliates. All rights reserved.
+ * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER.
+ *
+ * This code is free software; you can redistribute it and/or modify it
+ * under the terms of the GNU General Public License version 2 only, as
+ * published by the Free Software Foundation.
+ *
+ * This code is distributed in the hope that it will be useful, but WITHOUT
+ * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or
+ * FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License
+ * version 2 for more details (a copy is included in the LICENSE file that
+ * accompanied this code).
+ *
+ * You should have received a copy of the GNU General Public License version
+ * 2 along with this work; if not, write to the Free Software Foundation,
+ * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA.
+ *
+ * Please contact Oracle, 500 Oracle Parkway, Redwood Shores, CA 94065 USA
+ * or visit www.oracle.com if you need additional information or have any
+ * questions.
+ */
+
+package jdk.incubator.http;
+
+import java.io.BufferedOutputStream;
+import java.io.File;
+import java.io.FileInputStream;
+import java.io.IOException;
+import java.io.InputStream;
+import java.io.OutputStream;
+import java.net.Socket;
+import java.nio.ByteBuffer;
+import java.security.KeyManagementException;
+import java.security.KeyStore;
+import java.security.KeyStoreException;
+import java.security.NoSuchAlgorithmException;
+import java.security.UnrecoverableKeyException;
+import java.security.cert.CertificateException;
+import java.util.List;
+import java.util.Random;
+import java.util.StringTokenizer;
+import java.util.concurrent.BlockingQueue;
+import java.util.concurrent.CompletableFuture;
+import java.util.concurrent.ExecutorService;
+import java.util.concurrent.Executors;
+import java.util.concurrent.Flow;
+import java.util.concurrent.Flow.Subscriber;
+import java.util.concurrent.LinkedBlockingQueue;
+import java.util.concurrent.SubmissionPublisher;
+import java.util.concurrent.atomic.AtomicInteger;
+import java.util.concurrent.atomic.AtomicLong;
+import javax.net.ssl.KeyManagerFactory;
+import javax.net.ssl.*;
+import javax.net.ssl.TrustManagerFactory;
+import jdk.incubator.http.internal.common.Utils;
+import org.testng.annotations.Test;
+import jdk.incubator.http.internal.common.SSLFlowDelegate;
+
+@Test
+public class FlowTest {
+
+ private final SubmissionPublisher<List<ByteBuffer>> srcPublisher;
+ private final ExecutorService executor;
+ private static final long COUNTER = 3000;
+ private static final int LONGS_PER_BUF = 800;
+ static final long TOTAL_LONGS = COUNTER * LONGS_PER_BUF;
+ public static final ByteBuffer SENTINEL = ByteBuffer.allocate(0);
+ static volatile String alpn;
+
+ private final CompletableFuture<Void> completion;
+
+ public FlowTest() throws IOException {
+ executor = Executors.newCachedThreadPool();
+ srcPublisher = new SubmissionPublisher<>(executor, 20,
+ this::handlePublisherException);
+ SSLContext ctx = (new SimpleSSLContext()).get();
+ SSLEngine engineClient = ctx.createSSLEngine();
+ SSLParameters params = ctx.getSupportedSSLParameters();
+ params.setApplicationProtocols(new String[]{"proto1", "proto2"}); // server will choose proto2
+ params.setProtocols(new String[]{"TLSv1.2"}); // TODO: This is essential. Needs to be protocol impl
+ engineClient.setSSLParameters(params);
+ engineClient.setUseClientMode(true);
+ completion = new CompletableFuture<>();
+ SSLLoopbackSubscriber looper = new SSLLoopbackSubscriber(ctx, executor);
+ looper.start();
+ EndSubscriber end = new EndSubscriber(TOTAL_LONGS, completion);
+ SSLFlowDelegate sslClient = new SSLFlowDelegate(engineClient, executor, end, looper);
+ // going to measure how long handshake takes
+ final long start = System.currentTimeMillis();
+ sslClient.alpn().whenComplete((String s, Throwable t) -> {
+ if (t != null)
+ t.printStackTrace();
+ long endTime = System.currentTimeMillis();
+ alpn = s;
+ System.out.println("ALPN: " + alpn);
+ long period = (endTime - start);
+ System.out.printf("Handshake took %d ms\n", period);
+ });
+ Subscriber<List<ByteBuffer>> reader = sslClient.upstreamReader();
+ Subscriber<List<ByteBuffer>> writer = sslClient.upstreamWriter();
+ looper.setReturnSubscriber(reader);
+ // now connect all the pieces
+ srcPublisher.subscribe(writer);
+ String aa = sslClient.alpn().join();
+ System.out.println("AAALPN = " + aa);
+ }
+
+ static Random rand = new Random();
+
+ static int randomRange(int lower, int upper) {
+ if (lower > upper)
+ throw new IllegalArgumentException("lower > upper");
+ int diff = upper - lower;
+ int r = lower + rand.nextInt(diff);
+ return r - (r % 8); // round down to multiple of 8 (align for longs)
+ }
+
+ private void handlePublisherException(Object o, Throwable t) {
+ System.out.println("Src Publisher exception");
+ t.printStackTrace(System.out);
+ }
+
+ private static ByteBuffer getBuffer(long startingAt) {
+ ByteBuffer buf = ByteBuffer.allocate(LONGS_PER_BUF * 8);
+ for (int j = 0; j < LONGS_PER_BUF; j++) {
+ buf.putLong(startingAt++);
+ }
+ buf.flip();
+ return buf;
+ }
+
+ @Test
+ public void run() {
+ long count = 0;
+ System.out.printf("Submitting %d buffer arrays\n", COUNTER);
+ System.out.printf("LoopCount should be %d\n", TOTAL_LONGS);
+ for (long i = 0; i < COUNTER; i++) {
+ ByteBuffer b = getBuffer(count);
+ count += LONGS_PER_BUF;
+ srcPublisher.submit(List.of(b));
+ }
+ System.out.println("Finished submission. Waiting for loopback");
+ srcPublisher.close();
+ try {
+ completion.join();
+ if (!alpn.equals("proto2")) {
+ throw new RuntimeException("wrong alpn received");
+ }
+ System.out.println("OK");
+ } finally {
+ executor.shutdownNow();
+ }
+ }
+
+/*
+ public static void main(String[]args) throws Exception {
+ FlowTest test = new FlowTest();
+ test.run();
+ }
+*/
+
+ /**
+ * This Subscriber simulates an SSL loopback network. The object itself
+ * accepts outgoing SSL encrypted data which is looped back via two sockets
+ * (one of which is an SSLSocket emulating a server). The method
+ * {@link #setReturnSubscriber(java.util.concurrent.Flow.Subscriber) }
+ * is used to provide the Subscriber which feeds the incoming side
+ * of SSLFlowDelegate. Three threads are used to implement this behavior
+ * and a SubmissionPublisher drives the incoming read side.
+ * <p>
+ * A thread reads from the buffer, writes
+ * to the client j.n.Socket which is connected to a SSLSocket operating
+ * in server mode. A second thread loops back data read from the SSLSocket back to the
+ * client again. A third thread reads the client socket and pushes the data to
+ * a SubmissionPublisher that drives the reader side of the SSLFlowDelegate
+ */
+ static class SSLLoopbackSubscriber implements Subscriber<List<ByteBuffer>> {
+ private final BlockingQueue<ByteBuffer> buffer;
+ private final Socket clientSock;
+ private final SSLSocket serverSock;
+ private final Thread thread1, thread2, thread3;
+ private volatile Flow.Subscription clientSubscription;
+ private final SubmissionPublisher<List<ByteBuffer>> publisher;
+
+ SSLLoopbackSubscriber(SSLContext ctx, ExecutorService exec) throws IOException {
+ SSLServerSocketFactory fac = ctx.getServerSocketFactory();
+ SSLServerSocket serv = (SSLServerSocket) fac.createServerSocket(0);
+ SSLParameters params = serv.getSSLParameters();
+ params.setApplicationProtocols(new String[]{"proto2"});
+ serv.setSSLParameters(params);
+
+
+ int serverPort = serv.getLocalPort();
+ clientSock = new Socket("127.0.0.1", serverPort);
+ serverSock = (SSLSocket) serv.accept();
+ this.buffer = new LinkedBlockingQueue<>();
+ thread1 = new Thread(this::clientWriter, "clientWriter");
+ thread2 = new Thread(this::serverLoopback, "serverLoopback");
+ thread3 = new Thread(this::clientReader, "clientReader");
+ publisher = new SubmissionPublisher<>(exec, Flow.defaultBufferSize(),
+ this::handlePublisherException);
+ SSLFlowDelegate.Monitor.add(this::monitor);
+ }
+
+ public void start() {
+ thread1.start();
+ thread2.start();
+ thread3.start();
+ }
+
+ private void handlePublisherException(Object o, Throwable t) {
+ System.out.println("Loopback Publisher exception");
+ t.printStackTrace(System.out);
+ }
+
+ private final AtomicInteger readCount = new AtomicInteger();
+
+ // reads off the SSLSocket the data from the "server"
+ private void clientReader() {
+ try {
+ InputStream is = clientSock.getInputStream();
+ final int bufsize = FlowTest.randomRange(512, 16 * 1024);
+ System.out.println("clientReader: bufsize = " + bufsize);
+ while (true) {
+ byte[] buf = new byte[bufsize];
+ int n = is.read(buf);
+ if (n == -1) {
+ System.out.println("clientReader close");
+ publisher.close();
+ Utils.sleep(2000);
+ Utils.close(is, clientSock);
+ return;
+ }
+ ByteBuffer bb = ByteBuffer.wrap(buf, 0, n);
+ readCount.addAndGet(n);
+ publisher.submit(List.of(bb));
+ }
+ } catch (Throwable e) {
+ e.printStackTrace();
+ Utils.close(clientSock);
+ }
+ }
+
+ // writes the encrypted data from SSLFLowDelegate to the j.n.Socket
+ // which is connected to the SSLSocket emulating a server.
+ private void clientWriter() {
+ try {
+ OutputStream os =
+ new BufferedOutputStream(clientSock.getOutputStream());
+
+ while (true) {
+ ByteBuffer buf = buffer.take();
+ if (buf == FlowTest.SENTINEL) {
+ // finished
+ //Utils.sleep(2000);
+ System.out.println("clientWriter close");
+ clientSock.shutdownOutput();
+ System.out.println("clientWriter close return");
+ return;
+ }
+ writeToStream(os, buf);
+ clientSubscription.request(1);
+ }
+ } catch (Throwable e) {
+ e.printStackTrace();
+ }
+ }
+
+ private void writeToStream(OutputStream os, ByteBuffer buf) throws IOException {
+ byte[] b = buf.array();
+ int offset = buf.arrayOffset() + buf.position();
+ int n = buf.limit() - buf.position();
+ os.write(b, offset, n);
+ buf.position(buf.limit());
+ os.flush();
+ }
+
+ private final AtomicInteger loopCount = new AtomicInteger();
+
+ public String monitor() {
+ return "serverLoopback: loopcount = " + loopCount.toString()
+ + " clientRead: count = " + readCount.toString();
+ }
+
+ // thread2
+ private void serverLoopback() {
+ try {
+ InputStream is = serverSock.getInputStream();
+ OutputStream os = serverSock.getOutputStream();
+ final int bufsize = FlowTest.randomRange(512, 16 * 1024);
+ System.out.println("serverLoopback: bufsize = " + bufsize);
+ byte[] bb = new byte[bufsize];
+ while (true) {
+ int n = is.read(bb);
+ if (n == -1) {
+ Utils.sleep(2000);
+ is.close();
+ serverSock.close();
+ return;
+ }
+ os.write(bb, 0, n);
+ os.flush();
+ loopCount.addAndGet(n);
+ }
+ } catch (Throwable e) {
+ e.printStackTrace();
+ }
+ }
+
+
+ /**
+ * This needs to be called before the chain is subscribed. It can't be
+ * supplied in the constructor.
+ */
+ public void setReturnSubscriber(Subscriber<List<ByteBuffer>> returnSubscriber) {
+ publisher.subscribe(returnSubscriber);
+ }
+
+ @Override
+ public void onSubscribe(Flow.Subscription subscription) {
+ clientSubscription = subscription;
+ clientSubscription.request(5);
+ }
+
+ @Override
+ public void onNext(List<ByteBuffer> item) {
+ try {
+ for (ByteBuffer b : item)
+ buffer.put(b);
+ } catch (InterruptedException e) {
+ e.printStackTrace();
+ Utils.close(clientSock);
+ }
+ }
+
+ @Override
+ public void onError(Throwable throwable) {
+ throwable.printStackTrace();
+ Utils.close(clientSock);
+ }
+
+ @Override
+ public void onComplete() {
+ try {
+ buffer.put(FlowTest.SENTINEL);
+ } catch (InterruptedException e) {
+ e.printStackTrace();
+ Utils.close(clientSock);
+ }
+ }
+ }
+
+ /**
+ * The final subscriber which receives the decrypted looped-back data.
+ * Just needs to compare the data with what was sent. The given CF is
+ * either completed exceptionally with an error or normally on success.
+ */
+ static class EndSubscriber implements Subscriber<List<ByteBuffer>> {
+
+ private final long nbytes;
+
+ private final AtomicLong counter;
+ private volatile Flow.Subscription subscription;
+ private final CompletableFuture<Void> completion;
+
+ EndSubscriber(long nbytes, CompletableFuture<Void> completion) {
+ counter = new AtomicLong(0);
+ this.nbytes = nbytes;
+ this.completion = completion;
+ }
+
+ @Override
+ public void onSubscribe(Flow.Subscription subscription) {
+ this.subscription = subscription;
+ subscription.request(5);
+ }
+
+ public static String info(List<ByteBuffer> i) {
+ StringBuilder sb = new StringBuilder();
+ sb.append("size: ").append(Integer.toString(i.size()));
+ int x = 0;
+ for (ByteBuffer b : i)
+ x += b.remaining();
+ sb.append(" bytes: " + Integer.toString(x));
+ return sb.toString();
+ }
+
+ @Override
+ public void onNext(List<ByteBuffer> buffers) {
+ long currval = counter.get();
+ //if (currval % 500 == 0) {
+ //System.out.println("End: " + currval);
+ //}
+
+ for (ByteBuffer buf : buffers) {
+ while (buf.hasRemaining()) {
+ long n = buf.getLong();
+ //if (currval > (FlowTest.TOTAL_LONGS - 50)) {
+ //System.out.println("End: " + currval);
+ //}
+ if (n != currval++) {
+ System.out.println("ERROR at " + n + " != " + (currval - 1));
+ completion.completeExceptionally(new RuntimeException("ERROR"));
+ subscription.cancel();
+ return;
+ }
+ }
+ }
+
+ counter.set(currval);
+ subscription.request(1);
+ }
+
+ @Override
+ public void onError(Throwable throwable) {
+ completion.completeExceptionally(throwable);
+ }
+
+ @Override
+ public void onComplete() {
+ long n = counter.get();
+ if (n != nbytes) {
+ System.out.printf("nbytes=%d n=%d\n", nbytes, n);
+ completion.completeExceptionally(new RuntimeException("ERROR AT END"));
+ } else {
+ System.out.println("DONE OK: counter = " + n);
+ completion.complete(null);
+ }
+ }
+ }
+
+ /**
+ * Creates a simple usable SSLContext for SSLSocketFactory
+ * or a HttpsServer using either a given keystore or a default
+ * one in the test tree.
+ * <p>
+ * Using this class with a security manager requires the following
+ * permissions to be granted:
+ * <p>
+ * permission "java.util.PropertyPermission" "test.src.path", "read";
+ * permission java.io.FilePermission
+ * "${test.src}/../../../../lib/testlibrary/jdk/testlibrary/testkeys", "read";
+ * The exact path above depends on the location of the test.
+ */
+ static class SimpleSSLContext {
+
+ private final SSLContext ssl;
+
+ /**
+ * Loads default keystore from SimpleSSLContext source directory
+ */
+ public SimpleSSLContext() throws IOException {
+ String paths = System.getProperty("test.src.path");
+ StringTokenizer st = new StringTokenizer(paths, File.pathSeparator);
+ boolean securityExceptions = false;
+ SSLContext sslContext = null;
+ while (st.hasMoreTokens()) {
+ String path = st.nextToken();
+ try {
+ File f = new File(path, "../../../../lib/testlibrary/jdk/testlibrary/testkeys");
+ if (f.exists()) {
+ try (FileInputStream fis = new FileInputStream(f)) {
+ sslContext = init(fis);
+ break;
+ }
+ }
+ } catch (SecurityException e) {
+ // catch and ignore because permission only required
+ // for one entry on path (at most)
+ securityExceptions = true;
+ }
+ }
+ if (securityExceptions) {
+ System.out.println("SecurityExceptions thrown on loading testkeys");
+ }
+ ssl = sslContext;
+ }
+
+ private SSLContext init(InputStream i) throws IOException {
+ try {
+ char[] passphrase = "passphrase".toCharArray();
+ KeyStore ks = KeyStore.getInstance("JKS");
+ ks.load(i, passphrase);
+
+ KeyManagerFactory kmf = KeyManagerFactory.getInstance("SunX509");
+ kmf.init(ks, passphrase);
+
+ TrustManagerFactory tmf = TrustManagerFactory.getInstance("SunX509");
+ tmf.init(ks);
+
+ SSLContext ssl = SSLContext.getInstance("TLS");
+ ssl.init(kmf.getKeyManagers(), tmf.getTrustManagers(), null);
+ return ssl;
+ } catch (KeyManagementException | KeyStoreException |
+ UnrecoverableKeyException | CertificateException |
+ NoSuchAlgorithmException e) {
+ throw new RuntimeException(e.getMessage());
+ }
+ }
+
+ public SSLContext get() {
+ return ssl;
+ }
+ }
+}
--- /dev/null Thu Jan 01 00:00:00 1970 +0000
+++ b/test/jdk/java/net/httpclient/whitebox/jdk.incubator.httpclient/jdk/incubator/http/Http1HeaderParserTest.java Sun Nov 05 17:32:13 2017 +0000
@@ -0,0 +1,348 @@
+/*
+ * Copyright (c) 2017, Oracle and/or its affiliates. All rights reserved.
+ * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER.
+ *
+ * This code is free software; you can redistribute it and/or modify it
+ * under the terms of the GNU General Public License version 2 only, as
+ * published by the Free Software Foundation.
+ *
+ * This code is distributed in the hope that it will be useful, but WITHOUT
+ * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or
+ * FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License
+ * version 2 for more details (a copy is included in the LICENSE file that
+ * accompanied this code).
+ *
+ * You should have received a copy of the GNU General Public License version
+ * 2 along with this work; if not, write to the Free Software Foundation,
+ * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA.
+ *
+ * Please contact Oracle, 500 Oracle Parkway, Redwood Shores, CA 94065 USA
+ * or visit www.oracle.com if you need additional information or have any
+ * questions.
+ */
+
+package jdk.incubator.http;
+
+import java.io.ByteArrayInputStream;
+import java.net.ProtocolException;
+import java.nio.ByteBuffer;
+import java.util.ArrayList;
+import java.util.Arrays;
+import java.util.HashMap;
+import java.util.List;
+import java.util.Map;
+import java.util.stream.IntStream;
+import sun.net.www.MessageHeader;
+import org.testng.annotations.Test;
+import org.testng.annotations.DataProvider;
+import static java.lang.System.out;
+import static java.lang.String.format;
+import static java.nio.charset.StandardCharsets.US_ASCII;
+import static java.util.stream.Collectors.toList;
+import static org.testng.Assert.*;
+
+// Mostly verifies the "new" Http1HeaderParser returns the same results as the
+// tried and tested sun.net.www.MessageHeader.
+
+public class Http1HeaderParserTest {
+
+ @DataProvider(name = "responses")
+ public Object[][] responses() {
+ List<String> responses = new ArrayList<>();
+
+ String[] basic =
+ { "HTTP/1.1 200 OK\r\n\r\n",
+
+ "HTTP/1.1 200 OK\r\n" +
+ "Date: Mon, 15 Jan 2001 12:18:21 GMT\r\n" +
+ "Server: Apache/1.3.14 (Unix)\r\n" +
+ "Connection: close\r\n" +
+ "Content-Type: text/html; charset=iso-8859-1\r\n" +
+ "Content-Length: 10\r\n\r\n" +
+ "123456789",
+
+ "HTTP/1.1 200 OK\r\n" +
+ "Content-Length: 9\r\n" +
+ "Content-Type: text/html; charset=UTF-8\r\n\r\n" +
+ "XXXXX",
+
+ "HTTP/1.1 200 OK\r\n" +
+ "Content-Length: 9\r\n" +
+ "Content-Type: text/html; charset=UTF-8\r\n\r\n" + // more than one SP after ':'
+ "XXXXX",
+
+ "HTTP/1.1 200 OK\r\n" +
+ "Content-Length:\t10\r\n" +
+ "Content-Type:\ttext/html; charset=UTF-8\r\n\r\n" + // HT separator
+ "XXXXX",
+
+ "HTTP/1.1 200 OK\r\n" +
+ "Content-Length:\t\t10\r\n" +
+ "Content-Type:\t\ttext/html; charset=UTF-8\r\n\r\n" + // more than one HT after ':'
+ "XXXXX",
+
+ "HTTP/1.1 407 Proxy Authorization Required\r\n" +
+ "Proxy-Authenticate: Basic realm=\"a fake realm\"\r\n\r\n",
+
+ "HTTP/1.1 401 Unauthorized\r\n" +
+ "WWW-Authenticate: Digest realm=\"wally land\" domain=/ " +
+ "nonce=\"2B7F3A2B\" qop=\"auth\"\r\n\r\n",
+
+ "HTTP/1.1 200 OK\r\n" +
+ "X-Foo:\r\n\r\n", // no value
+
+ "HTTP/1.1 200 OK\r\n" +
+ "X-Foo:\r\n\r\n" + // no value, with response body
+ "Some Response Body",
+
+ "HTTP/1.1 200 OK\r\n" +
+ "X-Foo:\r\n" + // no value, followed by another header
+ "Content-Length: 10\r\n\r\n" +
+ "Some Response Body",
+
+ "HTTP/1.1 200 OK\r\n" +
+ "X-Foo:\r\n" + // no value, followed by another header, with response body
+ "Content-Length: 10\r\n\r\n",
+
+ "HTTP/1.1 200 OK\r\n" +
+ "X-Foo: chegar\r\n" +
+ "X-Foo: dfuchs\r\n" + // same header appears multiple times
+ "Content-Length: 0\r\n" +
+ "X-Foo: michaelm\r\n" +
+ "X-Foo: prappo\r\n\r\n",
+
+ "HTTP/1.1 200 OK\r\n" +
+ "X-Foo:\r\n" + // no value, same header appears multiple times
+ "X-Foo: dfuchs\r\n" +
+ "Content-Length: 0\r\n" +
+ "X-Foo: michaelm\r\n" +
+ "X-Foo: prappo\r\n\r\n",
+ };
+ Arrays.stream(basic).forEach(responses::add);
+
+ String[] foldingTemplate =
+ { "HTTP/1.1 200 OK\r\n" +
+ "Content-Length: 9\r\n" +
+ "Content-Type: text/html;$NEWLINE" + // folding field-value with '\n'|'\r'
+ " charset=UTF-8\r\n" + // one preceding SP
+ "Connection: close\r\n\r\n" +
+ "XXYYZZAABBCCDDEE",
+
+ "HTTP/1.1 200 OK\r\n" +
+ "Content-Length: 19\r\n" +
+ "Content-Type: text/html;$NEWLINE" + // folding field-value with '\n'|'\r
+ " charset=UTF-8\r\n" + // more than one preceding SP
+ "Connection: keep-alive\r\n\r\n" +
+ "XXYYZZAABBCCDDEEFFGG",
+
+ "HTTP/1.1 200 OK\r\n" +
+ "Content-Length: 999\r\n" +
+ "Content-Type: text/html;$NEWLINE" + // folding field-value with '\n'|'\r
+ "\tcharset=UTF-8\r\n" + // one preceding HT
+ "Connection: close\r\n\r\n" +
+ "XXYYZZAABBCCDDEE",
+
+ "HTTP/1.1 200 OK\r\n" +
+ "Content-Length: 54\r\n" +
+ "Content-Type: text/html;$NEWLINE" + // folding field-value with '\n'|'\r
+ "\t\t\tcharset=UTF-8\r\n" + // more than one preceding HT
+ "Connection: keep-alive\r\n\r\n" +
+ "XXYYZZAABBCCDDEEFFGG",
+
+ "HTTP/1.1 200 OK\r\n" +
+ "Content-Length: -1\r\n" +
+ "Content-Type: text/html;$NEWLINE" + // folding field-value with '\n'|'\r
+ "\t \t \tcharset=UTF-8\r\n" + // mix of preceding HT and SP
+ "Connection: keep-alive\r\n\r\n" +
+ "XXYYZZAABBCCDDEEFFGGHH",
+
+ "HTTP/1.1 200 OK\r\n" +
+ "Content-Length: 65\r\n" +
+ "Content-Type: text/html;$NEWLINE" + // folding field-value with '\n'|'\r
+ " \t \t charset=UTF-8\r\n" + // mix of preceding SP and HT
+ "Connection: keep-alive\r\n\r\n" +
+ "XXYYZZAABBCCDDEEFFGGHHII",
+ };
+ for (String newLineChar : new String[] { "\n", "\r" }) {
+ for (String template : foldingTemplate)
+ responses.add(template.replace("$NEWLINE", newLineChar));
+ }
+
+ String[] bad = // much of this is to retain parity with legacy MessageHeaders
+ { "HTTP/1.1 200 OK\r\n" +
+ "Connection:\r\n\r\n", // empty value, no body
+
+ "HTTP/1.1 200 OK\r\n" +
+ "Connection:\r\n\r\n" + // empty value, with body
+ "XXXXX",
+
+ "HTTP/1.1 200 OK\r\n" +
+ ": no header\r\n\r\n", // no/empty header-name, no body, no following header
+
+ "HTTP/1.1 200 OK\r\n" +
+ ": no; header\r\n" + // no/empty header-name, no body, following header
+ "Content-Length: 65\r\n\r\n",
+
+ "HTTP/1.1 200 OK\r\n" +
+ ": no header\r\n" + // no/empty header-name
+ "Content-Length: 65\r\n\r\n" +
+ "XXXXX",
+
+ "HTTP/1.1 200 OK\r\n" +
+ ": no header\r\n\r\n" + // no/empty header-name, followed by header
+ "XXXXX",
+
+ "HTTP/1.1 200 OK\r\n" +
+ "Conte\r" +
+ " nt-Length: 9\r\n" + // fold/bad header name ???
+ "Content-Type: text/html; charset=UTF-8\r\n\r\n" +
+ "XXXXX",
+
+ "HTTP/1.1 200 OK\r\n" +
+ "Conte\r" +
+ "nt-Length: 9\r\n" + // fold/bad header name ??? without preceding space
+ "Content-Type: text/html; charset=UTF-8\r\n\r\n" +
+ "XXXXXYYZZ",
+
+ "HTTP/1.0 404 Not Found\r\n" +
+ "header-without-colon\r\n\r\n",
+
+ "HTTP/1.0 404 Not Found\r\n" +
+ "header-without-colon\r\n\r\n" +
+ "SOMEBODY",
+
+ };
+ Arrays.stream(bad).forEach(responses::add);
+
+ return responses.stream().map(p -> new Object[] { p }).toArray(Object[][]::new);
+ }
+
+ @Test(dataProvider = "responses")
+ public void verifyHeaders(String respString) throws Exception {
+ byte[] bytes = respString.getBytes(US_ASCII);
+ ByteArrayInputStream bais = new ByteArrayInputStream(bytes);
+ MessageHeader m = new MessageHeader(bais);
+ Map<String,List<String>> messageHeaderMap = m.getHeaders();
+ int available = bais.available();
+
+ Http1HeaderParser decoder = new Http1HeaderParser();
+ ByteBuffer b = ByteBuffer.wrap(bytes);
+ decoder.parse(b);
+ Map<String,List<String>> decoderMap1 = decoder.headers().map();
+ assertEquals(available, b.remaining(),
+ "stream available not equal to remaining");
+
+ // assert status-line
+ String statusLine1 = messageHeaderMap.get(null).get(0);
+ String statusLine2 = decoder.statusLine();
+ if (statusLine1.startsWith("HTTP")) {// skip the case where MH's messes up the status-line
+ assertEquals(statusLine1, statusLine2, "Status-line not equal");
+ } else {
+ assertTrue(statusLine2.startsWith("HTTP/1."), "Status-line not HTTP/1.");
+ }
+
+ // remove the null'th entry with is the status-line
+ Map<String,List<String>> map = new HashMap<>();
+ for (Map.Entry<String,List<String>> e : messageHeaderMap.entrySet()) {
+ if (e.getKey() != null) {
+ map.put(e.getKey(), e.getValue());
+ }
+ }
+ messageHeaderMap = map;
+
+ assertHeadersEqual(messageHeaderMap, decoderMap1,
+ "messageHeaderMap not equal to decoderMap1");
+
+ // byte at a time
+ decoder = new Http1HeaderParser();
+ List<ByteBuffer> buffers = IntStream.range(0, bytes.length)
+ .mapToObj(i -> ByteBuffer.wrap(bytes, i, 1))
+ .collect(toList());
+ while (decoder.parse(buffers.remove(0)) != true);
+ Map<String,List<String>> decoderMap2 = decoder.headers().map();
+ assertEquals(available, buffers.size(),
+ "stream available not equals to remaining buffers");
+ assertEquals(decoderMap1, decoderMap2, "decoder maps not equal");
+ }
+
+ @DataProvider(name = "errors")
+ public Object[][] errors() {
+ List<String> responses = new ArrayList<>();
+
+ // These responses are parsed, somewhat, by MessageHeaders but give
+ // nonsensible results. They, correctly, fail with the Http1HeaderParser.
+ String[] bad =
+ {// "HTTP/1.1 402 Payment Required\r\n" +
+ // "Content-Length: 65\r\n\r", // missing trailing LF //TODO: incomplete
+
+ "HTTP/1.1 402 Payment Required\r\n" +
+ "Content-Length: 65\r\n\rT\r\n\r\nGGGGGG",
+
+ "HTTP/1.1 200OK\r\n\rT",
+
+ "HTTP/1.1 200OK\rT",
+ };
+ Arrays.stream(bad).forEach(responses::add);
+
+ return responses.stream().map(p -> new Object[] { p }).toArray(Object[][]::new);
+ }
+
+ @Test(dataProvider = "errors", expectedExceptions = ProtocolException.class)
+ public void errors(String respString) throws ProtocolException {
+ byte[] bytes = respString.getBytes(US_ASCII);
+ Http1HeaderParser decoder = new Http1HeaderParser();
+ ByteBuffer b = ByteBuffer.wrap(bytes);
+ decoder.parse(b);
+ }
+
+ void assertHeadersEqual(Map<String,List<String>> expected,
+ Map<String,List<String>> actual,
+ String msg) {
+
+ if (expected.equals(actual))
+ return;
+
+ assertEquals(expected.size(), actual.size(),
+ format("%s. Expected size %d, actual size %s. expected= %s, actual=%s.",
+ msg, expected.size(), actual.size(), expected, actual));
+
+ for (Map.Entry<String,List<String>> e : expected.entrySet()) {
+ String key = e.getKey();
+ List<String> values = e.getValue();
+
+ boolean found = false;
+ for (Map.Entry<String,List<String>> other: actual.entrySet()) {
+ if (key.equalsIgnoreCase(other.getKey())) {
+ found = true;
+ List<String> otherValues = other.getValue();
+ assertEquals(values.size(), otherValues.size(),
+ format("%s. Expected list size %d, actual size %s",
+ msg, values.size(), otherValues.size()));
+ if (!values.containsAll(otherValues) && otherValues.containsAll(values))
+ assertTrue(false, format("Lists are unequal [%s] [%s]", values, otherValues));
+ break;
+ }
+ }
+ assertTrue(found, format("header name, %s, not found in %s", key, actual));
+ }
+ }
+
+ // ---
+
+ /* Main entry point for standalone testing of the main functional test. */
+ public static void main(String... args) throws Exception {
+ Http1HeaderParserTest test = new Http1HeaderParserTest();
+ int count = 0;
+ for (Object[] objs : test.responses()) {
+ out.println("Testing " + count++ + ", " + objs[0]);
+ test.verifyHeaders((String) objs[0]);
+ }
+ for (Object[] objs : test.errors()) {
+ out.println("Testing " + count++ + ", " + objs[0]);
+ try {
+ test.errors((String) objs[0]);
+ throw new RuntimeException("Expected ProtocolException for " + objs[0]);
+ } catch (ProtocolException expected) { /* Ok */ }
+ }
+ }
+}
--- a/test/jdk/java/net/httpclient/whitebox/jdk.incubator.httpclient/jdk/incubator/http/RawChannelTest.java Sun Nov 05 17:05:57 2017 +0000
+++ b/test/jdk/java/net/httpclient/whitebox/jdk.incubator.httpclient/jdk/incubator/http/RawChannelTest.java Sun Nov 05 17:32:13 2017 +0000
@@ -24,6 +24,7 @@
package jdk.incubator.http;
import jdk.incubator.http.internal.websocket.RawChannel;
+import jdk.incubator.http.internal.websocket.WebSocketRequest;
import org.testng.annotations.Test;
import java.io.IOException;
@@ -83,6 +84,7 @@
new TestServer(server).start();
final RawChannel chan = channelOf(port);
+ print("RawChannel is %s", String.valueOf(chan));
initialWriteStall.await();
// It's very important not to forget the initial bytes, possibly
@@ -185,9 +187,21 @@
URI uri = URI.create("http://127.0.0.1:" + port + "/");
print("raw channel to %s", uri.toString());
HttpRequest req = HttpRequest.newBuilder(uri).build();
- HttpResponse<?> r = HttpClient.newHttpClient().send(req, discard(null));
- r.body();
- return ((HttpResponseImpl) r).rawChannel();
+ // Switch on isWebSocket flag to prevent the connection from
+ // being returned to the pool.
+ ((WebSocketRequest)req).isWebSocket(true);
+ HttpClient client = HttpClient.newHttpClient();
+ try {
+ HttpResponse<?> r = client.send(req, discard(null));
+ r.body();
+ return ((HttpResponseImpl) r).rawChannel();
+ } finally {
+ // Need to hold onto the client until the RawChannel is
+ // created. This would not be needed if we had created
+ // a WebSocket, but here we are fiddling directly
+ // with the internals of HttpResponseImpl!
+ java.lang.ref.Reference.reachabilityFence(client);
+ }
}
private class TestServer extends Thread { // Powered by Slowpokes
--- a/test/jdk/java/net/httpclient/whitebox/jdk.incubator.httpclient/jdk/incubator/http/ResponseHeadersTest.java Sun Nov 05 17:05:57 2017 +0000
+++ b/test/jdk/java/net/httpclient/whitebox/jdk.incubator.httpclient/jdk/incubator/http/ResponseHeadersTest.java Sun Nov 05 17:32:13 2017 +0000
@@ -142,82 +142,14 @@
throw new IOException("Status line not found");
}
- private static final class HttpConnectionStub extends HttpConnection {
- public HttpConnectionStub() {
- super(null, null);
- }
- @Override
- public void connect() throws IOException, InterruptedException {
- throw new AssertionError("Bad test assumption: should not have reached here!");
- }
- @Override
- public CompletableFuture<Void> connectAsync() {
- throw new AssertionError("Bad test assumption: should not have reached here!");
- }
- @Override
- boolean connected() {
- throw new AssertionError("Bad test assumption: should not have reached here!");
- }
- @Override
- boolean isSecure() {
- throw new AssertionError("Bad test assumption: should not have reached here!");
- }
- @Override
- boolean isProxied() {
- throw new AssertionError("Bad test assumption: should not have reached here!");
- }
- @Override
- CompletableFuture<Void> whenReceivingResponse() {
- throw new AssertionError("Bad test assumption: should not have reached here!");
- }
- @Override
- SocketChannel channel() {
- throw new AssertionError("Bad test assumption: should not have reached here!");
- }
- @Override
- ConnectionPool.CacheKey cacheKey() {
- throw new AssertionError("Bad test assumption: should not have reached here!");
- }
- @Override
- long write(ByteBuffer[] buffers, int start, int number) throws IOException {
- throw new AssertionError("Bad test assumption: should not have reached here!");
- }
- @Override
- long write(ByteBuffer buffer) throws IOException {
- throw new AssertionError("Bad test assumption: should not have reached here!");
- }
- @Override
- void writeAsync(ByteBufferReference[] buffers) throws IOException {
- throw new AssertionError("Bad test assumption: should not have reached here!");
- }
- @Override
- void writeAsyncUnordered(ByteBufferReference[] buffers) throws IOException {
- throw new AssertionError("Bad test assumption: should not have reached here!");
- }
- @Override
- void flushAsync() throws IOException {
- throw new AssertionError("Bad test assumption: should not have reached here!");
- }
- @Override
- public void close() {
- throw new AssertionError("Bad test assumption: should not have reached here!");
- }
- @Override
- void shutdownInput() throws IOException {
- throw new AssertionError("Bad test assumption: should not have reached here!");
- }
- @Override
- void shutdownOutput() throws IOException {
- throw new AssertionError("Bad test assumption: should not have reached here!");
- }
- @Override
- protected ByteBuffer readImpl() throws IOException {
+ private static final class ByteBufferSupplierStub {
+ ByteBuffer read() {
throw new AssertionError("Bad test assumption: should not have reached here!");
}
}
public static HttpHeaders createResponseHeaders(ByteBuffer buffer)
throws IOException{
- return new ResponseHeaders(new HttpConnectionStub(), buffer);
+ return new ResponseHeaders(new ByteBufferSupplierStub()::read, buffer);
}
}
--- /dev/null Thu Jan 01 00:00:00 1970 +0000
+++ b/test/jdk/java/net/httpclient/whitebox/jdk.incubator.httpclient/jdk/incubator/http/SSLTubeTest.java Sun Nov 05 17:32:13 2017 +0000
@@ -0,0 +1,403 @@
+/*
+ * Copyright (c) 2017, Oracle and/or its affiliates. All rights reserved.
+ * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER.
+ *
+ * This code is free software; you can redistribute it and/or modify it
+ * under the terms of the GNU General Public License version 2 only, as
+ * published by the Free Software Foundation.
+ *
+ * This code is distributed in the hope that it will be useful, but WITHOUT
+ * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or
+ * FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License
+ * version 2 for more details (a copy is included in the LICENSE file that
+ * accompanied this code).
+ *
+ * You should have received a copy of the GNU General Public License version
+ * 2 along with this work; if not, write to the Free Software Foundation,
+ * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA.
+ *
+ * Please contact Oracle, 500 Oracle Parkway, Redwood Shores, CA 94065 USA
+ * or visit www.oracle.com if you need additional information or have any
+ * questions.
+ */
+
+package jdk.incubator.http;
+
+import jdk.incubator.http.internal.common.Demand;
+import jdk.incubator.http.internal.common.FlowTube;
+import jdk.incubator.http.internal.common.SSLTube;
+import jdk.incubator.http.internal.common.SequentialScheduler;
+import org.testng.annotations.Test;
+
+import javax.net.ssl.KeyManagerFactory;
+import javax.net.ssl.SSLContext;
+import javax.net.ssl.SSLEngine;
+import javax.net.ssl.SSLParameters;
+import javax.net.ssl.TrustManagerFactory;
+import java.io.File;
+import java.io.FileInputStream;
+import java.io.IOException;
+import java.io.InputStream;
+import java.nio.ByteBuffer;
+import java.security.KeyManagementException;
+import java.security.KeyStore;
+import java.security.KeyStoreException;
+import java.security.NoSuchAlgorithmException;
+import java.security.UnrecoverableKeyException;
+import java.security.cert.CertificateException;
+import java.util.List;
+import java.util.Queue;
+import java.util.StringTokenizer;
+import java.util.concurrent.CompletableFuture;
+import java.util.concurrent.ConcurrentLinkedQueue;
+import java.util.concurrent.Executor;
+import java.util.concurrent.ExecutorService;
+import java.util.concurrent.Executors;
+import java.util.concurrent.Flow;
+import java.util.concurrent.ForkJoinPool;
+import java.util.concurrent.SubmissionPublisher;
+import java.util.concurrent.atomic.AtomicBoolean;
+import java.util.concurrent.atomic.AtomicLong;
+
+@Test
+public class SSLTubeTest {
+
+ private static final long COUNTER = 600;
+ private static final int LONGS_PER_BUF = 800;
+ private static final long TOTAL_LONGS = COUNTER * LONGS_PER_BUF;
+
+ private static ByteBuffer getBuffer(long startingAt) {
+ ByteBuffer buf = ByteBuffer.allocate(LONGS_PER_BUF * 8);
+ for (int j = 0; j < LONGS_PER_BUF; j++) {
+ buf.putLong(startingAt++);
+ }
+ buf.flip();
+ return buf;
+ }
+
+ @Test(timeOut = 30000)
+ public void run() throws IOException {
+ /* Start of wiring */
+ ExecutorService sslExecutor = Executors.newCachedThreadPool();
+ /* Emulates an echo server */
+ FlowTube server = new SSLTube(createSSLEngine(false),
+ sslExecutor,
+ new EchoTube(16));
+ FlowTube client = new SSLTube(createSSLEngine(true),
+ sslExecutor,
+ server);
+ SubmissionPublisher<List<ByteBuffer>> p =
+ new SubmissionPublisher<>(ForkJoinPool.commonPool(),
+ Integer.MAX_VALUE);
+ FlowTube.TubePublisher begin = p::subscribe;
+ CompletableFuture<Void> completion = new CompletableFuture<>();
+ EndSubscriber end = new EndSubscriber(TOTAL_LONGS, completion);
+ client.connectFlows(begin, end);
+ /* End of wiring */
+
+ long count = 0;
+ System.out.printf("Submitting %d buffer arrays\n", COUNTER);
+ System.out.printf("LoopCount should be %d\n", TOTAL_LONGS);
+ for (long i = 0; i < COUNTER; i++) {
+ ByteBuffer b = getBuffer(count);
+ count += LONGS_PER_BUF;
+ p.submit(List.of(b));
+ }
+ System.out.println("Finished submission. Waiting for loopback");
+ p.close();
+ try {
+ completion.join();
+ System.out.println("OK");
+ } finally {
+ sslExecutor.shutdownNow();
+ }
+ }
+
+ private static final class EchoTube implements FlowTube {
+
+ private final static Object EOF = new Object();
+ private final Executor executor = Executors.newSingleThreadExecutor();
+
+ private final Queue<Object> queue = new ConcurrentLinkedQueue<>();
+ private final int maxQueueSize;
+ private final SequentialScheduler processingScheduler =
+ new SequentialScheduler(createProcessingTask());
+
+ /* Writing into this tube */
+ private long unfulfilled;
+ private Flow.Subscription subscription;
+
+ /* Reading from this tube */
+ private final Demand demand = new Demand();
+ private final AtomicBoolean cancelled = new AtomicBoolean();
+ private Flow.Subscriber<? super List<ByteBuffer>> subscriber;
+
+ private EchoTube(int maxBufferSize) {
+ if (maxBufferSize < 1)
+ throw new IllegalArgumentException();
+ this.maxQueueSize = maxBufferSize;
+ }
+
+ @Override
+ public void subscribe(Flow.Subscriber<? super List<ByteBuffer>> subscriber) {
+ this.subscriber = subscriber;
+ this.subscriber.onSubscribe(new InternalSubscription());
+ }
+
+ @Override
+ public void onSubscribe(Flow.Subscription subscription) {
+ unfulfilled = maxQueueSize;
+ (this.subscription = subscription).request(maxQueueSize);
+ }
+
+ @Override
+ public void onNext(List<ByteBuffer> item) {
+ if (--unfulfilled == (maxQueueSize / 2)) {
+ subscription.request(maxQueueSize - unfulfilled);
+ unfulfilled = maxQueueSize;
+ }
+ queue.add(item);
+ processingScheduler.deferOrSchedule(executor);
+ }
+
+ @Override
+ public void onError(Throwable throwable) {
+ queue.add(throwable);
+ processingScheduler.deferOrSchedule(executor);
+ }
+
+ @Override
+ public void onComplete() {
+ queue.add(EOF);
+ processingScheduler.deferOrSchedule(executor);
+ }
+
+ @Override
+ public boolean isFinished() {
+ return false;
+ }
+
+ private class InternalSubscription implements Flow.Subscription {
+
+ @Override
+ public void request(long n) {
+ if (n <= 0) {
+ throw new InternalError();
+ }
+ demand.increase(n);
+ processingScheduler.runOrSchedule();
+ }
+
+ @Override
+ public void cancel() {
+ cancelled.set(true);
+ }
+ }
+
+ private SequentialScheduler.RestartableTask createProcessingTask() {
+ return new SequentialScheduler.CompleteRestartableTask() {
+
+ @Override
+ protected void run() {
+ while (!cancelled.get()) {
+ Object item = queue.peek();
+ if (item == null)
+ return;
+ try {
+ if (item instanceof List) {
+ if (!demand.tryDecrement())
+ return;
+ @SuppressWarnings("unchecked")
+ List<ByteBuffer> bytes = (List<ByteBuffer>) item;
+ subscriber.onNext(bytes);
+ } else if (item instanceof Throwable) {
+ cancelled.set(true);
+ subscriber.onError((Throwable) item);
+ } else if (item == EOF) {
+ cancelled.set(true);
+ subscriber.onComplete();
+ } else {
+ throw new InternalError(String.valueOf(item));
+ }
+ } finally {
+ Object removed = queue.remove();
+ assert removed == item;
+ }
+ }
+ }
+ };
+ }
+ }
+
+ /**
+ * The final subscriber which receives the decrypted looped-back data. Just
+ * needs to compare the data with what was sent. The given CF is either
+ * completed exceptionally with an error or normally on success.
+ */
+ private static class EndSubscriber implements FlowTube.TubeSubscriber {
+
+ private static final int REQUEST_WINDOW = 13;
+
+ private final long nbytes;
+ private final AtomicLong counter = new AtomicLong();
+ private final CompletableFuture<?> completion;
+ private volatile Flow.Subscription subscription;
+ private long unfulfilled;
+
+ EndSubscriber(long nbytes, CompletableFuture<?> completion) {
+ this.nbytes = nbytes;
+ this.completion = completion;
+ }
+
+ @Override
+ public void onSubscribe(Flow.Subscription subscription) {
+ this.subscription = subscription;
+ unfulfilled = REQUEST_WINDOW;
+ subscription.request(REQUEST_WINDOW);
+ }
+
+ public static String info(List<ByteBuffer> i) {
+ StringBuilder sb = new StringBuilder();
+ sb.append("size: ").append(Integer.toString(i.size()));
+ int x = 0;
+ for (ByteBuffer b : i)
+ x += b.remaining();
+ sb.append(" bytes: ").append(x);
+ return sb.toString();
+ }
+
+ @Override
+ public void onNext(List<ByteBuffer> buffers) {
+ if (--unfulfilled == (REQUEST_WINDOW / 2)) {
+ subscription.request(REQUEST_WINDOW - unfulfilled);
+ unfulfilled = REQUEST_WINDOW;
+ }
+
+ long currval = counter.get();
+ if (currval % 500 == 0) {
+ System.out.println("End: " + currval);
+ }
+
+ for (ByteBuffer buf : buffers) {
+ while (buf.hasRemaining()) {
+ long n = buf.getLong();
+ if (currval > (SSLTubeTest.TOTAL_LONGS - 50)) {
+ System.out.println("End: " + currval);
+ }
+ if (n != currval++) {
+ System.out.println("ERROR at " + n + " != " + (currval - 1));
+ completion.completeExceptionally(new RuntimeException("ERROR"));
+ subscription.cancel();
+ return;
+ }
+ }
+ }
+
+ counter.set(currval);
+ }
+
+ @Override
+ public void onError(Throwable throwable) {
+ completion.completeExceptionally(throwable);
+ }
+
+ @Override
+ public void onComplete() {
+ long n = counter.get();
+ if (n != nbytes) {
+ System.out.printf("nbytes=%d n=%d\n", nbytes, n);
+ completion.completeExceptionally(new RuntimeException("ERROR AT END"));
+ } else {
+ System.out.println("DONE OK");
+ completion.complete(null);
+ }
+ }
+ }
+
+ private static SSLEngine createSSLEngine(boolean client) throws IOException {
+ SSLContext context = (new SimpleSSLContext()).get();
+ SSLEngine engine = context.createSSLEngine();
+ SSLParameters params = context.getSupportedSSLParameters();
+ params.setProtocols(new String[]{"TLSv1.2"}); // TODO: This is essential. Needs to be protocol impl
+ if (client) {
+ params.setApplicationProtocols(new String[]{"proto1", "proto2"}); // server will choose proto2
+ } else {
+ params.setApplicationProtocols(new String[]{"proto2"}); // server will choose proto2
+ }
+ engine.setSSLParameters(params);
+ engine.setUseClientMode(client);
+ return engine;
+ }
+
+ /**
+ * Creates a simple usable SSLContext for SSLSocketFactory or a HttpsServer
+ * using either a given keystore or a default one in the test tree.
+ *
+ * Using this class with a security manager requires the following
+ * permissions to be granted:
+ *
+ * permission "java.util.PropertyPermission" "test.src.path", "read";
+ * permission java.io.FilePermission "${test.src}/../../../../lib/testlibrary/jdk/testlibrary/testkeys",
+ * "read"; The exact path above depends on the location of the test.
+ */
+ private static class SimpleSSLContext {
+
+ private final SSLContext ssl;
+
+ /**
+ * Loads default keystore from SimpleSSLContext source directory
+ */
+ public SimpleSSLContext() throws IOException {
+ String paths = System.getProperty("test.src.path");
+ StringTokenizer st = new StringTokenizer(paths, File.pathSeparator);
+ boolean securityExceptions = false;
+ SSLContext sslContext = null;
+ while (st.hasMoreTokens()) {
+ String path = st.nextToken();
+ try {
+ File f = new File(path, "../../../../lib/testlibrary/jdk/testlibrary/testkeys");
+ if (f.exists()) {
+ try (FileInputStream fis = new FileInputStream(f)) {
+ sslContext = init(fis);
+ break;
+ }
+ }
+ } catch (SecurityException e) {
+ // catch and ignore because permission only required
+ // for one entry on path (at most)
+ securityExceptions = true;
+ }
+ }
+ if (securityExceptions) {
+ System.err.println("SecurityExceptions thrown on loading testkeys");
+ }
+ ssl = sslContext;
+ }
+
+ private SSLContext init(InputStream i) throws IOException {
+ try {
+ char[] passphrase = "passphrase".toCharArray();
+ KeyStore ks = KeyStore.getInstance("JKS");
+ ks.load(i, passphrase);
+
+ KeyManagerFactory kmf = KeyManagerFactory.getInstance("SunX509");
+ kmf.init(ks, passphrase);
+
+ TrustManagerFactory tmf = TrustManagerFactory.getInstance("SunX509");
+ tmf.init(ks);
+
+ SSLContext ssl = SSLContext.getInstance("TLS");
+ ssl.init(kmf.getKeyManagers(), tmf.getTrustManagers(), null);
+ return ssl;
+ } catch (KeyManagementException | KeyStoreException |
+ UnrecoverableKeyException | CertificateException |
+ NoSuchAlgorithmException e) {
+ throw new RuntimeException(e.getMessage());
+ }
+ }
+
+ public SSLContext get() {
+ return ssl;
+ }
+ }
+}
--- a/test/jdk/java/net/httpclient/whitebox/jdk.incubator.httpclient/jdk/incubator/http/SelectorTest.java Sun Nov 05 17:05:57 2017 +0000
+++ b/test/jdk/java/net/httpclient/whitebox/jdk.incubator.httpclient/jdk/incubator/http/SelectorTest.java Sun Nov 05 17:32:13 2017 +0000
@@ -76,7 +76,7 @@
}
}
- @Test(timeOut = 10000)
+ @Test
public void test() throws Exception {
try (ServerSocket server = new ServerSocket(0)) {
--- /dev/null Thu Jan 01 00:00:00 1970 +0000
+++ b/test/jdk/java/net/httpclient/whitebox/jdk.incubator.httpclient/jdk/incubator/http/WrapperTest.java Sun Nov 05 17:32:13 2017 +0000
@@ -0,0 +1,263 @@
+/*
+ * Copyright (c) 2017, Oracle and/or its affiliates. All rights reserved.
+ * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER.
+ *
+ * This code is free software; you can redistribute it and/or modify it
+ * under the terms of the GNU General Public License version 2 only, as
+ * published by the Free Software Foundation.
+ *
+ * This code is distributed in the hope that it will be useful, but WITHOUT
+ * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or
+ * FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License
+ * version 2 for more details (a copy is included in the LICENSE file that
+ * accompanied this code).
+ *
+ * You should have received a copy of the GNU General Public License version
+ * 2 along with this work; if not, write to the Free Software Foundation,
+ * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA.
+ *
+ * Please contact Oracle, 500 Oracle Parkway, Redwood Shores, CA 94065 USA
+ * or visit www.oracle.com if you need additional information or have any
+ * questions.
+ */
+
+package jdk.incubator.http;
+
+import java.nio.ByteBuffer;
+import java.util.LinkedList;
+import java.util.List;
+import java.util.concurrent.*;
+import java.util.concurrent.atomic.*;
+import org.testng.annotations.Test;
+import jdk.incubator.http.internal.common.SubscriberWrapper;
+
+@Test
+public class WrapperTest {
+ static final int LO_PRI = 1;
+ static final int HI_PRI = 2;
+ static final int NUM_HI_PRI = 240;
+ static final int BUFSIZE = 1016;
+ static final int BUFSIZE_INT = BUFSIZE/4;
+ static final int HI_PRI_FREQ = 40;
+
+ static final int TOTAL = 10000;
+ //static final int TOTAL = 500;
+
+ final SubmissionPublisher<List<ByteBuffer>> publisher;
+ final SubscriberWrapper sub1, sub2, sub3;
+ final ExecutorService executor = Executors.newCachedThreadPool();
+ volatile int hipricount = 0;
+
+ void errorHandler(Flow.Subscriber<? super List<ByteBuffer>> sub, Throwable t) {
+ System.err.printf("Exception from %s : %s\n", sub.toString(), t.toString());
+ }
+
+ public WrapperTest() {
+ publisher = new SubmissionPublisher<>(executor, 600,
+ (a, b) -> {
+ errorHandler(a, b);
+ });
+
+ CompletableFuture<Void> notif = new CompletableFuture<>();
+ LastSubscriber ls = new LastSubscriber(notif);
+ sub1 = new Filter1(ls);
+ sub2 = new Filter2(sub1);
+ sub3 = new Filter2(sub2);
+ }
+
+ public class Filter2 extends SubscriberWrapper {
+ Filter2(SubscriberWrapper wrapper) {
+ super(wrapper);
+ }
+
+ // reverse the order of the bytes in each buffer
+ public void incoming(List<ByteBuffer> list, boolean complete) {
+ List<ByteBuffer> out = new LinkedList<>();
+ for (ByteBuffer inbuf : list) {
+ int size = inbuf.remaining();
+ ByteBuffer outbuf = ByteBuffer.allocate(size);
+ for (int i=size; i>0; i--) {
+ byte b = inbuf.get(i-1);
+ outbuf.put(b);
+ }
+ outbuf.flip();
+ out.add(outbuf);
+ }
+ if (complete) System.out.println("Filter2.complete");
+ outgoing(out, complete);
+ }
+
+ protected long windowUpdate(long currval) {
+ return currval == 0 ? 1 : 0;
+ }
+ }
+
+ volatile int filter1Calls = 0; // every third call we insert hi pri data
+
+ ByteBuffer getHiPri(int val) {
+ ByteBuffer buf = ByteBuffer.allocate(8);
+ buf.putInt(HI_PRI);
+ buf.putInt(val);
+ buf.flip();
+ return buf;
+ }
+
+ volatile int hiPriAdded = 0;
+
+ public class Filter1 extends SubscriberWrapper {
+ Filter1(Flow.Subscriber<List<ByteBuffer>> downstreamSubscriber)
+ {
+ super();
+ subscribe(downstreamSubscriber);
+ }
+
+ // Inserts up to NUM_HI_PRI hi priority buffers into flow
+ protected void incoming(List<ByteBuffer> in, boolean complete) {
+ if ((++filter1Calls % HI_PRI_FREQ) == 0 && (hiPriAdded++ < NUM_HI_PRI)) {
+ sub1.outgoing(getHiPri(hipricount++), false);
+ }
+ // pass data thru
+ if (complete) System.out.println("Filter1.complete");
+ outgoing(in, complete);
+ }
+
+ protected long windowUpdate(long currval) {
+ return currval == 0 ? 1 : 0;
+ }
+ }
+
+ /**
+ * Final subscriber in the chain. Compares the data sent by the original
+ * publisher.
+ */
+ static public class LastSubscriber implements Flow.Subscriber<List<ByteBuffer>> {
+ volatile Flow.Subscription subscription;
+ volatile int hipriCounter=0;
+ volatile int lopriCounter=0;
+ final CompletableFuture<Void> cf;
+
+ LastSubscriber(CompletableFuture<Void> cf) {
+ this.cf = cf;
+ }
+
+ @Override
+ public void onSubscribe(Flow.Subscription subscription) {
+ this.subscription = subscription;
+ subscription.request(50); // say
+ }
+
+ private void error(String...args) {
+ StringBuilder sb = new StringBuilder();
+ for (String s : args) {
+ sb.append(s);
+ sb.append(' ');
+ }
+ String msg = sb.toString();
+ System.out.println("Error: " + msg);
+ RuntimeException e = new RuntimeException(msg);
+ cf.completeExceptionally(e);
+ subscription.cancel(); // This is where we need a variant that include exception
+ }
+
+ private void check(ByteBuffer buf) {
+ int type = buf.getInt();
+ if (type == HI_PRI) {
+ // check next int is hi pri counter
+ int c = buf.getInt();
+ if (c != hipriCounter)
+ error("hi pri counter", Integer.toString(c), Integer.toString(hipriCounter));
+ hipriCounter++;
+ } else {
+ while (buf.hasRemaining()) {
+ if (buf.getInt() != lopriCounter)
+ error("lo pri counter", Integer.toString(lopriCounter));
+ lopriCounter++;
+ }
+ }
+ }
+
+ @Override
+ public void onNext(List<ByteBuffer> items) {
+ for (ByteBuffer item : items)
+ check(item);
+ subscription.request(1);
+ }
+
+ @Override
+ public void onError(Throwable throwable) {
+ error(throwable.getMessage());
+ }
+
+ @Override
+ public void onComplete() {
+ if (hipriCounter != NUM_HI_PRI)
+ error("hi pri at end wrong", Integer.toString(hipriCounter), Integer.toString(NUM_HI_PRI));
+ else {
+ System.out.println("LastSubscriber.complete");
+ cf.complete(null); // success
+ }
+ }
+ }
+
+ List<ByteBuffer> getBuffer(int c) {
+ ByteBuffer buf = ByteBuffer.allocate(BUFSIZE+4);
+ buf.putInt(LO_PRI);
+ for (int i=0; i<BUFSIZE_INT; i++) {
+ buf.putInt(c++);
+ }
+ buf.flip();
+ return List.of(buf);
+ }
+
+ boolean errorTest = false;
+
+ @Test
+ public void run() throws InterruptedException {
+ try {
+ CompletableFuture<Void> completion = sub3.completion();
+ publisher.subscribe(sub3);
+ // now submit a load of data
+ int counter = 0;
+ for (int i = 0; i < TOTAL; i++) {
+ List<ByteBuffer> bufs = getBuffer(counter);
+ //if (i==2)
+ //bufs.get(0).putInt(41, 1234); // error
+ counter += BUFSIZE_INT;
+ publisher.submit(bufs);
+ //if (i % 1000 == 0)
+ //Thread.sleep(1000);
+ //if (i == 99) {
+ //publisher.closeExceptionally(new RuntimeException("Test error"));
+ //errorTest = true;
+ //break;
+ //}
+ }
+ if (!errorTest) {
+ publisher.close();
+ }
+ System.out.println("Publisher completed");
+ completion.join();
+ System.out.println("Subscribers completed ok");
+ } finally {
+ executor.shutdownNow();
+ }
+ }
+
+ static void display(CompletableFuture<?> cf) {
+ System.out.print (cf);
+ if (!cf.isDone())
+ return;
+ try {
+ cf.join(); // wont block
+ } catch (Exception e) {
+ System.out.println(" " + e);
+ }
+ }
+
+/*
+ public static void main(String[] args) throws InterruptedException {
+ WrapperTest test = new WrapperTest();
+ test.run();
+ }
+*/
+}
--- /dev/null Thu Jan 01 00:00:00 1970 +0000
+++ b/test/jdk/java/net/httpclient/whitebox/jdk.incubator.httpclient/jdk/incubator/http/internal/common/DemandTest.java Sun Nov 05 17:32:13 2017 +0000
@@ -0,0 +1,202 @@
+/*
+ * Copyright (c) 2017, Oracle and/or its affiliates. All rights reserved.
+ * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER.
+ *
+ * This code is free software; you can redistribute it and/or modify it
+ * under the terms of the GNU General Public License version 2 only, as
+ * published by the Free Software Foundation.
+ *
+ * This code is distributed in the hope that it will be useful, but WITHOUT
+ * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or
+ * FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License
+ * version 2 for more details (a copy is included in the LICENSE file that
+ * accompanied this code).
+ *
+ * You should have received a copy of the GNU General Public License version
+ * 2 along with this work; if not, write to the Free Software Foundation,
+ * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA.
+ *
+ * Please contact Oracle, 500 Oracle Parkway, Redwood Shores, CA 94065 USA
+ * or visit www.oracle.com if you need additional information or have any
+ * questions.
+ */
+
+package jdk.incubator.http.internal.common;
+
+import org.testng.annotations.Test;
+
+import java.util.concurrent.CountDownLatch;
+import java.util.concurrent.CyclicBarrier;
+import java.util.concurrent.atomic.AtomicReference;
+
+import static org.testng.Assert.assertEquals;
+import static org.testng.Assert.assertFalse;
+import static org.testng.Assert.assertTrue;
+
+public class DemandTest {
+
+ @Test
+ public void test01() {
+ assertTrue(new Demand().isFulfilled());
+ }
+
+ @Test
+ public void test011() {
+ Demand d = new Demand();
+ d.increase(3);
+ d.decreaseAndGet(3);
+ assertTrue(d.isFulfilled());
+ }
+
+ @Test
+ public void test02() {
+ Demand d = new Demand();
+ d.increase(1);
+ assertFalse(d.isFulfilled());
+ }
+
+ @Test
+ public void test03() {
+ Demand d = new Demand();
+ d.increase(3);
+ assertEquals(d.decreaseAndGet(3), 3);
+ }
+
+ @Test
+ public void test04() {
+ Demand d = new Demand();
+ d.increase(3);
+ assertEquals(d.decreaseAndGet(5), 3);
+ }
+
+ @Test
+ public void test05() {
+ Demand d = new Demand();
+ d.increase(7);
+ assertEquals(d.decreaseAndGet(4), 4);
+ }
+
+ @Test
+ public void test06() {
+ Demand d = new Demand();
+ assertEquals(d.decreaseAndGet(3), 0);
+ }
+
+ @Test(expectedExceptions = IllegalArgumentException.class)
+ public void test07() {
+ Demand d = new Demand();
+ d.increase(0);
+ }
+
+ @Test(expectedExceptions = IllegalArgumentException.class)
+ public void test08() {
+ Demand d = new Demand();
+ d.increase(-1);
+ }
+
+ @Test(expectedExceptions = IllegalArgumentException.class)
+ public void test09() {
+ Demand d = new Demand();
+ d.increase(10);
+ d.decreaseAndGet(0);
+ }
+
+ @Test(expectedExceptions = IllegalArgumentException.class)
+ public void test10() {
+ Demand d = new Demand();
+ d.increase(13);
+ d.decreaseAndGet(-3);
+ }
+
+ @Test
+ public void test11() {
+ Demand d = new Demand();
+ d.increase(1);
+ assertTrue(d.tryDecrement());
+ }
+
+ @Test
+ public void test12() {
+ Demand d = new Demand();
+ d.increase(2);
+ assertTrue(d.tryDecrement());
+ }
+
+ @Test
+ public void test14() {
+ Demand d = new Demand();
+ assertFalse(d.tryDecrement());
+ }
+
+ @Test
+ public void test141() {
+ Demand d = new Demand();
+ d.increase(Long.MAX_VALUE);
+ assertFalse(d.isFulfilled());
+ }
+
+ @Test
+ public void test142() {
+ Demand d = new Demand();
+ d.increase(Long.MAX_VALUE);
+ d.increase(1);
+ assertFalse(d.isFulfilled());
+ }
+
+ @Test
+ public void test143() {
+ Demand d = new Demand();
+ d.increase(Long.MAX_VALUE);
+ d.increase(1);
+ assertFalse(d.isFulfilled());
+ }
+
+ @Test
+ public void test144() {
+ Demand d = new Demand();
+ d.increase(Long.MAX_VALUE);
+ d.increase(Long.MAX_VALUE);
+ d.decreaseAndGet(3);
+ d.decreaseAndGet(5);
+ assertFalse(d.isFulfilled());
+ }
+
+ @Test
+ public void test145() {
+ Demand d = new Demand();
+ d.increase(Long.MAX_VALUE);
+ d.decreaseAndGet(Long.MAX_VALUE);
+ assertTrue(d.isFulfilled());
+ }
+
+ @Test(invocationCount = 32)
+ public void test15() throws InterruptedException {
+ int N = Math.max(2, Runtime.getRuntime().availableProcessors() + 1);
+ int M = ((N + 1) * N) / 2; // 1 + 2 + 3 + ... N
+ Demand d = new Demand();
+ d.increase(M);
+ CyclicBarrier start = new CyclicBarrier(N);
+ CountDownLatch stop = new CountDownLatch(N);
+ AtomicReference<Throwable> error = new AtomicReference<>();
+ for (int i = 0; i < N; i++) {
+ int j = i + 1;
+ new Thread(() -> {
+ try {
+ start.await();
+ } catch (Exception e) {
+ error.compareAndSet(null, e);
+ }
+ try {
+ assertEquals(d.decreaseAndGet(j), j);
+ } catch (Throwable t) {
+ error.compareAndSet(null, t);
+ } finally {
+ stop.countDown();
+ }
+ }).start();
+ }
+ stop.await();
+ assertTrue(d.isFulfilled());
+ assertEquals(error.get(), null);
+ }
+}