diff --git a/jetty-spdy/spdy-http-client-transport/pom.xml b/jetty-spdy/spdy-http-client-transport/pom.xml
new file mode 100644
index 00000000000..7c14d20eab1
--- /dev/null
+++ b/jetty-spdy/spdy-http-client-transport/pom.xml
@@ -0,0 +1,101 @@
+
+
+
+ org.eclipse.jetty.spdy
+ spdy-parent
+ 9.1.0-SNAPSHOT
+
+
+ 4.0.0
+ spdy-http-client-transport
+ Jetty :: SPDY :: HTTP Client Transport
+
+
+ ${project.groupId}.client.http
+
+
+
+
+
+ maven-dependency-plugin
+
+
+ copy
+ generate-resources
+
+ copy
+
+
+
+
+ org.mortbay.jetty.npn
+ npn-boot
+ ${npn.version}
+ jar
+ false
+ ${project.build.directory}/npn
+
+
+
+
+
+
+
+ maven-surefire-plugin
+
+ -Xbootclasspath/p:${project.build.directory}/npn/npn-boot-${npn.version}.jar
+
+
+
+ org.apache.felix
+ maven-bundle-plugin
+ true
+
+
+
+ manifest
+
+
+
+ org.eclipse.jetty.spdy.client.http;version="9.1"
+ !org.eclipse.jetty.npn,org.eclipse.jetty.*;version="[9.0,10.0)",*
+
+
+
+
+
+
+
+
+
+
+ org.eclipse.jetty
+ jetty-client
+ ${project.version}
+
+
+ org.eclipse.jetty.spdy
+ spdy-client
+ ${project.version}
+
+
+ org.eclipse.jetty.spdy
+ spdy-http-common
+ ${project.version}
+
+
+
+ org.eclipse.jetty
+ jetty-server
+ ${project.version}
+ test
+
+
+ org.eclipse.jetty.spdy
+ spdy-http-server
+ ${project.version}
+ test
+
+
+
+
diff --git a/jetty-client/src/main/java/org/eclipse/jetty/client/spdy/HttpChannelOverSPDY.java b/jetty-spdy/spdy-http-client-transport/src/main/java/org/eclipse/jetty/spdy/client/http/HttpChannelOverSPDY.java
similarity index 97%
rename from jetty-client/src/main/java/org/eclipse/jetty/client/spdy/HttpChannelOverSPDY.java
rename to jetty-spdy/spdy-http-client-transport/src/main/java/org/eclipse/jetty/spdy/client/http/HttpChannelOverSPDY.java
index 96a56058e20..992f2a57616 100644
--- a/jetty-client/src/main/java/org/eclipse/jetty/client/spdy/HttpChannelOverSPDY.java
+++ b/jetty-spdy/spdy-http-client-transport/src/main/java/org/eclipse/jetty/spdy/client/http/HttpChannelOverSPDY.java
@@ -16,7 +16,7 @@
// ========================================================================
//
-package org.eclipse.jetty.client.spdy;
+package org.eclipse.jetty.spdy.client.http;
import org.eclipse.jetty.client.HttpChannel;
import org.eclipse.jetty.client.HttpDestination;
diff --git a/jetty-client/src/main/java/org/eclipse/jetty/client/spdy/HttpClientTransportOverSPDY.java b/jetty-spdy/spdy-http-client-transport/src/main/java/org/eclipse/jetty/spdy/client/http/HttpClientTransportOverSPDY.java
similarity index 98%
rename from jetty-client/src/main/java/org/eclipse/jetty/client/spdy/HttpClientTransportOverSPDY.java
rename to jetty-spdy/spdy-http-client-transport/src/main/java/org/eclipse/jetty/spdy/client/http/HttpClientTransportOverSPDY.java
index c76849673fb..1d5d1190893 100644
--- a/jetty-client/src/main/java/org/eclipse/jetty/client/spdy/HttpClientTransportOverSPDY.java
+++ b/jetty-spdy/spdy-http-client-transport/src/main/java/org/eclipse/jetty/spdy/client/http/HttpClientTransportOverSPDY.java
@@ -16,7 +16,7 @@
// ========================================================================
//
-package org.eclipse.jetty.client.spdy;
+package org.eclipse.jetty.spdy.client.http;
import java.net.SocketAddress;
diff --git a/jetty-client/src/main/java/org/eclipse/jetty/client/spdy/HttpConnectionOverSPDY.java b/jetty-spdy/spdy-http-client-transport/src/main/java/org/eclipse/jetty/spdy/client/http/HttpConnectionOverSPDY.java
similarity index 97%
rename from jetty-client/src/main/java/org/eclipse/jetty/client/spdy/HttpConnectionOverSPDY.java
rename to jetty-spdy/spdy-http-client-transport/src/main/java/org/eclipse/jetty/spdy/client/http/HttpConnectionOverSPDY.java
index fe325a9f13a..ffe025146bf 100644
--- a/jetty-client/src/main/java/org/eclipse/jetty/client/spdy/HttpConnectionOverSPDY.java
+++ b/jetty-spdy/spdy-http-client-transport/src/main/java/org/eclipse/jetty/spdy/client/http/HttpConnectionOverSPDY.java
@@ -16,7 +16,7 @@
// ========================================================================
//
-package org.eclipse.jetty.client.spdy;
+package org.eclipse.jetty.spdy.client.http;
import org.eclipse.jetty.client.HttpChannel;
import org.eclipse.jetty.client.HttpClient;
diff --git a/jetty-client/src/main/java/org/eclipse/jetty/client/spdy/HttpDestinationOverSPDY.java b/jetty-spdy/spdy-http-client-transport/src/main/java/org/eclipse/jetty/spdy/client/http/HttpDestinationOverSPDY.java
similarity index 70%
rename from jetty-client/src/main/java/org/eclipse/jetty/client/spdy/HttpDestinationOverSPDY.java
rename to jetty-spdy/spdy-http-client-transport/src/main/java/org/eclipse/jetty/spdy/client/http/HttpDestinationOverSPDY.java
index 0c505b74f40..24b2acf4b76 100644
--- a/jetty-client/src/main/java/org/eclipse/jetty/client/spdy/HttpDestinationOverSPDY.java
+++ b/jetty-spdy/spdy-http-client-transport/src/main/java/org/eclipse/jetty/spdy/client/http/HttpDestinationOverSPDY.java
@@ -16,7 +16,7 @@
// ========================================================================
//
-package org.eclipse.jetty.client.spdy;
+package org.eclipse.jetty.spdy.client.http;
import java.util.concurrent.atomic.AtomicReference;
@@ -30,7 +30,7 @@ import org.eclipse.jetty.util.Promise;
public class HttpDestinationOverSPDY extends HttpDestination implements Promise
{
private final AtomicReference connect = new AtomicReference<>(ConnectState.DISCONNECTED);
- private volatile HttpConnectionOverSPDY connection;
+ private HttpConnectionOverSPDY connection;
public HttpDestinationOverSPDY(HttpClient client, String scheme, String host, int port)
{
@@ -48,7 +48,7 @@ public class HttpDestinationOverSPDY extends HttpDestination implements Promise<
case DISCONNECTED:
{
if (!connect.compareAndSet(current, ConnectState.CONNECTING))
- continue;
+ break;
newConnection(this);
return;
}
@@ -59,10 +59,9 @@ public class HttpDestinationOverSPDY extends HttpDestination implements Promise<
}
case CONNECTED:
{
- HttpConnectionOverSPDY connection = this.connection;
- if (connection != null)
- process(connection, false);
- break;
+ if (process(connection, false))
+ break;
+ return;
}
default:
{
@@ -75,14 +74,14 @@ public class HttpDestinationOverSPDY extends HttpDestination implements Promise<
@Override
public void succeeded(Connection result)
{
+ HttpConnectionOverSPDY connection = this.connection = (HttpConnectionOverSPDY)result;
if (connect.compareAndSet(ConnectState.CONNECTING, ConnectState.CONNECTED))
{
- HttpConnectionOverSPDY connection = this.connection = (HttpConnectionOverSPDY)result;
process(connection, true);
}
else
{
- result.close();
+ connection.close();
failed(new IllegalStateException());
}
}
@@ -93,39 +92,40 @@ public class HttpDestinationOverSPDY extends HttpDestination implements Promise<
connect.set(ConnectState.DISCONNECTED);
}
- private void process(final HttpConnectionOverSPDY connection, boolean dispatch)
+ private boolean process(final HttpConnectionOverSPDY connection, boolean dispatch)
{
HttpClient client = getHttpClient();
final HttpExchange exchange = getHttpExchanges().poll();
LOG.debug("Processing exchange {} on connection {}", exchange, connection);
- if (exchange != null)
+ if (exchange == null)
+ return false;
+
+ final Request request = exchange.getRequest();
+ Throwable cause = request.getAbortCause();
+ if (cause != null)
{
- final Request request = exchange.getRequest();
- Throwable cause = request.getAbortCause();
- if (cause != null)
+ LOG.debug("Abort before processing {}: {}", exchange, cause);
+ abort(exchange, cause);
+ }
+ else
+ {
+ if (dispatch)
{
- abort(exchange, cause);
- LOG.debug("Aborted before processing {}: {}", exchange, cause);
+ client.getExecutor().execute(new Runnable()
+ {
+ @Override
+ public void run()
+ {
+ connection.send(exchange);
+ }
+ });
}
else
{
- if (dispatch)
- {
- client.getExecutor().execute(new Runnable()
- {
- @Override
- public void run()
- {
- connection.send(exchange);
- }
- });
- }
- else
- {
- connection.send(exchange);
- }
+ connection.send(exchange);
}
}
+ return true;
}
private enum ConnectState
diff --git a/jetty-client/src/main/java/org/eclipse/jetty/client/spdy/HttpReceiverOverSPDY.java b/jetty-spdy/spdy-http-client-transport/src/main/java/org/eclipse/jetty/spdy/client/http/HttpReceiverOverSPDY.java
similarity index 56%
rename from jetty-client/src/main/java/org/eclipse/jetty/client/spdy/HttpReceiverOverSPDY.java
rename to jetty-spdy/spdy-http-client-transport/src/main/java/org/eclipse/jetty/spdy/client/http/HttpReceiverOverSPDY.java
index 8cc69fdafbe..a870b5e7d7c 100644
--- a/jetty-client/src/main/java/org/eclipse/jetty/client/spdy/HttpReceiverOverSPDY.java
+++ b/jetty-spdy/spdy-http-client-transport/src/main/java/org/eclipse/jetty/spdy/client/http/HttpReceiverOverSPDY.java
@@ -16,7 +16,7 @@
// ========================================================================
//
-package org.eclipse.jetty.client.spdy;
+package org.eclipse.jetty.spdy.client.http;
import org.eclipse.jetty.client.HttpExchange;
import org.eclipse.jetty.client.HttpReceiver;
@@ -32,6 +32,7 @@ import org.eclipse.jetty.spdy.api.RstInfo;
import org.eclipse.jetty.spdy.api.Stream;
import org.eclipse.jetty.spdy.api.StreamFrameListener;
import org.eclipse.jetty.spdy.api.StreamStatus;
+import org.eclipse.jetty.spdy.http.HTTPSPDYHeader;
import org.eclipse.jetty.util.Callback;
import org.eclipse.jetty.util.Fields;
@@ -55,37 +56,52 @@ public class HttpReceiverOverSPDY extends HttpReceiver implements StreamFrameLis
if (exchange == null)
return;
- HttpResponse response = exchange.getResponse();
-
- Fields fields = replyInfo.getHeaders();
- // TODO: use HTTPSPDYHeader enum
- HttpVersion version = HttpVersion.fromString(fields.get(":version").value());
- response.version(version);
- Integer status = fields.get(":status").valueAsInt();
- response.status(status);
- response.reason(HttpStatus.getMessage(status));
-
- onResponseBegin(exchange);
-
- for (Fields.Field field : fields)
+ try
{
- // TODO: handle multiple values properly
- // TODO: skip special headers
- HttpField httpField = new HttpField(field.name(), field.value());
- onResponseHeader(exchange, httpField);
+ HttpResponse response = exchange.getResponse();
+
+ Fields fields = replyInfo.getHeaders();
+ short spdy = stream.getSession().getVersion();
+ HttpVersion version = HttpVersion.fromString(fields.get(HTTPSPDYHeader.VERSION.name(spdy)).value());
+ response.version(version);
+ String[] status = fields.get(HTTPSPDYHeader.STATUS.name(spdy)).value().split(" ", 2);
+
+ Integer code = Integer.parseInt(status[0]);
+ response.status(code);
+ String reason = status.length < 2 ? HttpStatus.getMessage(code) : status[1];
+ response.reason(reason);
+
+ if (responseBegin(exchange))
+ {
+ for (Fields.Field field : fields)
+ {
+ String name = field.name();
+ if (HTTPSPDYHeader.from(spdy, name) != null)
+ continue;
+ // TODO: handle multiple values properly
+ HttpField httpField = new HttpField(name, field.value());
+ responseHeader(exchange, httpField);
+ }
+
+ if (responseHeaders(exchange))
+ {
+ if (replyInfo.isClose())
+ {
+ responseSuccess(exchange);
+ }
+ }
+ }
}
-
- onResponseHeaders(exchange);
-
- if (replyInfo.isClose())
+ catch (Exception x)
{
- onResponseSuccess(exchange);
+ responseFailure(x);
}
}
@Override
public StreamFrameListener onPush(Stream stream, PushInfo pushInfo)
{
+ // SPDY push not supported
getHttpChannel().getSession().rst(new RstInfo(stream.getId(), StreamStatus.REFUSED_STREAM), new Callback.Adapter());
return null;
}
@@ -103,14 +119,24 @@ public class HttpReceiverOverSPDY extends HttpReceiver implements StreamFrameLis
if (exchange == null)
return;
- int length = dataInfo.length();
- // TODO: avoid data copy here
- onResponseContent(exchange, dataInfo.asByteBuffer(false));
- dataInfo.consume(length);
-
- if (dataInfo.isClose())
+ try
{
- onResponseSuccess(exchange);
+ int length = dataInfo.length();
+ // TODO: avoid data copy here
+ boolean process = responseContent(exchange, dataInfo.asByteBuffer(false));
+ dataInfo.consume(length);
+
+ if (process)
+ {
+ if (dataInfo.isClose())
+ {
+ responseSuccess(exchange);
+ }
+ }
+ }
+ catch (Exception x)
+ {
+ responseFailure(x);
}
}
}
diff --git a/jetty-client/src/main/java/org/eclipse/jetty/client/spdy/HttpSenderOverSPDY.java b/jetty-spdy/spdy-http-client-transport/src/main/java/org/eclipse/jetty/spdy/client/http/HttpSenderOverSPDY.java
similarity index 60%
rename from jetty-client/src/main/java/org/eclipse/jetty/client/spdy/HttpSenderOverSPDY.java
rename to jetty-spdy/spdy-http-client-transport/src/main/java/org/eclipse/jetty/spdy/client/http/HttpSenderOverSPDY.java
index 5ff7ca2dec0..34ac5a5da04 100644
--- a/jetty-client/src/main/java/org/eclipse/jetty/client/spdy/HttpSenderOverSPDY.java
+++ b/jetty-spdy/spdy-http-client-transport/src/main/java/org/eclipse/jetty/spdy/client/http/HttpSenderOverSPDY.java
@@ -16,17 +16,18 @@
// ========================================================================
//
-package org.eclipse.jetty.client.spdy;
+package org.eclipse.jetty.spdy.client.http;
import org.eclipse.jetty.client.HttpContent;
import org.eclipse.jetty.client.HttpExchange;
import org.eclipse.jetty.client.HttpSender;
import org.eclipse.jetty.client.api.Request;
import org.eclipse.jetty.http.HttpField;
-import org.eclipse.jetty.http.HttpFields;
import org.eclipse.jetty.spdy.api.ByteBufferDataInfo;
import org.eclipse.jetty.spdy.api.Stream;
import org.eclipse.jetty.spdy.api.SynInfo;
+import org.eclipse.jetty.spdy.http.HTTPSPDYHeader;
+import org.eclipse.jetty.util.Callback;
import org.eclipse.jetty.util.Fields;
import org.eclipse.jetty.util.Promise;
@@ -46,14 +47,33 @@ public class HttpSenderOverSPDY extends HttpSender
}
@Override
- protected void sendHeaders(HttpExchange exchange, final HttpContent content)
+ protected void sendHeaders(HttpExchange exchange, final HttpContent content, final Callback callback)
{
final Request request = exchange.getRequest();
+ short spdyVersion = getHttpChannel().getSession().getVersion();
Fields fields = new Fields();
- HttpFields headers = request.getHeaders();
- for (HttpField header : headers)
- fields.add(header.getName(), header.getValue());
+ HttpField hostHeader = null;
+ for (HttpField header : request.getHeaders())
+ {
+ String name = header.getName();
+ // The host header needs a special treatment
+ if (HTTPSPDYHeader.from(spdyVersion, name) != HTTPSPDYHeader.HOST)
+ fields.add(name, header.getValue());
+ else
+ hostHeader = header;
+ }
+
+ // Add special SPDY headers
+ fields.put(HTTPSPDYHeader.METHOD.name(spdyVersion), request.getMethod().asString());
+ String path = request.getPath();
+ String query = request.getQuery();
+ if (query != null)
+ path += "?" + query;
+ fields.put(HTTPSPDYHeader.URI.name(spdyVersion), path);
+ fields.put(HTTPSPDYHeader.VERSION.name(spdyVersion), request.getVersion().asString());
+ if (hostHeader != null)
+ fields.put(HTTPSPDYHeader.HOST.name(spdyVersion), hostHeader.getValue());
SynInfo synInfo = new SynInfo(fields, !content.hasContent());
getHttpChannel().getSession().syn(synInfo, getHttpChannel().getHttpReceiver(), new Promise()
@@ -63,23 +83,29 @@ public class HttpSenderOverSPDY extends HttpSender
{
if (content.hasContent())
HttpSenderOverSPDY.this.stream = stream;
- content.succeeded();
+ callback.succeeded();
}
@Override
public void failed(Throwable failure)
{
- content.failed(failure);
+ callback.failed(failure);
}
});
}
@Override
- protected void sendContent(HttpExchange exchange, HttpContent content)
+ protected void sendContent(HttpExchange exchange, HttpContent content, Callback callback)
{
- assert stream != null;
- ByteBufferDataInfo dataInfo = new ByteBufferDataInfo(content.getByteBuffer(), content.isLast());
- stream.data(dataInfo, content);
+ if (content.isConsumed())
+ {
+ callback.succeeded();
+ }
+ else
+ {
+ ByteBufferDataInfo dataInfo = new ByteBufferDataInfo(content.getByteBuffer(), content.isLast());
+ stream.data(dataInfo, callback);
+ }
}
@Override
diff --git a/jetty-spdy/spdy-http-client-transport/src/test/java/org/eclipse/jetty/spdy/client/http/AbstractHttpClientServerTest.java b/jetty-spdy/spdy-http-client-transport/src/test/java/org/eclipse/jetty/spdy/client/http/AbstractHttpClientServerTest.java
new file mode 100644
index 00000000000..644067fcff7
--- /dev/null
+++ b/jetty-spdy/spdy-http-client-transport/src/test/java/org/eclipse/jetty/spdy/client/http/AbstractHttpClientServerTest.java
@@ -0,0 +1,82 @@
+//
+// ========================================================================
+// Copyright (c) 1995-2013 Mort Bay Consulting Pty. Ltd.
+// ------------------------------------------------------------------------
+// All rights reserved. This program and the accompanying materials
+// are made available under the terms of the Eclipse Public License v1.0
+// and Apache License v2.0 which accompanies this distribution.
+//
+// The Eclipse Public License is available at
+// http://www.eclipse.org/legal/epl-v10.html
+//
+// The Apache License v2.0 is available at
+// http://www.opensource.org/licenses/apache2.0.php
+//
+// You may elect to redistribute this code under either of these licenses.
+// ========================================================================
+//
+
+package org.eclipse.jetty.spdy.client.http;
+
+import org.eclipse.jetty.client.HttpClient;
+import org.eclipse.jetty.http.HttpScheme;
+import org.eclipse.jetty.server.Handler;
+import org.eclipse.jetty.server.HttpConfiguration;
+import org.eclipse.jetty.server.NetworkConnector;
+import org.eclipse.jetty.server.Server;
+import org.eclipse.jetty.server.ServerConnector;
+import org.eclipse.jetty.spdy.api.SPDY;
+import org.eclipse.jetty.spdy.client.SPDYClient;
+import org.eclipse.jetty.spdy.server.http.HTTPSPDYServerConnectionFactory;
+import org.eclipse.jetty.spdy.server.http.PushStrategy;
+import org.eclipse.jetty.toolchain.test.TestTracker;
+import org.eclipse.jetty.util.thread.QueuedThreadPool;
+import org.junit.After;
+import org.junit.Rule;
+
+public abstract class AbstractHttpClientServerTest
+{
+ @Rule
+ public final TestTracker tracker = new TestTracker();
+
+ protected Server server;
+ protected NetworkConnector connector;
+ protected SPDYClient.Factory factory;
+ protected HttpClient client;
+ protected String scheme = HttpScheme.HTTP.asString();
+
+ public void start(Handler handler) throws Exception
+ {
+ server = new Server();
+
+ short version = SPDY.V3;
+
+ HTTPSPDYServerConnectionFactory spdyConnectionFactory = new HTTPSPDYServerConnectionFactory(version, new HttpConfiguration(), new PushStrategy.None());
+ connector = new ServerConnector(server, spdyConnectionFactory);
+
+ server.addConnector(connector);
+ server.setHandler(handler);
+ server.start();
+
+ QueuedThreadPool executor = new QueuedThreadPool();
+ executor.setName(executor.getName() + "-client");
+
+ factory = new SPDYClient.Factory(executor);
+ factory.start();
+
+ client = new HttpClient(new HttpClientTransportOverSPDY(factory.newSPDYClient(version)), null);
+ client.setExecutor(executor);
+ client.start();
+ }
+
+ @After
+ public void dispose() throws Exception
+ {
+ if (client != null)
+ client.stop();
+ if (factory != null)
+ factory.stop();
+ if (server != null)
+ server.stop();
+ }
+}
diff --git a/jetty-spdy/spdy-http-client-transport/src/test/java/org/eclipse/jetty/spdy/client/http/EmptyServerHandler.java b/jetty-spdy/spdy-http-client-transport/src/test/java/org/eclipse/jetty/spdy/client/http/EmptyServerHandler.java
new file mode 100644
index 00000000000..dfa5b95ad8b
--- /dev/null
+++ b/jetty-spdy/spdy-http-client-transport/src/test/java/org/eclipse/jetty/spdy/client/http/EmptyServerHandler.java
@@ -0,0 +1,36 @@
+//
+// ========================================================================
+// Copyright (c) 1995-2013 Mort Bay Consulting Pty. Ltd.
+// ------------------------------------------------------------------------
+// All rights reserved. This program and the accompanying materials
+// are made available under the terms of the Eclipse Public License v1.0
+// and Apache License v2.0 which accompanies this distribution.
+//
+// The Eclipse Public License is available at
+// http://www.eclipse.org/legal/epl-v10.html
+//
+// The Apache License v2.0 is available at
+// http://www.opensource.org/licenses/apache2.0.php
+//
+// You may elect to redistribute this code under either of these licenses.
+// ========================================================================
+//
+
+package org.eclipse.jetty.spdy.client.http;
+
+import java.io.IOException;
+import javax.servlet.ServletException;
+import javax.servlet.http.HttpServletRequest;
+import javax.servlet.http.HttpServletResponse;
+
+import org.eclipse.jetty.server.Request;
+import org.eclipse.jetty.server.handler.AbstractHandler;
+
+public class EmptyServerHandler extends AbstractHandler
+{
+ @Override
+ public void handle(String target, Request baseRequest, HttpServletRequest request, HttpServletResponse response) throws IOException, ServletException
+ {
+ baseRequest.setHandled(true);
+ }
+}
diff --git a/jetty-spdy/spdy-http-client-transport/src/test/java/org/eclipse/jetty/spdy/client/http/HttpClientTest.java b/jetty-spdy/spdy-http-client-transport/src/test/java/org/eclipse/jetty/spdy/client/http/HttpClientTest.java
new file mode 100644
index 00000000000..76e29eeab0b
--- /dev/null
+++ b/jetty-spdy/spdy-http-client-transport/src/test/java/org/eclipse/jetty/spdy/client/http/HttpClientTest.java
@@ -0,0 +1,422 @@
+//
+// ========================================================================
+// Copyright (c) 1995-2013 Mort Bay Consulting Pty. Ltd.
+// ------------------------------------------------------------------------
+// All rights reserved. This program and the accompanying materials
+// are made available under the terms of the Eclipse Public License v1.0
+// and Apache License v2.0 which accompanies this distribution.
+//
+// The Eclipse Public License is available at
+// http://www.eclipse.org/legal/epl-v10.html
+//
+// The Apache License v2.0 is available at
+// http://www.opensource.org/licenses/apache2.0.php
+//
+// You may elect to redistribute this code under either of these licenses.
+// ========================================================================
+//
+
+package org.eclipse.jetty.spdy.client.http;
+
+import java.io.IOException;
+import java.net.URI;
+import java.net.URLEncoder;
+import java.nio.ByteBuffer;
+import java.util.Arrays;
+import java.util.concurrent.ExecutionException;
+import java.util.concurrent.TimeUnit;
+import java.util.concurrent.TimeoutException;
+import java.util.concurrent.atomic.AtomicInteger;
+import java.util.zip.GZIPOutputStream;
+import javax.servlet.ServletException;
+import javax.servlet.ServletOutputStream;
+import javax.servlet.http.HttpServletRequest;
+import javax.servlet.http.HttpServletResponse;
+
+import org.eclipse.jetty.client.api.ContentResponse;
+import org.eclipse.jetty.client.api.Request;
+import org.eclipse.jetty.client.api.Response;
+import org.eclipse.jetty.client.util.BytesContentProvider;
+import org.eclipse.jetty.http.HttpMethod;
+import org.eclipse.jetty.server.handler.AbstractHandler;
+import org.eclipse.jetty.toolchain.test.annotation.Slow;
+import org.junit.Assert;
+import org.junit.Test;
+
+public class HttpClientTest extends AbstractHttpClientServerTest
+{
+ @Test
+ public void test_GET_ResponseWithoutContent() throws Exception
+ {
+ start(new EmptyServerHandler());
+
+ Response response = client.GET(scheme + "://localhost:" + connector.getLocalPort());
+
+ Assert.assertNotNull(response);
+ Assert.assertEquals(200, response.getStatus());
+ }
+
+ @Test
+ public void test_GET_ResponseWithContent() throws Exception
+ {
+ final byte[] data = new byte[]{0, 1, 2, 3, 4, 5, 6, 7};
+ start(new AbstractHandler()
+ {
+ @Override
+ public void handle(String target, org.eclipse.jetty.server.Request baseRequest, HttpServletRequest request, HttpServletResponse response) throws IOException, ServletException
+ {
+ response.getOutputStream().write(data);
+ baseRequest.setHandled(true);
+ }
+ });
+
+ ContentResponse response = client.GET(scheme + "://localhost:" + connector.getLocalPort());
+
+ Assert.assertNotNull(response);
+ Assert.assertEquals(200, response.getStatus());
+ byte[] content = response.getContent();
+ Assert.assertArrayEquals(data, content);
+ }
+
+ @Test
+ public void test_GET_WithParameters_ResponseWithContent() throws Exception
+ {
+ final String paramName1 = "a";
+ final String paramName2 = "b";
+ start(new AbstractHandler()
+ {
+ @Override
+ public void handle(String target, org.eclipse.jetty.server.Request baseRequest, HttpServletRequest request, HttpServletResponse response) throws IOException, ServletException
+ {
+ response.setCharacterEncoding("UTF-8");
+ ServletOutputStream output = response.getOutputStream();
+ String paramValue1 = request.getParameter(paramName1);
+ output.write(paramValue1.getBytes("UTF-8"));
+ String paramValue2 = request.getParameter(paramName2);
+ Assert.assertEquals("", paramValue2);
+ output.write("empty".getBytes("UTF-8"));
+ baseRequest.setHandled(true);
+ }
+ });
+
+ String value1 = "\u20AC";
+ String paramValue1 = URLEncoder.encode(value1, "UTF-8");
+ String query = paramName1 + "=" + paramValue1 + "&" + paramName2;
+ ContentResponse response = client.GET(scheme + "://localhost:" + connector.getLocalPort() + "/?" + query);
+
+ Assert.assertNotNull(response);
+ Assert.assertEquals(200, response.getStatus());
+ String content = new String(response.getContent(), "UTF-8");
+ Assert.assertEquals(value1 + "empty", content);
+ }
+
+ @Test
+ public void test_GET_WithParametersMultiValued_ResponseWithContent() throws Exception
+ {
+ final String paramName1 = "a";
+ final String paramName2 = "b";
+ start(new AbstractHandler()
+ {
+ @Override
+ public void handle(String target, org.eclipse.jetty.server.Request baseRequest, HttpServletRequest request, HttpServletResponse response) throws IOException, ServletException
+ {
+ response.setCharacterEncoding("UTF-8");
+ ServletOutputStream output = response.getOutputStream();
+ String[] paramValues1 = request.getParameterValues(paramName1);
+ for (String paramValue : paramValues1)
+ output.write(paramValue.getBytes("UTF-8"));
+ String paramValue2 = request.getParameter(paramName2);
+ output.write(paramValue2.getBytes("UTF-8"));
+ baseRequest.setHandled(true);
+ }
+ });
+
+ String value11 = "\u20AC";
+ String value12 = "\u20AA";
+ String value2 = "&";
+ String paramValue11 = URLEncoder.encode(value11, "UTF-8");
+ String paramValue12 = URLEncoder.encode(value12, "UTF-8");
+ String paramValue2 = URLEncoder.encode(value2, "UTF-8");
+ String query = paramName1 + "=" + paramValue11 + "&" + paramName1 + "=" + paramValue12 + "&" + paramName2 + "=" + paramValue2;
+ ContentResponse response = client.GET(scheme + "://localhost:" + connector.getLocalPort() + "/?" + query);
+
+ Assert.assertNotNull(response);
+ Assert.assertEquals(200, response.getStatus());
+ String content = new String(response.getContent(), "UTF-8");
+ Assert.assertEquals(value11 + value12 + value2, content);
+ }
+
+ @Test
+ public void test_POST_WithParameters() throws Exception
+ {
+ final String paramName = "a";
+ final String paramValue = "\u20AC";
+ start(new AbstractHandler()
+ {
+ @Override
+ public void handle(String target, org.eclipse.jetty.server.Request baseRequest, HttpServletRequest request, HttpServletResponse response) throws IOException, ServletException
+ {
+ baseRequest.setHandled(true);
+ String value = request.getParameter(paramName);
+ if (paramValue.equals(value))
+ {
+ response.setCharacterEncoding("UTF-8");
+ response.setContentType("text/plain");
+ response.getOutputStream().print(value);
+ }
+ }
+ });
+
+ ContentResponse response = client.POST(scheme + "://localhost:" + connector.getLocalPort())
+ .param(paramName, paramValue)
+ .timeout(5, TimeUnit.SECONDS)
+ .send();
+
+ Assert.assertNotNull(response);
+ Assert.assertEquals(200, response.getStatus());
+ Assert.assertEquals(paramValue, new String(response.getContent(), "UTF-8"));
+ }
+
+ @Test
+ public void test_PUT_WithParameters() throws Exception
+ {
+ final String paramName = "a";
+ final String paramValue = "\u20AC";
+ start(new AbstractHandler()
+ {
+ @Override
+ public void handle(String target, org.eclipse.jetty.server.Request baseRequest, HttpServletRequest request, HttpServletResponse response) throws IOException, ServletException
+ {
+ baseRequest.setHandled(true);
+ String value = request.getParameter(paramName);
+ if (paramValue.equals(value))
+ {
+ response.setCharacterEncoding("UTF-8");
+ response.setContentType("text/plain");
+ response.getOutputStream().print(value);
+ }
+ }
+ });
+
+ URI uri = URI.create(scheme + "://localhost:" + connector.getLocalPort() + "/path?" + paramName + "=" + paramValue);
+ ContentResponse response = client.newRequest(uri)
+ .method(HttpMethod.PUT)
+ .timeout(5, TimeUnit.SECONDS)
+ .send();
+
+ Assert.assertNotNull(response);
+ Assert.assertEquals(200, response.getStatus());
+ Assert.assertEquals(paramValue, new String(response.getContent(), "UTF-8"));
+ }
+
+ @Test
+ public void test_POST_WithParameters_WithContent() throws Exception
+ {
+ final byte[] content = {0, 1, 2, 3};
+ final String paramName = "a";
+ final String paramValue = "\u20AC";
+ start(new AbstractHandler()
+ {
+ @Override
+ public void handle(String target, org.eclipse.jetty.server.Request baseRequest, HttpServletRequest request, HttpServletResponse response) throws IOException, ServletException
+ {
+ baseRequest.setHandled(true);
+ String value = request.getParameter(paramName);
+ if (paramValue.equals(value))
+ {
+ response.setCharacterEncoding("UTF-8");
+ response.setContentType("application/octet-stream");
+ response.getOutputStream().write(content);
+ }
+ }
+ });
+
+ ContentResponse response = client.POST(scheme + "://localhost:" + connector.getLocalPort() + "/?b=1")
+ .param(paramName, paramValue)
+ .content(new BytesContentProvider(content))
+ .timeout(555, TimeUnit.SECONDS)
+ .send();
+
+ Assert.assertNotNull(response);
+ Assert.assertEquals(200, response.getStatus());
+ Assert.assertArrayEquals(content, response.getContent());
+ }
+
+ @Test
+ public void test_POST_WithContent_NotifiesRequestContentListener() throws Exception
+ {
+ final byte[] content = {0, 1, 2, 3};
+ start(new EmptyServerHandler());
+
+ ContentResponse response = client.POST(scheme + "://localhost:" + connector.getLocalPort())
+ .onRequestContent(new Request.ContentListener()
+ {
+ @Override
+ public void onContent(Request request, ByteBuffer buffer)
+ {
+ byte[] bytes = new byte[buffer.remaining()];
+ buffer.get(bytes);
+ if (!Arrays.equals(content, bytes))
+ request.abort(new Exception());
+ }
+ })
+ .content(new BytesContentProvider(content))
+ .timeout(5, TimeUnit.SECONDS)
+ .send();
+
+ Assert.assertNotNull(response);
+ Assert.assertEquals(200, response.getStatus());
+ }
+
+ @Test
+ public void test_POST_WithContent_TracksProgress() throws Exception
+ {
+ start(new EmptyServerHandler());
+
+ final AtomicInteger progress = new AtomicInteger();
+ ContentResponse response = client.POST(scheme + "://localhost:" + connector.getLocalPort())
+ .onRequestContent(new Request.ContentListener()
+ {
+ @Override
+ public void onContent(Request request, ByteBuffer buffer)
+ {
+ byte[] bytes = new byte[buffer.remaining()];
+ Assert.assertEquals(1, bytes.length);
+ buffer.get(bytes);
+ Assert.assertEquals(bytes[0], progress.getAndIncrement());
+ }
+ })
+ .content(new BytesContentProvider(new byte[]{0}, new byte[]{1}, new byte[]{2}, new byte[]{3}, new byte[]{4}))
+ .timeout(5, TimeUnit.SECONDS)
+ .send();
+
+ Assert.assertNotNull(response);
+ Assert.assertEquals(200, response.getStatus());
+ Assert.assertEquals(5, progress.get());
+ }
+
+ @Test
+ public void test_GZIP_ContentEncoding() throws Exception
+ {
+ final byte[] data = {0, 1, 2, 3, 4, 5, 6, 7, 8, 9};
+ start(new AbstractHandler()
+ {
+ @Override
+ public void handle(String target, org.eclipse.jetty.server.Request baseRequest, HttpServletRequest request, HttpServletResponse response) throws IOException, ServletException
+ {
+ baseRequest.setHandled(true);
+ response.setHeader("Content-Encoding", "gzip");
+ GZIPOutputStream gzipOutput = new GZIPOutputStream(response.getOutputStream());
+ gzipOutput.write(data);
+ gzipOutput.finish();
+ }
+ });
+
+ ContentResponse response = client.newRequest("localhost", connector.getLocalPort())
+ .scheme(scheme)
+ .timeout(5, TimeUnit.SECONDS)
+ .send();
+
+ Assert.assertEquals(200, response.getStatus());
+ Assert.assertArrayEquals(data, response.getContent());
+ }
+
+ @Slow
+ @Test
+ public void test_Request_IdleTimeout() throws Exception
+ {
+ final long idleTimeout = 1000;
+ start(new AbstractHandler()
+ {
+ @Override
+ public void handle(String target, org.eclipse.jetty.server.Request baseRequest, HttpServletRequest request, HttpServletResponse response) throws IOException, ServletException
+ {
+ try
+ {
+ baseRequest.setHandled(true);
+ TimeUnit.MILLISECONDS.sleep(2 * idleTimeout);
+ }
+ catch (InterruptedException x)
+ {
+ throw new ServletException(x);
+ }
+ }
+ });
+
+ final String host = "localhost";
+ final int port = connector.getLocalPort();
+ try
+ {
+ client.newRequest(host, port)
+ .scheme(scheme)
+ .idleTimeout(idleTimeout, TimeUnit.MILLISECONDS)
+ .timeout(3 * idleTimeout, TimeUnit.MILLISECONDS)
+ .send();
+ Assert.fail();
+ }
+ catch (ExecutionException expected)
+ {
+ Assert.assertTrue(expected.getCause() instanceof TimeoutException);
+ }
+
+ // Make another request without specifying the idle timeout, should not fail
+ ContentResponse response = client.newRequest(host, port)
+ .scheme(scheme)
+ .timeout(3 * idleTimeout, TimeUnit.MILLISECONDS)
+ .send();
+
+ Assert.assertNotNull(response);
+ Assert.assertEquals(200, response.getStatus());
+ }
+
+ @Test
+ public void testSendToIPv6Address() throws Exception
+ {
+ start(new EmptyServerHandler());
+
+ ContentResponse response = client.newRequest("[::1]", connector.getLocalPort())
+ .scheme(scheme)
+ .timeout(5, TimeUnit.SECONDS)
+ .send();
+
+ Assert.assertNotNull(response);
+ Assert.assertEquals(200, response.getStatus());
+ }
+
+ @Test
+ public void test_HEAD_With_ResponseContentLength() throws Exception
+ {
+ final int length = 1024;
+ start(new AbstractHandler()
+ {
+ @Override
+ public void handle(String target, org.eclipse.jetty.server.Request baseRequest, HttpServletRequest request, HttpServletResponse response) throws IOException, ServletException
+ {
+ baseRequest.setHandled(true);
+ response.getOutputStream().write(new byte[length]);
+ }
+ });
+
+ // HEAD requests receive a Content-Length header, but do not
+ // receive the content so they must handle this case properly
+ ContentResponse response = client.newRequest("localhost", connector.getLocalPort())
+ .scheme(scheme)
+ .method(HttpMethod.HEAD)
+ .timeout(5, TimeUnit.SECONDS)
+ .send();
+
+ Assert.assertNotNull(response);
+ Assert.assertEquals(200, response.getStatus());
+ Assert.assertEquals(0, response.getContent().length);
+
+ // Perform a normal GET request to be sure the content is now read
+ response = client.newRequest("localhost", connector.getLocalPort())
+ .scheme(scheme)
+ .timeout(5, TimeUnit.SECONDS)
+ .send();
+
+ Assert.assertNotNull(response);
+ Assert.assertEquals(200, response.getStatus());
+ Assert.assertEquals(length, response.getContent().length);
+ }
+}
diff --git a/jetty-spdy/spdy-http-client-transport/src/test/resources/jetty-logging.properties b/jetty-spdy/spdy-http-client-transport/src/test/resources/jetty-logging.properties
new file mode 100644
index 00000000000..8163013d571
--- /dev/null
+++ b/jetty-spdy/spdy-http-client-transport/src/test/resources/jetty-logging.properties
@@ -0,0 +1,4 @@
+org.eclipse.jetty.util.log.class=org.eclipse.jetty.util.log.StdErrLog
+#org.eclipse.jetty.LEVEL=DEBUG
+#org.eclipse.jetty.client.LEVEL=DEBUG
+#org.eclipse.jetty.spdy.LEVEL=DEBUG
diff --git a/jetty-spdy/spdy-http-client-transport/src/test/resources/keystore.jks b/jetty-spdy/spdy-http-client-transport/src/test/resources/keystore.jks
new file mode 100644
index 00000000000..428ba54776e
Binary files /dev/null and b/jetty-spdy/spdy-http-client-transport/src/test/resources/keystore.jks differ
diff --git a/jetty-spdy/spdy-http-client-transport/src/test/resources/truststore.jks b/jetty-spdy/spdy-http-client-transport/src/test/resources/truststore.jks
new file mode 100644
index 00000000000..839cb8c3515
Binary files /dev/null and b/jetty-spdy/spdy-http-client-transport/src/test/resources/truststore.jks differ