From d8e51cca72c855483df9c6900c6678a553fc3f16 Mon Sep 17 00:00:00 2001
From: Jesse McConnell
Date: Wed, 29 Aug 2012 11:49:16 -0500
Subject: [PATCH] add jetty-rhttp from codehaus to jetty-9
---
jetty-rhttp/README.txt | 33 ++
jetty-rhttp/jetty-rhttp-client/pom.xml | 56 +++
.../jetty/rhttp/client/AbstractClient.java | 270 +++++++++++
.../jetty/rhttp/client/ApacheClient.java | 156 +++++++
.../jetty/rhttp/client/ClientListener.java | 67 +++
.../jetty/rhttp/client/JettyClient.java | 306 +++++++++++++
.../jetty/rhttp/client/RHTTPClient.java | 133 ++++++
.../jetty/rhttp/client/RHTTPListener.java | 36 ++
.../jetty/rhttp/client/RHTTPRequest.java | 266 +++++++++++
.../jetty/rhttp/client/RHTTPResponse.java | 256 +++++++++++
.../rhttp/client/RetryingApacheClient.java | 112 +++++
.../jetty/rhttp/client/ApacheClientTest.java | 75 +++
.../jetty/rhttp/client/ClientTest.java | 299 ++++++++++++
.../jetty/rhttp/client/JettyClientTest.java | 52 +++
.../jetty/rhttp/client/RequestTest.java | 85 ++++
.../jetty/rhttp/client/ResponseTest.java | 85 ++++
jetty-rhttp/jetty-rhttp-connector/pom.xml | 62 +++
.../src/main/config/etc/jetty-rhttp.xml | 23 +
.../rhttp/connector/ReverseHTTPConnector.java | 170 +++++++
.../connector/ReverseHTTPConnectorTest.java | 187 ++++++++
.../rhttp/connector/TestReverseServer.java | 58 +++
jetty-rhttp/jetty-rhttp-gateway/pom.xml | 65 +++
.../jetty/rhttp/gateway/ClientDelegate.java | 92 ++++
.../jetty/rhttp/gateway/ConnectorServlet.java | 224 +++++++++
.../jetty/rhttp/gateway/ExternalRequest.java | 54 +++
.../jetty/rhttp/gateway/ExternalServlet.java | 88 ++++
.../eclipse/jetty/rhttp/gateway/Gateway.java | 99 ++++
.../rhttp/gateway/GatewayProxyServer.java | 228 ++++++++++
.../jetty/rhttp/gateway/GatewayServer.java | 171 +++++++
.../rhttp/gateway/HostTargetIdRetriever.java | 54 +++
.../org/eclipse/jetty/rhttp/gateway/Main.java | 128 ++++++
.../rhttp/gateway/StandardClientDelegate.java | 172 +++++++
.../gateway/StandardExternalRequest.java | 189 ++++++++
.../jetty/rhttp/gateway/StandardGateway.java | 131 ++++++
.../gateway/StandardTargetIdRetriever.java | 40 ++
.../rhttp/gateway/TargetIdRetriever.java | 40 ++
.../eclipse/jetty/rhttp/gateway/Utils.java | 40 ++
.../rhttp/gateway/ClientTimeoutTest.java | 115 +++++
.../rhttp/gateway/DisconnectClientTest.java | 93 ++++
.../rhttp/gateway/DuplicateClientTest.java | 84 ++++
.../ExternalRequestNotSuspendedTest.java | 186 ++++++++
.../rhttp/gateway/ExternalTimeoutTest.java | 126 +++++
.../rhttp/gateway/GatewayEchoServer.java | 109 +++++
.../jetty/rhttp/gateway/GatewayEchoTest.java | 77 ++++
.../jetty/rhttp/gateway/GatewayLoadTest.java | 202 +++++++++
.../rhttp/gateway/GatewayTimeoutTest.java | 130 ++++++
.../rhttp/gateway/HandshakeClientTest.java | 75 +++
.../gateway/HostTargetIdRetrieverTest.java | 107 +++++
.../src/test/resources/log4j.properties | 13 +
jetty-rhttp/jetty-rhttp-loadtest/pom.xml | 58 +++
.../eclipse/jetty/rhttp/loadtest/Loader.java | 429 ++++++++++++++++++
.../eclipse/jetty/rhttp/loadtest/Server.java | 69 +++
.../src/main/resources/log4j.properties | 13 +
jetty-rhttp/pom.xml | 108 +++++
54 files changed, 6596 insertions(+)
create mode 100644 jetty-rhttp/README.txt
create mode 100644 jetty-rhttp/jetty-rhttp-client/pom.xml
create mode 100644 jetty-rhttp/jetty-rhttp-client/src/main/java/org/eclipse/jetty/rhttp/client/AbstractClient.java
create mode 100644 jetty-rhttp/jetty-rhttp-client/src/main/java/org/eclipse/jetty/rhttp/client/ApacheClient.java
create mode 100644 jetty-rhttp/jetty-rhttp-client/src/main/java/org/eclipse/jetty/rhttp/client/ClientListener.java
create mode 100644 jetty-rhttp/jetty-rhttp-client/src/main/java/org/eclipse/jetty/rhttp/client/JettyClient.java
create mode 100644 jetty-rhttp/jetty-rhttp-client/src/main/java/org/eclipse/jetty/rhttp/client/RHTTPClient.java
create mode 100644 jetty-rhttp/jetty-rhttp-client/src/main/java/org/eclipse/jetty/rhttp/client/RHTTPListener.java
create mode 100644 jetty-rhttp/jetty-rhttp-client/src/main/java/org/eclipse/jetty/rhttp/client/RHTTPRequest.java
create mode 100644 jetty-rhttp/jetty-rhttp-client/src/main/java/org/eclipse/jetty/rhttp/client/RHTTPResponse.java
create mode 100644 jetty-rhttp/jetty-rhttp-client/src/main/java/org/eclipse/jetty/rhttp/client/RetryingApacheClient.java
create mode 100644 jetty-rhttp/jetty-rhttp-client/src/test/java/org/eclipse/jetty/rhttp/client/ApacheClientTest.java
create mode 100644 jetty-rhttp/jetty-rhttp-client/src/test/java/org/eclipse/jetty/rhttp/client/ClientTest.java
create mode 100644 jetty-rhttp/jetty-rhttp-client/src/test/java/org/eclipse/jetty/rhttp/client/JettyClientTest.java
create mode 100644 jetty-rhttp/jetty-rhttp-client/src/test/java/org/eclipse/jetty/rhttp/client/RequestTest.java
create mode 100644 jetty-rhttp/jetty-rhttp-client/src/test/java/org/eclipse/jetty/rhttp/client/ResponseTest.java
create mode 100644 jetty-rhttp/jetty-rhttp-connector/pom.xml
create mode 100644 jetty-rhttp/jetty-rhttp-connector/src/main/config/etc/jetty-rhttp.xml
create mode 100644 jetty-rhttp/jetty-rhttp-connector/src/main/java/org/eclipse/jetty/rhttp/connector/ReverseHTTPConnector.java
create mode 100644 jetty-rhttp/jetty-rhttp-connector/src/test/java/org/eclipse/jetty/rhttp/connector/ReverseHTTPConnectorTest.java
create mode 100644 jetty-rhttp/jetty-rhttp-connector/src/test/java/org/eclipse/jetty/rhttp/connector/TestReverseServer.java
create mode 100644 jetty-rhttp/jetty-rhttp-gateway/pom.xml
create mode 100644 jetty-rhttp/jetty-rhttp-gateway/src/main/java/org/eclipse/jetty/rhttp/gateway/ClientDelegate.java
create mode 100644 jetty-rhttp/jetty-rhttp-gateway/src/main/java/org/eclipse/jetty/rhttp/gateway/ConnectorServlet.java
create mode 100644 jetty-rhttp/jetty-rhttp-gateway/src/main/java/org/eclipse/jetty/rhttp/gateway/ExternalRequest.java
create mode 100644 jetty-rhttp/jetty-rhttp-gateway/src/main/java/org/eclipse/jetty/rhttp/gateway/ExternalServlet.java
create mode 100644 jetty-rhttp/jetty-rhttp-gateway/src/main/java/org/eclipse/jetty/rhttp/gateway/Gateway.java
create mode 100644 jetty-rhttp/jetty-rhttp-gateway/src/main/java/org/eclipse/jetty/rhttp/gateway/GatewayProxyServer.java
create mode 100644 jetty-rhttp/jetty-rhttp-gateway/src/main/java/org/eclipse/jetty/rhttp/gateway/GatewayServer.java
create mode 100644 jetty-rhttp/jetty-rhttp-gateway/src/main/java/org/eclipse/jetty/rhttp/gateway/HostTargetIdRetriever.java
create mode 100644 jetty-rhttp/jetty-rhttp-gateway/src/main/java/org/eclipse/jetty/rhttp/gateway/Main.java
create mode 100644 jetty-rhttp/jetty-rhttp-gateway/src/main/java/org/eclipse/jetty/rhttp/gateway/StandardClientDelegate.java
create mode 100644 jetty-rhttp/jetty-rhttp-gateway/src/main/java/org/eclipse/jetty/rhttp/gateway/StandardExternalRequest.java
create mode 100644 jetty-rhttp/jetty-rhttp-gateway/src/main/java/org/eclipse/jetty/rhttp/gateway/StandardGateway.java
create mode 100644 jetty-rhttp/jetty-rhttp-gateway/src/main/java/org/eclipse/jetty/rhttp/gateway/StandardTargetIdRetriever.java
create mode 100644 jetty-rhttp/jetty-rhttp-gateway/src/main/java/org/eclipse/jetty/rhttp/gateway/TargetIdRetriever.java
create mode 100644 jetty-rhttp/jetty-rhttp-gateway/src/main/java/org/eclipse/jetty/rhttp/gateway/Utils.java
create mode 100644 jetty-rhttp/jetty-rhttp-gateway/src/test/java/org/eclipse/jetty/rhttp/gateway/ClientTimeoutTest.java
create mode 100644 jetty-rhttp/jetty-rhttp-gateway/src/test/java/org/eclipse/jetty/rhttp/gateway/DisconnectClientTest.java
create mode 100644 jetty-rhttp/jetty-rhttp-gateway/src/test/java/org/eclipse/jetty/rhttp/gateway/DuplicateClientTest.java
create mode 100644 jetty-rhttp/jetty-rhttp-gateway/src/test/java/org/eclipse/jetty/rhttp/gateway/ExternalRequestNotSuspendedTest.java
create mode 100644 jetty-rhttp/jetty-rhttp-gateway/src/test/java/org/eclipse/jetty/rhttp/gateway/ExternalTimeoutTest.java
create mode 100644 jetty-rhttp/jetty-rhttp-gateway/src/test/java/org/eclipse/jetty/rhttp/gateway/GatewayEchoServer.java
create mode 100644 jetty-rhttp/jetty-rhttp-gateway/src/test/java/org/eclipse/jetty/rhttp/gateway/GatewayEchoTest.java
create mode 100644 jetty-rhttp/jetty-rhttp-gateway/src/test/java/org/eclipse/jetty/rhttp/gateway/GatewayLoadTest.java
create mode 100644 jetty-rhttp/jetty-rhttp-gateway/src/test/java/org/eclipse/jetty/rhttp/gateway/GatewayTimeoutTest.java
create mode 100644 jetty-rhttp/jetty-rhttp-gateway/src/test/java/org/eclipse/jetty/rhttp/gateway/HandshakeClientTest.java
create mode 100644 jetty-rhttp/jetty-rhttp-gateway/src/test/java/org/eclipse/jetty/rhttp/gateway/HostTargetIdRetrieverTest.java
create mode 100644 jetty-rhttp/jetty-rhttp-gateway/src/test/resources/log4j.properties
create mode 100644 jetty-rhttp/jetty-rhttp-loadtest/pom.xml
create mode 100644 jetty-rhttp/jetty-rhttp-loadtest/src/main/java/org/eclipse/jetty/rhttp/loadtest/Loader.java
create mode 100644 jetty-rhttp/jetty-rhttp-loadtest/src/main/java/org/eclipse/jetty/rhttp/loadtest/Server.java
create mode 100644 jetty-rhttp/jetty-rhttp-loadtest/src/main/resources/log4j.properties
create mode 100644 jetty-rhttp/pom.xml
diff --git a/jetty-rhttp/README.txt b/jetty-rhttp/README.txt
new file mode 100644
index 00000000000..46ca4e743e4
--- /dev/null
+++ b/jetty-rhttp/README.txt
@@ -0,0 +1,33 @@
+Reverse HTTP
+
+The HTTP server paradigm is a valuable abstraction for browsing and accessing data and applications in a RESTful fashion from thin clients or
+other applications. However, when it comes to mobile devices, the server paradigm is often not available because those devices exist on
+restricted networks that do not allow inbound connections. These devices (eg. phones, tablets, industrial controllers, etc.) often have
+signficant content (eg. photos, video, music, contacts, etc.) and services (eg. GPS, phone, modem, camera, sound) that are worthwile to access
+remotely and often the HTTP server model is very applicable.
+
+The Jetty reverse HTTP module provides a gateway that efficiently allows HTTP connectivety to servers running in outbound-only networks. There are two key components:
+
+The reverse HTTP connector is a jetty connector (like the HTTP, SSL, AJP connectors) that accepts HTTP requests for the Jetty server instance. However, the reverse HTTP connector does not accept inbound TCP/IP connections. Instead it makes an outbound HTTP connection to the reverse HTTP gateway and uses a long polling mechanism to efficiently and asynchronously fetch requests and send responses.
+
+The reverse HTTP gateway is a jetty server that accepts inbound connections from one or more Reverse HTTP connectors and makes them available as normal HTTP targets.
+
+To demonstrate this from a source release, first run a gateway instance:
+
+ cd jetty-reverse-http/reverse-http-gateway
+ mvn exec:java
+
+In another window, you can run 3 test servers with reverse connectors with:
+
+ cd jetty-reverse-http/reverse-http-connector
+ mvn exec:java
+
+
+The three servers are using context path ID's at the gateway (virtual host and cookie based mappings can also be done), so you can access the
+three servers via the gateway at:
+
+ http://localhost:8080/gw/A
+ http://localhost:8080/gw/B
+ http://localhost:8080/gw/C
+
+
diff --git a/jetty-rhttp/jetty-rhttp-client/pom.xml b/jetty-rhttp/jetty-rhttp-client/pom.xml
new file mode 100644
index 00000000000..8fcf7253b2d
--- /dev/null
+++ b/jetty-rhttp/jetty-rhttp-client/pom.xml
@@ -0,0 +1,56 @@
+
+
+
+ org.eclipse.jetty.rhttp
+ jetty-rhttp-project
+ 9.0.0-SNAPSHOT
+
+
+ 4.0.0
+ reverse-http-client
+ jar
+ Jetty :: Reverse HTTP :: Client
+
+
+
+ org.eclipse.jetty
+ jetty-util
+
+
+ org.eclipse.jetty
+ jetty-io
+
+
+ org.eclipse.jetty
+ jetty-client
+ ${project.version}
+
+
+ org.eclipse.jetty
+ jetty-http
+ ${project.version}
+
+
+ org.apache.httpcomponents
+ httpclient
+ 4.0
+
+
+ net.jcip
+ jcip-annotations
+ 1.0
+ provided
+
+
+ junit
+ junit
+
+
+ org.eclipse.jetty
+ jetty-server
+ ${project.version}
+ test
+
+
+
+
diff --git a/jetty-rhttp/jetty-rhttp-client/src/main/java/org/eclipse/jetty/rhttp/client/AbstractClient.java b/jetty-rhttp/jetty-rhttp-client/src/main/java/org/eclipse/jetty/rhttp/client/AbstractClient.java
new file mode 100644
index 00000000000..afdf0a5e860
--- /dev/null
+++ b/jetty-rhttp/jetty-rhttp-client/src/main/java/org/eclipse/jetty/rhttp/client/AbstractClient.java
@@ -0,0 +1,270 @@
+//
+// ========================================================================
+// Copyright (c) 1995-2012 Mort Bay Consulting Pty. Ltd.
+// ------------------------------------------------------------------------
+// All rights reserved. This program and the accompanying materials
+// are made available under the terms of the Eclipse Public License v1.0
+// and Apache License v2.0 which accompanies this distribution.
+//
+// The Eclipse Public License is available at
+// http://www.eclipse.org/legal/epl-v10.html
+//
+// The Apache License v2.0 is available at
+// http://www.opensource.org/licenses/apache2.0.php
+//
+// You may elect to redistribute this code under either of these licenses.
+// ========================================================================
+//
+
+package org.eclipse.jetty.rhttp.client;
+
+import java.io.IOException;
+import java.io.UnsupportedEncodingException;
+import java.net.URLEncoder;
+import java.util.HashMap;
+import java.util.List;
+import java.util.Map;
+import java.util.concurrent.CopyOnWriteArrayList;
+
+import org.eclipse.jetty.util.component.AbstractLifeCycle;
+import org.eclipse.jetty.util.log.Log;
+import org.eclipse.jetty.util.log.Logger;
+
+/**
+ * @version $Revision$ $Date$
+ */
+public abstract class AbstractClient extends AbstractLifeCycle implements RHTTPClient
+{
+ private final Logger logger = Log.getLogger("org.mortbay.jetty.rhttp.client");
+ private final List listeners = new CopyOnWriteArrayList();
+ private final List clientListeners = new CopyOnWriteArrayList();
+ private final String targetId;
+ private volatile Status status = Status.DISCONNECTED;
+
+ public AbstractClient(String targetId)
+ {
+ this.targetId = targetId;
+ }
+
+ public String getGatewayURI()
+ {
+ return "http://"+getHost()+":"+getPort()+getPath();
+ }
+
+ public String getTargetId()
+ {
+ return targetId;
+ }
+
+ public Logger getLogger()
+ {
+ return logger;
+ }
+
+ public void addListener(RHTTPListener listener)
+ {
+ listeners.add(listener);
+ }
+
+ public void removeListener(RHTTPListener listener)
+ {
+ listeners.remove(listener);
+ }
+
+ public void addClientListener(ClientListener listener)
+ {
+ clientListeners.add(listener);
+ }
+
+ public void removeClientListener(ClientListener listener)
+ {
+ clientListeners.remove(listener);
+ }
+
+ protected void notifyRequests(List requests)
+ {
+ for (RHTTPRequest request : requests)
+ {
+ for (RHTTPListener listener : listeners)
+ {
+ try
+ {
+ listener.onRequest(request);
+ }
+ catch (Throwable x)
+ {
+ logger.warn("Listener " + listener + " threw", x);
+ try
+ {
+ deliver(newExceptionResponse(request.getId(), x));
+ }
+ catch (IOException xx)
+ {
+ logger.debug("Could not deliver exception response", xx);
+ }
+ }
+ }
+ }
+ }
+
+ protected RHTTPResponse newExceptionResponse(int requestId, Throwable x)
+ {
+ try
+ {
+ int statusCode = 500;
+ String statusMessage = "Internal Server Error";
+ Map headers = new HashMap();
+ byte[] body = x.toString().getBytes("UTF-8");
+ return new RHTTPResponse(requestId, statusCode, statusMessage, headers, body);
+ }
+ catch (UnsupportedEncodingException xx)
+ {
+ throw new AssertionError(xx);
+ }
+ }
+
+ protected void notifyConnectRequired()
+ {
+ for (ClientListener listener : clientListeners)
+ {
+ try
+ {
+ listener.connectRequired();
+ }
+ catch (Throwable x)
+ {
+ logger.warn("ClientListener " + listener + " threw", x);
+ }
+ }
+ }
+
+ protected void notifyConnectException()
+ {
+ for (ClientListener listener : clientListeners)
+ {
+ try
+ {
+ listener.connectException();
+ }
+ catch (Throwable x)
+ {
+ logger.warn("ClientListener " + listener + " threw", x);
+ }
+ }
+ }
+
+ protected void notifyConnectClosed()
+ {
+ for (ClientListener listener : clientListeners)
+ {
+ try
+ {
+ listener.connectClosed();
+ }
+ catch (Throwable xx)
+ {
+ logger.warn("ClientListener " + listener + " threw", xx);
+ }
+ }
+ }
+
+ protected void notifyDeliverException(RHTTPResponse response)
+ {
+ for (ClientListener listener : clientListeners)
+ {
+ try
+ {
+ listener.deliverException(response);
+ }
+ catch (Throwable x)
+ {
+ logger.warn("ClientListener " + listener + " threw", x);
+ }
+ }
+ }
+
+ protected String urlEncode(String value)
+ {
+ try
+ {
+ return URLEncoder.encode(value, "UTF-8");
+ }
+ catch (UnsupportedEncodingException x)
+ {
+ getLogger().debug("", x);
+ return null;
+ }
+ }
+
+ protected boolean isConnected()
+ {
+ return status == Status.CONNECTED;
+ }
+
+ protected boolean isDisconnecting()
+ {
+ return status == Status.DISCONNECTING;
+ }
+
+ protected boolean isDisconnected()
+ {
+ return status == Status.DISCONNECTED;
+ }
+
+ public void connect() throws IOException
+ {
+ if (isDisconnected())
+ status = Status.CONNECTING;
+
+ syncHandshake();
+ this.status = Status.CONNECTED;
+
+ asyncConnect();
+ }
+
+ public void disconnect() throws IOException
+ {
+ if (isConnected())
+ {
+ status = Status.DISCONNECTING;
+ try
+ {
+ syncDisconnect();
+ }
+ finally
+ {
+ status = Status.DISCONNECTED;
+ }
+ }
+ }
+
+ public void deliver(RHTTPResponse response) throws IOException
+ {
+ asyncDeliver(response);
+ }
+
+ protected abstract void syncHandshake() throws IOException;
+
+ protected abstract void asyncConnect();
+
+ protected abstract void syncDisconnect() throws IOException;
+
+ protected abstract void asyncDeliver(RHTTPResponse response);
+
+ protected void connectComplete(byte[] responseContent) throws IOException
+ {
+ List requests = RHTTPRequest.fromFrameBytes(responseContent);
+ getLogger().debug("Client {} connect returned from gateway, requests {}", getTargetId(), requests);
+
+ // Requests are arrived, reconnect while we process them
+ if (!isDisconnecting() && !isDisconnected())
+ asyncConnect();
+
+ notifyRequests(requests);
+ }
+
+ protected enum Status
+ {
+ CONNECTING, CONNECTED, DISCONNECTING, DISCONNECTED
+ }
+}
diff --git a/jetty-rhttp/jetty-rhttp-client/src/main/java/org/eclipse/jetty/rhttp/client/ApacheClient.java b/jetty-rhttp/jetty-rhttp-client/src/main/java/org/eclipse/jetty/rhttp/client/ApacheClient.java
new file mode 100644
index 00000000000..38723666a8a
--- /dev/null
+++ b/jetty-rhttp/jetty-rhttp-client/src/main/java/org/eclipse/jetty/rhttp/client/ApacheClient.java
@@ -0,0 +1,156 @@
+//
+// ========================================================================
+// Copyright (c) 1995-2012 Mort Bay Consulting Pty. Ltd.
+// ------------------------------------------------------------------------
+// All rights reserved. This program and the accompanying materials
+// are made available under the terms of the Eclipse Public License v1.0
+// and Apache License v2.0 which accompanies this distribution.
+//
+// The Eclipse Public License is available at
+// http://www.eclipse.org/legal/epl-v10.html
+//
+// The Apache License v2.0 is available at
+// http://www.opensource.org/licenses/apache2.0.php
+//
+// You may elect to redistribute this code under either of these licenses.
+// ========================================================================
+//
+
+package org.eclipse.jetty.rhttp.client;
+
+import java.io.IOException;
+
+import org.apache.http.HttpEntity;
+import org.apache.http.HttpHost;
+import org.apache.http.HttpResponse;
+import org.apache.http.HttpStatus;
+import org.apache.http.NoHttpResponseException;
+import org.apache.http.client.HttpClient;
+import org.apache.http.client.methods.HttpPost;
+import org.apache.http.entity.ByteArrayEntity;
+import org.apache.http.util.EntityUtils;
+
+/**
+ * Implementation of {@link RHTTPClient} that uses Apache's HttpClient.
+ *
+ * @version $Revision$ $Date$
+ */
+public class ApacheClient extends AbstractClient
+{
+ private final HttpClient httpClient;
+ private final String gatewayPath;
+
+ public ApacheClient(HttpClient httpClient, String gatewayPath, String targetId)
+ {
+ super(targetId);
+ this.httpClient = httpClient;
+ this.gatewayPath = gatewayPath;
+ }
+
+ public String getHost()
+ {
+ return ((HttpHost)httpClient.getParams().getParameter("http.default-host")).getHostName();
+ }
+
+ public int getPort()
+ {
+ return ((HttpHost)httpClient.getParams().getParameter("http.default-host")).getPort();
+ }
+
+ public String getPath()
+ {
+ return gatewayPath;
+ }
+
+ protected void syncHandshake() throws IOException
+ {
+ HttpPost handshake = new HttpPost(gatewayPath + "/" + urlEncode(getTargetId()) + "/handshake");
+ HttpResponse response = httpClient.execute(handshake);
+ int statusCode = response.getStatusLine().getStatusCode();
+ HttpEntity entity = response.getEntity();
+ if (entity != null)
+ entity.consumeContent();
+ if (statusCode != HttpStatus.SC_OK)
+ throw new IOException("Handshake failed");
+ getLogger().debug("Client {} handshake returned from gateway", getTargetId(), null);
+ }
+
+ protected void asyncConnect()
+ {
+ new Thread()
+ {
+ @Override
+ public void run()
+ {
+ try
+ {
+ HttpPost connect = new HttpPost(gatewayPath + "/" + urlEncode(getTargetId()) + "/connect");
+ getLogger().debug("Client {} connect sent to gateway", getTargetId(), null);
+ HttpResponse response = httpClient.execute(connect);
+ int statusCode = response.getStatusLine().getStatusCode();
+ HttpEntity entity = response.getEntity();
+ byte[] responseContent = EntityUtils.toByteArray(entity);
+ if (statusCode == HttpStatus.SC_OK)
+ connectComplete(responseContent);
+ else if (statusCode == HttpStatus.SC_UNAUTHORIZED)
+ notifyConnectRequired();
+ else
+ notifyConnectException();
+ }
+ catch (NoHttpResponseException x)
+ {
+ notifyConnectClosed();
+ }
+ catch (IOException x)
+ {
+ getLogger().debug("", x);
+ notifyConnectException();
+ }
+ }
+ }.start();
+ }
+
+ protected void syncDisconnect() throws IOException
+ {
+ HttpPost disconnect = new HttpPost(gatewayPath + "/" + urlEncode(getTargetId()) + "/disconnect");
+ HttpResponse response = httpClient.execute(disconnect);
+ int statusCode = response.getStatusLine().getStatusCode();
+ HttpEntity entity = response.getEntity();
+ if (entity != null)
+ entity.consumeContent();
+ if (statusCode != HttpStatus.SC_OK)
+ throw new IOException("Disconnect failed");
+ getLogger().debug("Client {} disconnect returned from gateway", getTargetId(), null);
+ }
+
+ protected void asyncDeliver(final RHTTPResponse response)
+ {
+ new Thread()
+ {
+ @Override
+ public void run()
+ {
+ try
+ {
+ HttpPost deliver = new HttpPost(gatewayPath + "/" + urlEncode(getTargetId()) + "/deliver");
+ deliver.setEntity(new ByteArrayEntity(response.getFrameBytes()));
+ getLogger().debug("Client {} deliver sent to gateway, response {}", getTargetId(), response);
+ HttpResponse httpResponse = httpClient.execute(deliver);
+ int statusCode = httpResponse.getStatusLine().getStatusCode();
+ HttpEntity entity = httpResponse.getEntity();
+ if (entity != null)
+ entity.consumeContent();
+ if (statusCode == HttpStatus.SC_UNAUTHORIZED)
+ notifyConnectRequired();
+ else if (statusCode != HttpStatus.SC_OK)
+ notifyDeliverException(response);
+ }
+ catch (IOException x)
+ {
+ getLogger().debug("", x);
+ notifyDeliverException(response);
+ }
+ }
+ }.start();
+ }
+}
diff --git a/jetty-rhttp/jetty-rhttp-client/src/main/java/org/eclipse/jetty/rhttp/client/ClientListener.java b/jetty-rhttp/jetty-rhttp-client/src/main/java/org/eclipse/jetty/rhttp/client/ClientListener.java
new file mode 100644
index 00000000000..7af843fffb1
--- /dev/null
+++ b/jetty-rhttp/jetty-rhttp-client/src/main/java/org/eclipse/jetty/rhttp/client/ClientListener.java
@@ -0,0 +1,67 @@
+//
+// ========================================================================
+// Copyright (c) 1995-2012 Mort Bay Consulting Pty. Ltd.
+// ------------------------------------------------------------------------
+// All rights reserved. This program and the accompanying materials
+// are made available under the terms of the Eclipse Public License v1.0
+// and Apache License v2.0 which accompanies this distribution.
+//
+// The Eclipse Public License is available at
+// http://www.eclipse.org/legal/epl-v10.html
+//
+// The Apache License v2.0 is available at
+// http://www.opensource.org/licenses/apache2.0.php
+//
+// You may elect to redistribute this code under either of these licenses.
+// ========================================================================
+//
+
+package org.eclipse.jetty.rhttp.client;
+
+/**
+ * A listener for network-related events happening on the gateway client.
+ *
+ * @version $Revision$ $Date$
+ */
+public interface ClientListener
+{
+ /**
+ * Called when the client detects that the server requested a new connect.
+ */
+ public void connectRequired();
+
+ /**
+ * Called when the client detects that the connection has been closed by the server.
+ */
+ public void connectClosed();
+
+ /**
+ * Called when the client detects a generic exception while trying to connect to the server.
+ */
+ public void connectException();
+
+ /**
+ * Called when the client detects a generic exception while tryint to deliver to the server.
+ * @param response the Response object that should have been sent to the server
+ */
+ public void deliverException(RHTTPResponse response);
+
+ public static class Adapter implements ClientListener
+ {
+ public void connectRequired()
+ {
+ }
+
+ public void connectClosed()
+ {
+ }
+
+ public void connectException()
+ {
+ }
+
+ public void deliverException(RHTTPResponse response)
+ {
+ }
+ }
+}
diff --git a/jetty-rhttp/jetty-rhttp-client/src/main/java/org/eclipse/jetty/rhttp/client/JettyClient.java b/jetty-rhttp/jetty-rhttp-client/src/main/java/org/eclipse/jetty/rhttp/client/JettyClient.java
new file mode 100644
index 00000000000..26d7e519141
--- /dev/null
+++ b/jetty-rhttp/jetty-rhttp-client/src/main/java/org/eclipse/jetty/rhttp/client/JettyClient.java
@@ -0,0 +1,306 @@
+//
+// ========================================================================
+// Copyright (c) 1995-2012 Mort Bay Consulting Pty. Ltd.
+// ------------------------------------------------------------------------
+// All rights reserved. This program and the accompanying materials
+// are made available under the terms of the Eclipse Public License v1.0
+// and Apache License v2.0 which accompanies this distribution.
+//
+// The Eclipse Public License is available at
+// http://www.eclipse.org/legal/epl-v10.html
+//
+// The Apache License v2.0 is available at
+// http://www.opensource.org/licenses/apache2.0.php
+//
+// You may elect to redistribute this code under either of these licenses.
+// ========================================================================
+//
+
+package org.eclipse.jetty.rhttp.client;
+
+import java.io.ByteArrayOutputStream;
+import java.io.EOFException;
+import java.io.IOException;
+
+import org.eclipse.jetty.client.Address;
+import org.eclipse.jetty.client.ContentExchange;
+import org.eclipse.jetty.client.HttpClient;
+import org.eclipse.jetty.client.HttpExchange;
+import org.eclipse.jetty.http.HttpMethods;
+import org.eclipse.jetty.http.HttpURI;
+import org.eclipse.jetty.io.Buffer;
+import org.eclipse.jetty.io.ByteArrayBuffer;
+import org.eclipse.jetty.io.EofException;
+
+/**
+ * Implementation of {@link RHTTPClient} that uses Jetty's HttpClient.
+ *
+ * @version $Revision$ $Date$
+ */
+public class JettyClient extends AbstractClient
+{
+ private final HttpClient httpClient;
+ private final Address gatewayAddress;
+ private final String gatewayPath;
+
+ public JettyClient(HttpClient httpClient, Address gatewayAddress, String gatewayPath, String targetId)
+ {
+ super(targetId);
+ this.httpClient = httpClient;
+ this.gatewayAddress = gatewayAddress;
+ this.gatewayPath = gatewayPath;
+ }
+
+ public JettyClient(HttpClient httpClient, String gatewayURI, String targetId)
+ {
+ super(targetId);
+
+ HttpURI uri = new HttpURI(gatewayURI);
+
+ this.httpClient = httpClient;
+ this.gatewayAddress = new Address(uri.getHost(),uri.getPort());
+ this.gatewayPath = uri.getPath();
+ }
+
+ public String getHost()
+ {
+ return gatewayAddress.getHost();
+ }
+
+ public int getPort()
+ {
+ return gatewayAddress.getPort();
+ }
+
+ public String getPath()
+ {
+ return gatewayPath;
+ }
+
+ @Override
+ protected void doStart() throws Exception
+ {
+ httpClient.start();
+ super.doStart();
+ }
+
+ @Override
+ protected void doStop() throws Exception
+ {
+ super.doStop();
+ httpClient.stop();
+ }
+
+ protected void syncHandshake() throws IOException
+ {
+ HandshakeExchange exchange = new HandshakeExchange();
+ exchange.setMethod(HttpMethods.POST);
+ exchange.setAddress(gatewayAddress);
+ exchange.setURI(gatewayPath + "/" + urlEncode(getTargetId()) + "/handshake");
+ httpClient.send(exchange);
+ getLogger().debug("Client {} handshake sent to gateway", getTargetId(), null);
+
+ try
+ {
+ int exchangeStatus = exchange.waitForDone();
+ if (exchangeStatus != HttpExchange.STATUS_COMPLETED)
+ throw new IOException("Handshake failed");
+ if (exchange.getResponseStatus() != 200)
+ throw new IOException("Handshake failed");
+ getLogger().debug("Client {} handshake returned from gateway", getTargetId(), null);
+ }
+ catch (InterruptedException x)
+ {
+ Thread.currentThread().interrupt();
+ throw newIOException(x);
+ }
+ }
+
+ private IOException newIOException(Throwable x)
+ {
+ return (IOException)new IOException().initCause(x);
+ }
+
+ protected void asyncConnect()
+ {
+ try
+ {
+ ConnectExchange exchange = new ConnectExchange();
+ exchange.setMethod(HttpMethods.POST);
+ exchange.setAddress(gatewayAddress);
+ exchange.setURI(gatewayPath + "/" + urlEncode(getTargetId()) + "/connect");
+ httpClient.send(exchange);
+ getLogger().debug("Client {} connect sent to gateway", getTargetId(), null);
+ }
+ catch (IOException x)
+ {
+ getLogger().debug("Could not send exchange", x);
+ throw new RuntimeException(x);
+ }
+ }
+
+ protected void syncDisconnect() throws IOException
+ {
+ DisconnectExchange exchange = new DisconnectExchange();
+ exchange.setMethod(HttpMethods.POST);
+ exchange.setAddress(gatewayAddress);
+ exchange.setURI(gatewayPath + "/" + urlEncode(getTargetId()) + "/disconnect");
+ httpClient.send(exchange);
+ getLogger().debug("Client {} disconnect sent to gateway", getTargetId(), null);
+ try
+ {
+ int status = exchange.waitForDone();
+ if (status != HttpExchange.STATUS_COMPLETED)
+ throw new IOException("Disconnect failed");
+ if (exchange.getResponseStatus() != 200)
+ throw new IOException("Disconnect failed");
+ getLogger().debug("Client {} disconnect returned from gateway", getTargetId(), null);
+ }
+ catch (InterruptedException x)
+ {
+ Thread.currentThread().interrupt();
+ throw newIOException(x);
+ }
+ }
+
+ protected void asyncDeliver(RHTTPResponse response)
+ {
+ try
+ {
+ DeliverExchange exchange = new DeliverExchange(response);
+ exchange.setMethod(HttpMethods.POST);
+ exchange.setAddress(gatewayAddress);
+ exchange.setURI(gatewayPath + "/" + urlEncode(getTargetId()) + "/deliver");
+ exchange.setRequestContent(new ByteArrayBuffer(response.getFrameBytes()));
+ httpClient.send(exchange);
+ getLogger().debug("Client {} deliver sent to gateway, response {}", getTargetId(), response);
+ }
+ catch (IOException x)
+ {
+ getLogger().debug("Could not send exchange", x);
+ throw new RuntimeException(x);
+ }
+ }
+
+ protected class HandshakeExchange extends ContentExchange
+ {
+ protected HandshakeExchange()
+ {
+ super(true);
+ }
+
+ @Override
+ protected void onConnectionFailed(Throwable x)
+ {
+ getLogger().warn(x.toString());
+ getLogger().debug(x);
+ }
+ }
+
+ protected class ConnectExchange extends ContentExchange
+ {
+ private final ByteArrayOutputStream content = new ByteArrayOutputStream();
+
+ protected ConnectExchange()
+ {
+ super(true);
+ }
+
+ @Override
+ protected void onResponseContent(Buffer buffer) throws IOException
+ {
+ buffer.writeTo(content);
+ }
+
+ @Override
+ protected void onResponseComplete()
+ {
+ int responseStatus = getResponseStatus();
+ if (responseStatus == 200)
+ {
+ try
+ {
+ connectComplete(content.toByteArray());
+ }
+ catch (IOException x)
+ {
+ onException(x);
+ }
+ }
+ else if (responseStatus == 401)
+ {
+ notifyConnectRequired();
+ }
+ else
+ {
+ notifyConnectException();
+ }
+ }
+
+ @Override
+ protected void onException(Throwable x)
+ {
+ getLogger().debug(x);
+ if (x instanceof EofException || x instanceof EOFException)
+ {
+ notifyConnectClosed();
+ }
+ else
+ {
+ notifyConnectException();
+ }
+ }
+
+ @Override
+ protected void onConnectionFailed(Throwable x)
+ {
+ getLogger().debug(x);
+ }
+ }
+
+ protected class DisconnectExchange extends ContentExchange
+ {
+ protected DisconnectExchange()
+ {
+ super(true);
+ }
+ }
+
+ protected class DeliverExchange extends ContentExchange
+ {
+ private final RHTTPResponse response;
+
+ protected DeliverExchange(RHTTPResponse response)
+ {
+ super(true);
+ this.response = response;
+ }
+
+ @Override
+ protected void onResponseComplete() throws IOException
+ {
+ int responseStatus = getResponseStatus();
+ if (responseStatus == 401)
+ {
+ notifyConnectRequired();
+ }
+ else if (responseStatus != 200)
+ {
+ notifyDeliverException(response);
+ }
+ }
+
+ @Override
+ protected void onException(Throwable x)
+ {
+ getLogger().debug(x);
+ notifyDeliverException(response);
+ }
+
+ @Override
+ protected void onConnectionFailed(Throwable x)
+ {
+ getLogger().debug(x);
+ }
+ }
+}
diff --git a/jetty-rhttp/jetty-rhttp-client/src/main/java/org/eclipse/jetty/rhttp/client/RHTTPClient.java b/jetty-rhttp/jetty-rhttp-client/src/main/java/org/eclipse/jetty/rhttp/client/RHTTPClient.java
new file mode 100644
index 00000000000..bdaa1303558
--- /dev/null
+++ b/jetty-rhttp/jetty-rhttp-client/src/main/java/org/eclipse/jetty/rhttp/client/RHTTPClient.java
@@ -0,0 +1,133 @@
+//
+// ========================================================================
+// Copyright (c) 1995-2012 Mort Bay Consulting Pty. Ltd.
+// ------------------------------------------------------------------------
+// All rights reserved. This program and the accompanying materials
+// are made available under the terms of the Eclipse Public License v1.0
+// and Apache License v2.0 which accompanies this distribution.
+//
+// The Eclipse Public License is available at
+// http://www.eclipse.org/legal/epl-v10.html
+//
+// The Apache License v2.0 is available at
+// http://www.opensource.org/licenses/apache2.0.php
+//
+// You may elect to redistribute this code under either of these licenses.
+// ========================================================================
+//
+
+package org.eclipse.jetty.rhttp.client;
+
+import java.io.IOException;
+
+/**
+ * RHTTPClient represent a client of the gateway server.
+ * A Client has a server side counterpart with which communicates
+ * using a comet protocol.
The Client, its server-side
+ * counterpart and the comet protocol form the Half-Object plus Protocol
+ * pattern.
+ * A Client must first connect to the gateway server, to let the gateway
+ * server know its targetId, an identifier that uniquely distinguish this
+ * Client from other Clients.
+ * Once connected, the gateway server will use a comet procotol to notify the
+ * Client of server-side events, and the Client can send
+ * information to the gateway server to notify it of client-side events.
+ * Server-side event are notified to {@link RHTTPListener}s, while relevant
+ * network events are communicated to {@link ClientListener}s.
+ *
+ * @version $Revision$ $Date$
+ */
+public interface RHTTPClient
+{
+ /**
+ * @return The gateway uri, typically "http://gatewayhost:gatewayport/gatewaypath".
+ */
+ public String getGatewayURI();
+
+ /**
+ * @return The gateway host
+ */
+ public String getHost();
+
+ /**
+ * @return The gateway port
+ */
+ public int getPort();
+
+ /**
+ * @return The gateway path
+ */
+ public String getPath();
+
+ /**
+ * @return the targetId that uniquely identifies this client.
+ */
+ public String getTargetId();
+
+ /**
+ * Connects to the gateway server, establishing the long poll communication
+ * with the gateway server to be notified of server-side events.
+ * The connect is performed in two steps:
+ *
+ * - first, a connect message is sent to the gateway server; the gateway server
+ * will notice this is a first connect message and reply immediately with
+ * an empty response
+ * - second, another connect message is sent to the gateway server which interprets
+ * it as a long poll request
+ *
+ * The long poll request may return either because one or more server-side events
+ * happened, or because it expired.
+ * Any connect message after the first is treated as a long poll request.
+ *
+ * @throws IOException if it is not possible to connect to the gateway server
+ * @see #disconnect()
+ */
+ public void connect() throws IOException;
+
+ /**
+ * Disconnects from the gateway server.
+ * Just after the disconnect request is processed by to the gateway server, it will
+ * return the currently outstanding long poll request.
+ * If this client is not connected, it does nothing
+ *
+ * @throws IOException if it is not possible to contact the gateway server to disconnect
+ * @see #connect()
+ */
+ public void disconnect() throws IOException;
+
+ /**
+ * Sends a response to the gateway server.
+ *
+ * @param response the response to send
+ * @throws IOException if it is not possible to contact the gateway server
+ */
+ public void deliver(RHTTPResponse response) throws IOException;
+
+ /**
+ * Adds the given listener to this client.
+ * @param listener the listener to add
+ * @see #removeListener(RHTTPListener)
+ */
+ public void addListener(RHTTPListener listener);
+
+ /**
+ * Removes the given listener from this client.
+ * @param listener the listener to remove
+ * @see #addListener(RHTTPListener)
+ */
+ public void removeListener(RHTTPListener listener);
+
+ /**
+ * Adds the given client listener to this client.
+ * @param listener the client listener to add
+ * @see #removeClientListener(ClientListener)
+ */
+ public void addClientListener(ClientListener listener);
+
+ /**
+ * Removes the given client listener from this client.
+ * @param listener the client listener to remove
+ * @see #addClientListener(ClientListener)
+ */
+ public void removeClientListener(ClientListener listener);
+}
diff --git a/jetty-rhttp/jetty-rhttp-client/src/main/java/org/eclipse/jetty/rhttp/client/RHTTPListener.java b/jetty-rhttp/jetty-rhttp-client/src/main/java/org/eclipse/jetty/rhttp/client/RHTTPListener.java
new file mode 100644
index 00000000000..3e679f8adca
--- /dev/null
+++ b/jetty-rhttp/jetty-rhttp-client/src/main/java/org/eclipse/jetty/rhttp/client/RHTTPListener.java
@@ -0,0 +1,36 @@
+//
+// ========================================================================
+// Copyright (c) 1995-2012 Mort Bay Consulting Pty. Ltd.
+// ------------------------------------------------------------------------
+// All rights reserved. This program and the accompanying materials
+// are made available under the terms of the Eclipse Public License v1.0
+// and Apache License v2.0 which accompanies this distribution.
+//
+// The Eclipse Public License is available at
+// http://www.eclipse.org/legal/epl-v10.html
+//
+// The Apache License v2.0 is available at
+// http://www.opensource.org/licenses/apache2.0.php
+//
+// You may elect to redistribute this code under either of these licenses.
+// ========================================================================
+//
+
+package org.eclipse.jetty.rhttp.client;
+
+/**
+ * Implementations of this class listen for requests arriving from the gateway server
+ * and notified by {@link RHTTPClient}.
+ *
+ * @version $Revision$ $Date$
+ */
+public interface RHTTPListener
+{
+ /**
+ * Callback method called by {@link RHTTPClient} to inform that the gateway server
+ * sent a request to the gateway client.
+ * @param request the request sent by the gateway server.
+ * @throws Exception allowed to be thrown by implementations
+ */
+ public void onRequest(RHTTPRequest request) throws Exception;
+}
diff --git a/jetty-rhttp/jetty-rhttp-client/src/main/java/org/eclipse/jetty/rhttp/client/RHTTPRequest.java b/jetty-rhttp/jetty-rhttp-client/src/main/java/org/eclipse/jetty/rhttp/client/RHTTPRequest.java
new file mode 100644
index 00000000000..0daf4bccca3
--- /dev/null
+++ b/jetty-rhttp/jetty-rhttp-client/src/main/java/org/eclipse/jetty/rhttp/client/RHTTPRequest.java
@@ -0,0 +1,266 @@
+//
+// ========================================================================
+// Copyright (c) 1995-2012 Mort Bay Consulting Pty. Ltd.
+// ------------------------------------------------------------------------
+// All rights reserved. This program and the accompanying materials
+// are made available under the terms of the Eclipse Public License v1.0
+// and Apache License v2.0 which accompanies this distribution.
+//
+// The Eclipse Public License is available at
+// http://www.eclipse.org/legal/epl-v10.html
+//
+// The Apache License v2.0 is available at
+// http://www.opensource.org/licenses/apache2.0.php
+//
+// You may elect to redistribute this code under either of these licenses.
+// ========================================================================
+//
+
+package org.eclipse.jetty.rhttp.client;
+
+import java.io.ByteArrayOutputStream;
+import java.io.IOException;
+import java.util.ArrayList;
+import java.util.LinkedHashMap;
+import java.util.List;
+import java.util.Map;
+
+import org.eclipse.jetty.http.HttpParser;
+import org.eclipse.jetty.io.Buffer;
+import org.eclipse.jetty.io.ByteArrayBuffer;
+
+/**
+ * Represents the external request information that is carried over the comet protocol.
+ * Instances of this class are converted into an opaque byte array of the form:
+ *
+ * <request-id> SPACE <request-length> CRLF
+ * <external-request>
+ *
+ * The byte array form is carried as body of a normal HTTP response returned by the gateway server
+ * to the gateway client.
+ * @see RHTTPResponse
+ * @version $Revision$ $Date$
+ */
+public class RHTTPRequest
+{
+ private static final String CRLF = "\r\n";
+ private static final byte[] CRLF_BYTES = CRLF.getBytes();
+
+ private final int id;
+ private final byte[] requestBytes;
+ private final byte[] frameBytes;
+ private volatile String method;
+ private volatile String uri;
+ private volatile Map headers;
+ private volatile byte[] body;
+
+ public static List fromFrameBytes(byte[] bytes)
+ {
+ List result = new ArrayList();
+ int start = 0;
+ while (start < bytes.length)
+ {
+ // Scan until we find the space
+ int end = start;
+ while (bytes[end] != ' ') ++end;
+ int requestId = Integer.parseInt(new String(bytes, start, end - start));
+ start = end + 1;
+
+ // Scan until end of line
+ while (bytes[end] != '\n') ++end;
+ int length = Integer.parseInt(new String(bytes, start, end - start - 1));
+ start = end + 1;
+
+ byte[] requestBytes = new byte[length];
+ System.arraycopy(bytes, start, requestBytes, 0, length);
+ RHTTPRequest request = fromRequestBytes(requestId, requestBytes);
+ result.add(request);
+ start += length;
+ }
+ return result;
+ }
+
+ public static RHTTPRequest fromRequestBytes(int requestId, byte[] requestBytes)
+ {
+ return new RHTTPRequest(requestId, requestBytes);
+ }
+
+ public RHTTPRequest(int id, String method, String uri, Map headers, byte[] body)
+ {
+ this.id = id;
+ this.method = method;
+ this.uri = uri;
+ this.headers = headers;
+ this.body = body;
+ this.requestBytes = toRequestBytes();
+ this.frameBytes = toFrameBytes(requestBytes);
+ }
+
+ private RHTTPRequest(int id, byte[] requestBytes)
+ {
+ this.id = id;
+ this.requestBytes = requestBytes;
+ this.frameBytes = toFrameBytes(requestBytes);
+ // Other fields are lazily initialized
+ }
+
+ private void initialize()
+ {
+ try
+ {
+ final ByteArrayOutputStream body = new ByteArrayOutputStream();
+ HttpParser parser = new HttpParser(new ByteArrayBuffer(requestBytes), new HttpParser.EventHandler()
+ {
+ @Override
+ public void startRequest(Buffer method, Buffer uri, Buffer httpVersion) throws IOException
+ {
+ RHTTPRequest.this.method = method.toString("UTF-8");
+ RHTTPRequest.this.uri = uri.toString("UTF-8");
+ RHTTPRequest.this.headers = new LinkedHashMap();
+ }
+
+ @Override
+ public void startResponse(Buffer httpVersion, int statusCode, Buffer statusMessage) throws IOException
+ {
+ }
+
+ @Override
+ public void parsedHeader(Buffer name, Buffer value) throws IOException
+ {
+ RHTTPRequest.this.headers.put(name.toString("UTF-8"), value.toString("UTF-8"));
+ }
+
+ @Override
+ public void content(Buffer content) throws IOException
+ {
+ content.writeTo(body);
+ }
+ });
+ parser.parse();
+ this.body = body.toByteArray();
+ }
+ catch (IOException x)
+ {
+ // Cannot happen: we're parsing from a byte[], not from an I/O stream
+ throw new AssertionError(x);
+ }
+ }
+
+ public int getId()
+ {
+ return id;
+ }
+
+ public byte[] getRequestBytes()
+ {
+ return requestBytes;
+ }
+
+ public byte[] getFrameBytes()
+ {
+ return frameBytes;
+ }
+
+ public String getMethod()
+ {
+ if (method == null)
+ initialize();
+ return method;
+ }
+
+ public String getURI()
+ {
+ if (uri == null)
+ initialize();
+ return uri;
+ }
+
+ public Map getHeaders()
+ {
+ if (headers == null)
+ initialize();
+ return headers;
+ }
+
+ public byte[] getBody()
+ {
+ if (body == null)
+ initialize();
+ return body;
+ }
+
+ private byte[] toRequestBytes()
+ {
+ try
+ {
+ ByteArrayOutputStream bytes = new ByteArrayOutputStream();
+ bytes.write(method.getBytes("UTF-8"));
+ bytes.write(' ');
+ bytes.write(uri.getBytes("UTF-8"));
+ bytes.write(' ');
+ bytes.write("HTTP/1.1".getBytes("UTF-8"));
+ bytes.write(CRLF_BYTES);
+ for (Map.Entry entry : headers.entrySet())
+ {
+ bytes.write(entry.getKey().getBytes("UTF-8"));
+ bytes.write(':');
+ bytes.write(' ');
+ bytes.write(entry.getValue().getBytes("UTF-8"));
+ bytes.write(CRLF_BYTES);
+ }
+ bytes.write(CRLF_BYTES);
+ bytes.write(body);
+ bytes.close();
+ return bytes.toByteArray();
+ }
+ catch (IOException x)
+ {
+ throw new AssertionError(x);
+ }
+ }
+
+ private byte[] toFrameBytes(byte[] requestBytes)
+ {
+ try
+ {
+ ByteArrayOutputStream bytes = new ByteArrayOutputStream();
+ bytes.write(String.valueOf(id).getBytes("UTF-8"));
+ bytes.write(' ');
+ bytes.write(String.valueOf(requestBytes.length).getBytes("UTF-8"));
+ bytes.write(CRLF_BYTES);
+ bytes.write(requestBytes);
+ bytes.close();
+ return bytes.toByteArray();
+ }
+ catch (IOException x)
+ {
+ throw new AssertionError(x);
+ }
+ }
+
+ @Override
+ public String toString()
+ {
+ // Use fields to avoid initialization
+ StringBuilder builder = new StringBuilder();
+ builder.append(id).append(" ");
+ builder.append(method).append(" ");
+ builder.append(uri).append(" ");
+ builder.append(requestBytes.length).append("/");
+ builder.append(frameBytes.length);
+ return builder.toString();
+ }
+
+ public String toLongString()
+ {
+ // Use getters to trigger initialization
+ StringBuilder builder = new StringBuilder();
+ builder.append(id).append(" ");
+ builder.append(getMethod()).append(" ");
+ builder.append(getURI()).append(CRLF);
+ for (Map.Entry header : getHeaders().entrySet())
+ builder.append(header.getKey()).append(": ").append(header.getValue()).append(CRLF);
+ builder.append(getBody().length).append(" body bytes").append(CRLF);
+ return builder.toString();
+ }
+}
diff --git a/jetty-rhttp/jetty-rhttp-client/src/main/java/org/eclipse/jetty/rhttp/client/RHTTPResponse.java b/jetty-rhttp/jetty-rhttp-client/src/main/java/org/eclipse/jetty/rhttp/client/RHTTPResponse.java
new file mode 100644
index 00000000000..3357fabf17a
--- /dev/null
+++ b/jetty-rhttp/jetty-rhttp-client/src/main/java/org/eclipse/jetty/rhttp/client/RHTTPResponse.java
@@ -0,0 +1,256 @@
+//
+// ========================================================================
+// Copyright (c) 1995-2012 Mort Bay Consulting Pty. Ltd.
+// ------------------------------------------------------------------------
+// All rights reserved. This program and the accompanying materials
+// are made available under the terms of the Eclipse Public License v1.0
+// and Apache License v2.0 which accompanies this distribution.
+//
+// The Eclipse Public License is available at
+// http://www.eclipse.org/legal/epl-v10.html
+//
+// The Apache License v2.0 is available at
+// http://www.opensource.org/licenses/apache2.0.php
+//
+// You may elect to redistribute this code under either of these licenses.
+// ========================================================================
+//
+
+package org.eclipse.jetty.rhttp.client;
+
+import java.io.ByteArrayOutputStream;
+import java.io.IOException;
+import java.util.LinkedHashMap;
+import java.util.Map;
+
+import org.eclipse.jetty.http.HttpParser;
+import org.eclipse.jetty.io.Buffer;
+import org.eclipse.jetty.io.ByteArrayBuffer;
+
+/**
+ * Represents the resource provider response information that is carried over the comet protocol.
+ * Instances of this class are converted into an opaque byte array of the form:
+ *
+ * <request-id> SPACE <response-length> CRLF
+ * <resource-response>
+ *
+ * The byte array form is carried as body of a normal HTTP request made by the gateway client to
+ * the gateway server.
+ * @see RHTTPRequest
+ * @version $Revision$ $Date$
+ */
+public class RHTTPResponse
+{
+ private static final String CRLF = "\r\n";
+ private static final byte[] CRLF_BYTES = CRLF.getBytes();
+
+ private final int id;
+ private final byte[] responseBytes;
+ private final byte[] frameBytes;
+ private volatile int code;
+ private volatile String message;
+ private volatile Map headers;
+ private volatile byte[] body;
+
+ public static RHTTPResponse fromFrameBytes(byte[] bytes)
+ {
+ int start = 0;
+ // Scan until we find the space
+ int end = start;
+ while (bytes[end] != ' ') ++end;
+ int responseId = Integer.parseInt(new String(bytes, start, end - start));
+ start = end + 1;
+
+ // Scan until end of line
+ while (bytes[end] != '\n') ++end;
+ int length = Integer.parseInt(new String(bytes, start, end - start - 1));
+ start = end + 1;
+
+ byte[] responseBytes = new byte[length];
+ System.arraycopy(bytes, start, responseBytes, 0, length);
+ return fromResponseBytes(responseId, responseBytes);
+ }
+
+ public static RHTTPResponse fromResponseBytes(int id, byte[] responseBytes)
+ {
+ return new RHTTPResponse(id, responseBytes);
+ }
+
+ public RHTTPResponse(int id, int code, String message, Map headers, byte[] body)
+ {
+ this.id = id;
+ this.code = code;
+ this.message = message;
+ this.headers = headers;
+ this.body = body;
+ this.responseBytes = toResponseBytes();
+ this.frameBytes = toFrameBytes(responseBytes);
+ }
+
+ private RHTTPResponse(int id, byte[] responseBytes)
+ {
+ this.id = id;
+ this.responseBytes = responseBytes;
+ this.frameBytes = toFrameBytes(responseBytes);
+ // Other fields are lazily initialized
+ }
+
+ private void initialize()
+ {
+ try
+ {
+ final ByteArrayOutputStream body = new ByteArrayOutputStream();
+ HttpParser parser = new HttpParser(new ByteArrayBuffer(responseBytes), new HttpParser.EventHandler()
+ {
+ @Override
+ public void startRequest(Buffer method, Buffer uri, Buffer httpVersion) throws IOException
+ {
+ }
+
+ @Override
+ public void startResponse(Buffer httpVersion, int statusCode, Buffer statusMessage) throws IOException
+ {
+ RHTTPResponse.this.code = statusCode;
+ RHTTPResponse.this.message = statusMessage.toString("UTF-8");
+ RHTTPResponse.this.headers = new LinkedHashMap();
+ }
+
+ @Override
+ public void parsedHeader(Buffer name, Buffer value) throws IOException
+ {
+ RHTTPResponse.this.headers.put(name.toString("UTF-8"), value.toString("UTF-8"));
+ }
+
+ @Override
+ public void content(Buffer content) throws IOException
+ {
+ content.writeTo(body);
+ }
+ });
+ parser.parse();
+ this.body = body.toByteArray();
+ }
+ catch (IOException x)
+ {
+ // Cannot happen: we're parsing from a byte[], not from an I/O stream
+ throw new AssertionError(x);
+ }
+ }
+
+ public int getId()
+ {
+ return id;
+ }
+
+ public byte[] getResponseBytes()
+ {
+ return responseBytes;
+ }
+
+ public byte[] getFrameBytes()
+ {
+ return frameBytes;
+ }
+
+ public int getStatusCode()
+ {
+ if (code == 0)
+ initialize();
+ return code;
+ }
+
+ public String getStatusMessage()
+ {
+ if (message == null)
+ initialize();
+ return message;
+ }
+
+ public Map getHeaders()
+ {
+ if (headers == null)
+ initialize();
+ return headers;
+ }
+
+ public byte[] getBody()
+ {
+ if (body == null)
+ initialize();
+ return body;
+ }
+
+ private byte[] toResponseBytes()
+ {
+ try
+ {
+ ByteArrayOutputStream bytes = new ByteArrayOutputStream();
+ bytes.write("HTTP/1.1".getBytes("UTF-8"));
+ bytes.write(' ');
+ bytes.write(String.valueOf(code).getBytes("UTF-8"));
+ bytes.write(' ');
+ bytes.write(message.getBytes("UTF-8"));
+ bytes.write(CRLF_BYTES);
+ for (Map.Entry entry : headers.entrySet())
+ {
+ bytes.write(entry.getKey().getBytes("UTF-8"));
+ bytes.write(':');
+ bytes.write(' ');
+ bytes.write(entry.getValue().getBytes("UTF-8"));
+ bytes.write(CRLF_BYTES);
+ }
+ bytes.write(CRLF_BYTES);
+ bytes.write(body);
+ bytes.close();
+ return bytes.toByteArray();
+ }
+ catch (IOException x)
+ {
+ throw new AssertionError(x);
+ }
+ }
+
+ private byte[] toFrameBytes(byte[] responseBytes)
+ {
+ try
+ {
+ ByteArrayOutputStream bytes = new ByteArrayOutputStream();
+ bytes.write(String.valueOf(id).getBytes("UTF-8"));
+ bytes.write(' ');
+ bytes.write(String.valueOf(responseBytes.length).getBytes("UTF-8"));
+ bytes.write(CRLF_BYTES);
+ bytes.write(responseBytes);
+ return bytes.toByteArray();
+ }
+ catch (IOException x)
+ {
+ throw new AssertionError(x);
+ }
+ }
+
+ @Override
+ public String toString()
+ {
+ // Use fields to avoid initialization
+ StringBuilder builder = new StringBuilder();
+ builder.append(id).append(" ");
+ builder.append(code).append(" ");
+ builder.append(message).append(" ");
+ builder.append(responseBytes.length).append("/");
+ builder.append(frameBytes.length);
+ return builder.toString();
+ }
+
+ public String toLongString()
+ {
+ // Use getters to trigger initialization
+ StringBuilder builder = new StringBuilder();
+ builder.append(id).append(" ");
+ builder.append(getStatusCode()).append(" ");
+ builder.append(getStatusMessage()).append(CRLF);
+ for (Map.Entry header : getHeaders().entrySet())
+ builder.append(header.getKey()).append(": ").append(header.getValue()).append(CRLF);
+ builder.append(getBody().length).append(" body bytes").append(CRLF);
+ return builder.toString();
+ }
+}
diff --git a/jetty-rhttp/jetty-rhttp-client/src/main/java/org/eclipse/jetty/rhttp/client/RetryingApacheClient.java b/jetty-rhttp/jetty-rhttp-client/src/main/java/org/eclipse/jetty/rhttp/client/RetryingApacheClient.java
new file mode 100644
index 00000000000..b461abbad11
--- /dev/null
+++ b/jetty-rhttp/jetty-rhttp-client/src/main/java/org/eclipse/jetty/rhttp/client/RetryingApacheClient.java
@@ -0,0 +1,112 @@
+//
+// ========================================================================
+// Copyright (c) 1995-2012 Mort Bay Consulting Pty. Ltd.
+// ------------------------------------------------------------------------
+// All rights reserved. This program and the accompanying materials
+// are made available under the terms of the Eclipse Public License v1.0
+// and Apache License v2.0 which accompanies this distribution.
+//
+// The Eclipse Public License is available at
+// http://www.eclipse.org/legal/epl-v10.html
+//
+// The Apache License v2.0 is available at
+// http://www.opensource.org/licenses/apache2.0.php
+//
+// You may elect to redistribute this code under either of these licenses.
+// ========================================================================
+//
+
+package org.eclipse.jetty.rhttp.client;
+
+import java.io.IOException;
+
+import org.apache.http.client.HttpClient;
+
+/**
+ * @version $Revision$ $Date$
+ */
+public class RetryingApacheClient extends ApacheClient
+{
+ public RetryingApacheClient(HttpClient httpClient, String gatewayURI, String targetId)
+ {
+ super(httpClient, gatewayURI, targetId);
+ addClientListener(new RetryClientListener());
+ }
+
+ @Override
+ protected void syncHandshake() throws IOException
+ {
+ while (true)
+ {
+ try
+ {
+ super.syncHandshake();
+ break;
+ }
+ catch (IOException x)
+ {
+ getLogger().debug("Handshake failed, backing off and retrying");
+ try
+ {
+ Thread.sleep(1000);
+ }
+ catch (InterruptedException xx)
+ {
+ throw (IOException)new IOException().initCause(xx);
+ }
+ }
+ }
+ }
+
+ private class RetryClientListener implements ClientListener
+ {
+ public void connectRequired()
+ {
+ getLogger().debug("Connect requested by server");
+ try
+ {
+ connect();
+ }
+ catch (IOException x)
+ {
+ // The connect() method is retried, so if it fails, it's a hard failure
+ getLogger().debug("Connect failed after server required connect, giving up");
+ }
+ }
+
+ public void connectClosed()
+ {
+ connectException();
+ }
+
+ public void connectException()
+ {
+ getLogger().debug("Connect failed, backing off and retrying");
+ try
+ {
+ Thread.sleep(1000);
+ asyncConnect();
+ }
+ catch (InterruptedException x)
+ {
+ // Ignore and stop retrying
+ Thread.currentThread().interrupt();
+ }
+ }
+
+ public void deliverException(RHTTPResponse response)
+ {
+ getLogger().debug("Deliver failed, backing off and retrying");
+ try
+ {
+ Thread.sleep(1000);
+ asyncDeliver(response);
+ }
+ catch (InterruptedException x)
+ {
+ // Ignore and stop retrying
+ Thread.currentThread().interrupt();
+ }
+ }
+ }
+}
diff --git a/jetty-rhttp/jetty-rhttp-client/src/test/java/org/eclipse/jetty/rhttp/client/ApacheClientTest.java b/jetty-rhttp/jetty-rhttp-client/src/test/java/org/eclipse/jetty/rhttp/client/ApacheClientTest.java
new file mode 100644
index 00000000000..478d4d2cd42
--- /dev/null
+++ b/jetty-rhttp/jetty-rhttp-client/src/test/java/org/eclipse/jetty/rhttp/client/ApacheClientTest.java
@@ -0,0 +1,75 @@
+//
+// ========================================================================
+// Copyright (c) 1995-2012 Mort Bay Consulting Pty. Ltd.
+// ------------------------------------------------------------------------
+// All rights reserved. This program and the accompanying materials
+// are made available under the terms of the Eclipse Public License v1.0
+// and Apache License v2.0 which accompanies this distribution.
+//
+// The Eclipse Public License is available at
+// http://www.eclipse.org/legal/epl-v10.html
+//
+// The Apache License v2.0 is available at
+// http://www.opensource.org/licenses/apache2.0.php
+//
+// You may elect to redistribute this code under either of these licenses.
+// ========================================================================
+//
+
+package org.eclipse.jetty.rhttp.client;
+
+import java.io.IOException;
+
+import org.apache.http.HttpHost;
+import org.apache.http.client.HttpRequestRetryHandler;
+import org.apache.http.conn.ClientConnectionManager;
+import org.apache.http.conn.scheme.PlainSocketFactory;
+import org.apache.http.conn.scheme.Scheme;
+import org.apache.http.conn.scheme.SchemeRegistry;
+import org.apache.http.impl.client.DefaultHttpClient;
+import org.apache.http.impl.conn.tsccm.ThreadSafeClientConnManager;
+import org.apache.http.params.BasicHttpParams;
+import org.apache.http.params.HttpParams;
+import org.apache.http.protocol.HttpContext;
+import org.eclipse.jetty.rhttp.client.ApacheClient;
+import org.eclipse.jetty.rhttp.client.RHTTPClient;
+import org.eclipse.jetty.util.log.Log;
+import org.eclipse.jetty.util.log.StdErrLog;
+
+
+/**
+ * @version $Revision$ $Date$
+ */
+public class ApacheClientTest extends ClientTest
+{
+ {
+ ((StdErrLog)Log.getLog()).setHideStacks(!Log.getLog().isDebugEnabled());
+ }
+
+ private ClientConnectionManager connectionManager;
+
+ protected RHTTPClient createClient(int port, String targetId) throws Exception
+ {
+ SchemeRegistry schemeRegistry = new SchemeRegistry();
+ schemeRegistry.register(new Scheme("http", PlainSocketFactory.getSocketFactory(), port));
+ connectionManager = new ThreadSafeClientConnManager(new BasicHttpParams(), schemeRegistry);
+ HttpParams httpParams = new BasicHttpParams();
+ httpParams.setParameter("http.default-host", new HttpHost("localhost", port));
+ DefaultHttpClient httpClient = new DefaultHttpClient(connectionManager, httpParams);
+ httpClient.setHttpRequestRetryHandler(new NoRetryHandler());
+ return new ApacheClient(httpClient, "", targetId);
+ }
+
+ protected void destroyClient(RHTTPClient client) throws Exception
+ {
+ connectionManager.shutdown();
+ }
+
+ private class NoRetryHandler implements HttpRequestRetryHandler
+ {
+ public boolean retryRequest(IOException x, int failedAttempts, HttpContext httpContext)
+ {
+ return false;
+ }
+ }
+}
diff --git a/jetty-rhttp/jetty-rhttp-client/src/test/java/org/eclipse/jetty/rhttp/client/ClientTest.java b/jetty-rhttp/jetty-rhttp-client/src/test/java/org/eclipse/jetty/rhttp/client/ClientTest.java
new file mode 100644
index 00000000000..dae49dd7be3
--- /dev/null
+++ b/jetty-rhttp/jetty-rhttp-client/src/test/java/org/eclipse/jetty/rhttp/client/ClientTest.java
@@ -0,0 +1,299 @@
+//
+// ========================================================================
+// Copyright (c) 1995-2012 Mort Bay Consulting Pty. Ltd.
+// ------------------------------------------------------------------------
+// All rights reserved. This program and the accompanying materials
+// are made available under the terms of the Eclipse Public License v1.0
+// and Apache License v2.0 which accompanies this distribution.
+//
+// The Eclipse Public License is available at
+// http://www.eclipse.org/legal/epl-v10.html
+//
+// The Apache License v2.0 is available at
+// http://www.opensource.org/licenses/apache2.0.php
+//
+// You may elect to redistribute this code under either of these licenses.
+// ========================================================================
+//
+
+package org.eclipse.jetty.rhttp.client;
+
+import java.io.IOException;
+import java.util.LinkedHashMap;
+import java.util.concurrent.CountDownLatch;
+import java.util.concurrent.TimeUnit;
+
+import javax.servlet.ServletException;
+import javax.servlet.http.HttpServletRequest;
+import javax.servlet.http.HttpServletResponse;
+
+import junit.framework.TestCase;
+
+import org.eclipse.jetty.rhttp.client.ClientListener;
+import org.eclipse.jetty.rhttp.client.RHTTPClient;
+import org.eclipse.jetty.rhttp.client.RHTTPResponse;
+import org.eclipse.jetty.server.Connector;
+import org.eclipse.jetty.server.Request;
+import org.eclipse.jetty.server.Server;
+import org.eclipse.jetty.server.bio.SocketConnector;
+import org.eclipse.jetty.server.handler.AbstractHandler;
+import org.eclipse.jetty.server.nio.SelectChannelConnector;
+import org.eclipse.jetty.util.log.Log;
+
+
+/**
+ * @version $Revision$ $Date$
+ */
+public abstract class ClientTest extends TestCase
+{
+ protected abstract RHTTPClient createClient(int port, String targetId) throws Exception;
+
+ protected abstract void destroyClient(RHTTPClient client) throws Exception;
+
+ public void testConnectNoServer() throws Exception
+ {
+ RHTTPClient client = createClient(8080, "test1");
+ try
+ {
+ client.connect();
+ fail();
+ }
+ catch (IOException x)
+ {
+ }
+ finally
+ {
+ destroyClient(client);
+ }
+ }
+
+ public void testServerExceptionOnHandshake() throws Exception
+ {
+ final CountDownLatch serverLatch = new CountDownLatch(1);
+
+ Server server = new Server();
+ Connector connector = new SelectChannelConnector();
+ server.addConnector(connector);
+ server.setHandler(new AbstractHandler()
+ {
+ public void handle(String target, Request request, HttpServletRequest httpRequest, HttpServletResponse httpResponse) throws IOException, ServletException
+ {
+ request.setHandled(true);
+ if (target.endsWith("/handshake"))
+ {
+ serverLatch.countDown();
+ throw new TestException();
+ }
+ }
+ });
+ server.start();
+ try
+ {
+ RHTTPClient client = createClient(connector.getLocalPort(), "test2");
+ try
+ {
+ try
+ {
+ client.connect();
+ fail();
+ }
+ catch (IOException x)
+ {
+ }
+
+ assertTrue(serverLatch.await(1000, TimeUnit.MILLISECONDS));
+ }
+ finally
+ {
+ destroyClient(client);
+ }
+ }
+ finally
+ {
+ server.stop();
+ }
+ }
+
+ public void testServerExceptionOnConnect() throws Exception
+ {
+ final CountDownLatch serverLatch = new CountDownLatch(1);
+
+ Server server = new Server();
+ Connector connector = new SelectChannelConnector();
+ server.addConnector(connector);
+ server.setHandler(new AbstractHandler()
+ {
+ public void handle(String target, Request request, HttpServletRequest httpRequest, HttpServletResponse httpResponse) throws IOException, ServletException
+ {
+ request.setHandled(true);
+ if (target.endsWith("/connect"))
+ {
+ serverLatch.countDown();
+ throw new TestException();
+ }
+ }
+ });
+ server.start();
+ try
+ {
+ RHTTPClient client = createClient(connector.getLocalPort(), "test3");
+ try
+ {
+ final CountDownLatch connectLatch = new CountDownLatch(1);
+ client.addClientListener(new ClientListener.Adapter()
+ {
+ @Override
+ public void connectException()
+ {
+ connectLatch.countDown();
+ }
+ });
+ client.connect();
+
+ assertTrue(serverLatch.await(1000, TimeUnit.MILLISECONDS));
+ assertTrue(connectLatch.await(1000, TimeUnit.MILLISECONDS));
+ }
+ finally
+ {
+ destroyClient(client);
+ }
+ }
+ finally
+ {
+ server.stop();
+ }
+ }
+
+ public void testServerExceptionOnDeliver() throws Exception
+ {
+ final CountDownLatch serverLatch = new CountDownLatch(1);
+
+ Server server = new Server();
+ Connector connector = new SelectChannelConnector();
+ server.addConnector(connector);
+ server.setHandler(new AbstractHandler()
+ {
+ public void handle(String target, Request request, HttpServletRequest httpRequest, HttpServletResponse httpResponse) throws IOException, ServletException
+ {
+ request.setHandled(true);
+ if (target.endsWith("/connect"))
+ {
+ serverLatch.countDown();
+ try
+ {
+ // Simulate a long poll timeout
+ Thread.sleep(10000);
+ }
+ catch (InterruptedException x)
+ {
+ Thread.currentThread().interrupt();
+ }
+ }
+ else if (target.endsWith("/deliver"))
+ {
+ // Throw an exception on deliver
+ throw new TestException();
+ }
+ }
+ });
+ server.start();
+ try
+ {
+ RHTTPClient client = createClient(connector.getLocalPort(), "test4");
+ try
+ {
+ final CountDownLatch deliverLatch = new CountDownLatch(1);
+ client.addClientListener(new ClientListener.Adapter()
+ {
+ @Override
+ public void deliverException(RHTTPResponse response)
+ {
+ deliverLatch.countDown();
+ }
+ });
+ client.connect();
+
+ assertTrue(serverLatch.await(1000, TimeUnit.MILLISECONDS));
+
+ client.deliver(new RHTTPResponse(1, 200, "OK", new LinkedHashMap(), new byte[0]));
+
+ assertTrue(deliverLatch.await(1000, TimeUnit.MILLISECONDS));
+ }
+ finally
+ {
+ destroyClient(client);
+ }
+ }
+ finally
+ {
+ server.stop();
+ }
+ }
+
+ public void testServerShutdownAfterConnect() throws Exception
+ {
+ final CountDownLatch connectLatch = new CountDownLatch(1);
+ final CountDownLatch stopLatch = new CountDownLatch(1);
+
+ Server server = new Server();
+ Connector connector = new SocketConnector();
+ server.addConnector(connector);
+ server.setHandler(new AbstractHandler()
+ {
+ public void handle(String target, Request request, HttpServletRequest httpRequest, HttpServletResponse httpResponse) throws IOException, ServletException
+ {
+ request.setHandled(true);
+ if (target.endsWith("/connect"))
+ {
+ connectLatch.countDown();
+ try
+ {
+ Thread.sleep(10000);
+ }
+ catch (InterruptedException e)
+ {
+ stopLatch.countDown();
+ }
+ }
+ }
+ });
+ server.start();
+ try
+ {
+ RHTTPClient client = createClient(connector.getLocalPort(), "test5");
+ try
+ {
+ final CountDownLatch serverLatch = new CountDownLatch(1);
+ client.addClientListener(new ClientListener.Adapter()
+ {
+ @Override
+ public void connectClosed()
+ {
+ serverLatch.countDown();
+ }
+ });
+ client.connect();
+
+ assertTrue(connectLatch.await(2000, TimeUnit.MILLISECONDS));
+
+ server.stop();
+ assertTrue(stopLatch.await(2000, TimeUnit.MILLISECONDS));
+
+ assertTrue(serverLatch.await(2000, TimeUnit.MILLISECONDS));
+ }
+ finally
+ {
+ destroyClient(client);
+ }
+ }
+ finally
+ {
+ server.stop();
+ }
+ }
+
+ public static class TestException extends NullPointerException
+ {
+
+ }
+}
diff --git a/jetty-rhttp/jetty-rhttp-client/src/test/java/org/eclipse/jetty/rhttp/client/JettyClientTest.java b/jetty-rhttp/jetty-rhttp-client/src/test/java/org/eclipse/jetty/rhttp/client/JettyClientTest.java
new file mode 100644
index 00000000000..8ebe859303b
--- /dev/null
+++ b/jetty-rhttp/jetty-rhttp-client/src/test/java/org/eclipse/jetty/rhttp/client/JettyClientTest.java
@@ -0,0 +1,52 @@
+//
+// ========================================================================
+// Copyright (c) 1995-2012 Mort Bay Consulting Pty. Ltd.
+// ------------------------------------------------------------------------
+// All rights reserved. This program and the accompanying materials
+// are made available under the terms of the Eclipse Public License v1.0
+// and Apache License v2.0 which accompanies this distribution.
+//
+// The Eclipse Public License is available at
+// http://www.eclipse.org/legal/epl-v10.html
+//
+// The Apache License v2.0 is available at
+// http://www.opensource.org/licenses/apache2.0.php
+//
+// You may elect to redistribute this code under either of these licenses.
+// ========================================================================
+//
+
+package org.eclipse.jetty.rhttp.client;
+
+import org.eclipse.jetty.client.Address;
+import org.eclipse.jetty.client.HttpClient;
+import org.eclipse.jetty.rhttp.client.JettyClient;
+import org.eclipse.jetty.rhttp.client.RHTTPClient;
+import org.eclipse.jetty.util.log.Log;
+import org.eclipse.jetty.util.log.StdErrLog;
+
+
+/**
+ * @version $Revision$ $Date$
+ */
+public class JettyClientTest extends ClientTest
+{
+ {
+ ((StdErrLog)Log.getLog()).setHideStacks(!Log.getLog().isDebugEnabled());
+ }
+
+ private HttpClient httpClient;
+
+ protected RHTTPClient createClient(int port, String targetId) throws Exception
+ {
+ ((StdErrLog)Log.getLog()).setSource(true);
+ httpClient = new HttpClient();
+ httpClient.start();
+ return new JettyClient(httpClient, new Address("localhost", port), "", targetId);
+ }
+
+ protected void destroyClient(RHTTPClient client) throws Exception
+ {
+ httpClient.stop();
+ }
+}
diff --git a/jetty-rhttp/jetty-rhttp-client/src/test/java/org/eclipse/jetty/rhttp/client/RequestTest.java b/jetty-rhttp/jetty-rhttp-client/src/test/java/org/eclipse/jetty/rhttp/client/RequestTest.java
new file mode 100644
index 00000000000..b34ab4931e1
--- /dev/null
+++ b/jetty-rhttp/jetty-rhttp-client/src/test/java/org/eclipse/jetty/rhttp/client/RequestTest.java
@@ -0,0 +1,85 @@
+//
+// ========================================================================
+// Copyright (c) 1995-2012 Mort Bay Consulting Pty. Ltd.
+// ------------------------------------------------------------------------
+// All rights reserved. This program and the accompanying materials
+// are made available under the terms of the Eclipse Public License v1.0
+// and Apache License v2.0 which accompanies this distribution.
+//
+// The Eclipse Public License is available at
+// http://www.eclipse.org/legal/epl-v10.html
+//
+// The Apache License v2.0 is available at
+// http://www.opensource.org/licenses/apache2.0.php
+//
+// You may elect to redistribute this code under either of these licenses.
+// ========================================================================
+//
+
+package org.eclipse.jetty.rhttp.client;
+
+import java.util.Arrays;
+import java.util.LinkedHashMap;
+import java.util.List;
+import java.util.Map;
+
+import org.eclipse.jetty.rhttp.client.RHTTPRequest;
+
+import junit.framework.TestCase;
+
+/**
+ * @version $Revision$ $Date$
+ */
+public class RequestTest extends TestCase
+{
+ public void testRequestConversions() throws Exception
+ {
+ int id = 1;
+ String method = "GET";
+ String uri = "/test";
+ Map headers = new LinkedHashMap();
+ headers.put("X", "X");
+ headers.put("Y", "Y");
+ headers.put("Z", "Z");
+ byte[] body = "BODY".getBytes("UTF-8");
+ headers.put("Content-Length", String.valueOf(body.length));
+ RHTTPRequest request1 = new RHTTPRequest(id, method, uri, headers, body);
+ byte[] requestBytes1 = request1.getRequestBytes();
+ RHTTPRequest request2 = RHTTPRequest.fromRequestBytes(id, requestBytes1);
+ assertEquals(id, request2.getId());
+ assertEquals(method, request2.getMethod());
+ assertEquals(uri, request2.getURI());
+ assertEquals(headers, request2.getHeaders());
+ assertTrue(Arrays.equals(request2.getBody(), body));
+
+ byte[] requestBytes2 = request2.getRequestBytes();
+ assertTrue(Arrays.equals(requestBytes1, requestBytes2));
+ }
+
+ public void testFrameConversions() throws Exception
+ {
+ int id = 1;
+ String method = "GET";
+ String uri = "/test";
+ Map headers = new LinkedHashMap();
+ headers.put("X", "X");
+ headers.put("Y", "Y");
+ headers.put("Z", "Z");
+ byte[] body = "BODY".getBytes("UTF-8");
+ headers.put("Content-Length", String.valueOf(body.length));
+ RHTTPRequest request1 = new RHTTPRequest(id, method, uri, headers, body);
+ byte[] frameBytes1 = request1.getFrameBytes();
+ List requests = RHTTPRequest.fromFrameBytes(frameBytes1);
+ assertNotNull(requests);
+ assertEquals(1, requests.size());
+ RHTTPRequest request2 = requests.get(0);
+ assertEquals(id, request2.getId());
+ assertEquals(method, request2.getMethod());
+ assertEquals(uri, request2.getURI());
+ assertEquals(headers, request2.getHeaders());
+ assertTrue(Arrays.equals(request2.getBody(), body));
+
+ byte[] frameBytes2 = request2.getFrameBytes();
+ assertTrue(Arrays.equals(frameBytes1, frameBytes2));
+ }
+}
diff --git a/jetty-rhttp/jetty-rhttp-client/src/test/java/org/eclipse/jetty/rhttp/client/ResponseTest.java b/jetty-rhttp/jetty-rhttp-client/src/test/java/org/eclipse/jetty/rhttp/client/ResponseTest.java
new file mode 100644
index 00000000000..4873369c379
--- /dev/null
+++ b/jetty-rhttp/jetty-rhttp-client/src/test/java/org/eclipse/jetty/rhttp/client/ResponseTest.java
@@ -0,0 +1,85 @@
+//
+// ========================================================================
+// Copyright (c) 1995-2012 Mort Bay Consulting Pty. Ltd.
+// ------------------------------------------------------------------------
+// All rights reserved. This program and the accompanying materials
+// are made available under the terms of the Eclipse Public License v1.0
+// and Apache License v2.0 which accompanies this distribution.
+//
+// The Eclipse Public License is available at
+// http://www.eclipse.org/legal/epl-v10.html
+//
+// The Apache License v2.0 is available at
+// http://www.opensource.org/licenses/apache2.0.php
+//
+// You may elect to redistribute this code under either of these licenses.
+// ========================================================================
+//
+
+package org.eclipse.jetty.rhttp.client;
+
+import java.util.Arrays;
+import java.util.LinkedHashMap;
+import java.util.Map;
+
+import org.eclipse.jetty.rhttp.client.RHTTPResponse;
+import org.eclipse.jetty.util.log.Log;
+import org.eclipse.jetty.util.log.StdErrLog;
+
+import junit.framework.TestCase;
+
+/**
+ * @version $Revision$ $Date$
+ */
+public class ResponseTest extends TestCase
+{
+ {
+ ((StdErrLog)Log.getLog()).setHideStacks(!Log.getLog().isDebugEnabled());
+ }
+
+ public void testResponseConversions() throws Exception
+ {
+ int id = 1;
+ int statusCode = 200;
+ String statusMessage = "OK";
+ Map headers = new LinkedHashMap();
+ headers.put("X", "X");
+ headers.put("Y", "Y");
+ headers.put("Z", "Z");
+ byte[] body = "BODY".getBytes("UTF-8");
+ RHTTPResponse response1 = new RHTTPResponse(id, statusCode, statusMessage, headers, body);
+ byte[] responseBytes1 = response1.getResponseBytes();
+ RHTTPResponse response2 = RHTTPResponse.fromResponseBytes(id, responseBytes1);
+ assertEquals(id, response2.getId());
+ assertEquals(statusCode, response2.getStatusCode());
+ assertEquals(statusMessage, response2.getStatusMessage());
+ assertEquals(headers, response2.getHeaders());
+ assertTrue(Arrays.equals(response2.getBody(), body));
+
+ byte[] responseBytes2 = response2.getResponseBytes();
+ assertTrue(Arrays.equals(responseBytes1, responseBytes2));
+ }
+
+ public void testFrameConversions() throws Exception
+ {
+ int id = 1;
+ int statusCode = 200;
+ String statusMessage = "OK";
+ Map headers = new LinkedHashMap();
+ headers.put("X", "X");
+ headers.put("Y", "Y");
+ headers.put("Z", "Z");
+ byte[] body = "BODY".getBytes("UTF-8");
+ RHTTPResponse response1 = new RHTTPResponse(id, statusCode, statusMessage, headers, body);
+ byte[] frameBytes1 = response1.getFrameBytes();
+ RHTTPResponse response2 = RHTTPResponse.fromFrameBytes(frameBytes1);
+ assertEquals(id, response2.getId());
+ assertEquals(statusCode, response2.getStatusCode());
+ assertEquals(response2.getStatusMessage(), statusMessage);
+ assertEquals(headers, response2.getHeaders());
+ assertTrue(Arrays.equals(response2.getBody(), body));
+
+ byte[] frameBytes2 = response2.getFrameBytes();
+ assertTrue(Arrays.equals(frameBytes1, frameBytes2));
+ }
+}
diff --git a/jetty-rhttp/jetty-rhttp-connector/pom.xml b/jetty-rhttp/jetty-rhttp-connector/pom.xml
new file mode 100644
index 00000000000..c507ebd4fdf
--- /dev/null
+++ b/jetty-rhttp/jetty-rhttp-connector/pom.xml
@@ -0,0 +1,62 @@
+
+
+
+ org.eclipse.jetty.rhttp
+ jetty-rhttp-project
+ 9.0.0-SNAPSHOT
+
+
+ 4.0.0
+ reverse-http-connector
+ jar
+ Jetty :: Reverse HTTP :: Connector
+
+
+
+
+ org.codehaus.mojo
+ exec-maven-plugin
+
+ test
+ org.eclipse.jetty.rhttp.connector.TestReverseServer
+
+
+
+
+
+
+
+ org.eclipse.jetty
+ reverse-http-client
+ ${project.version}
+
+
+ org.eclipse.jetty
+ jetty-util
+
+
+ org.eclipse.jetty
+ jetty-io
+
+
+ org.eclipse.jetty
+ jetty-server
+
+
+ org.eclipse.jetty
+ example-jetty-embedded
+ ${project.version}
+ test
+
+
+ junit
+ junit
+
+
+ javax.servlet
+ servlet-api
+ test
+
+
+
+
diff --git a/jetty-rhttp/jetty-rhttp-connector/src/main/config/etc/jetty-rhttp.xml b/jetty-rhttp/jetty-rhttp-connector/src/main/config/etc/jetty-rhttp.xml
new file mode 100644
index 00000000000..ba7055f5524
--- /dev/null
+++ b/jetty-rhttp/jetty-rhttp-connector/src/main/config/etc/jetty-rhttp.xml
@@ -0,0 +1,23 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ http://localhost:8888/
+ nodeA
+
+
+
+
+
diff --git a/jetty-rhttp/jetty-rhttp-connector/src/main/java/org/eclipse/jetty/rhttp/connector/ReverseHTTPConnector.java b/jetty-rhttp/jetty-rhttp-connector/src/main/java/org/eclipse/jetty/rhttp/connector/ReverseHTTPConnector.java
new file mode 100644
index 00000000000..d95e4995dea
--- /dev/null
+++ b/jetty-rhttp/jetty-rhttp-connector/src/main/java/org/eclipse/jetty/rhttp/connector/ReverseHTTPConnector.java
@@ -0,0 +1,170 @@
+//
+// ========================================================================
+// Copyright (c) 1995-2012 Mort Bay Consulting Pty. Ltd.
+// ------------------------------------------------------------------------
+// All rights reserved. This program and the accompanying materials
+// are made available under the terms of the Eclipse Public License v1.0
+// and Apache License v2.0 which accompanies this distribution.
+//
+// The Eclipse Public License is available at
+// http://www.eclipse.org/legal/epl-v10.html
+//
+// The Apache License v2.0 is available at
+// http://www.opensource.org/licenses/apache2.0.php
+//
+// You may elect to redistribute this code under either of these licenses.
+// ========================================================================
+//
+
+package org.eclipse.jetty.rhttp.connector;
+
+import java.io.IOException;
+import java.util.concurrent.BlockingQueue;
+import java.util.concurrent.LinkedBlockingQueue;
+
+import org.eclipse.jetty.io.ByteArrayEndPoint;
+import org.eclipse.jetty.io.Connection;
+import org.eclipse.jetty.io.EndPoint;
+import org.eclipse.jetty.rhttp.client.RHTTPClient;
+import org.eclipse.jetty.rhttp.client.RHTTPListener;
+import org.eclipse.jetty.rhttp.client.RHTTPRequest;
+import org.eclipse.jetty.rhttp.client.RHTTPResponse;
+import org.eclipse.jetty.server.AbstractConnector;
+import org.eclipse.jetty.server.AbstractHttpConnection;
+import org.eclipse.jetty.server.BlockingHttpConnection;
+import org.eclipse.jetty.util.component.LifeCycle;
+import org.eclipse.jetty.util.log.Log;
+import org.eclipse.jetty.util.log.Logger;
+
+/**
+ * An implementation of a Jetty connector that uses a {@link RHTTPClient} connected
+ * to a gateway server to receive requests, feed them to the Jetty server, and
+ * forward responses from the Jetty server to the gateway server.
+ *
+ * @version $Revision$ $Date$
+ */
+public class ReverseHTTPConnector extends AbstractConnector implements RHTTPListener
+{
+ private static final Logger LOG = Log.getLogger(ReverseHTTPConnector.class);
+
+ private final BlockingQueue requests = new LinkedBlockingQueue();
+ private final RHTTPClient client;
+
+ public ReverseHTTPConnector(RHTTPClient client)
+ {
+ this.client = client;
+ super.setHost(client.getHost());
+ super.setPort(client.getPort());
+ }
+
+ @Override
+ public void setHost(String host)
+ {
+ throw new UnsupportedOperationException();
+ }
+
+ @Override
+ public void setPort(int port)
+ {
+ throw new UnsupportedOperationException();
+ }
+
+ @Override
+ protected void doStart() throws Exception
+ {
+ if (client instanceof LifeCycle)
+ ((LifeCycle)client).start();
+ super.doStart();
+ client.connect();
+ }
+
+ @Override
+ protected void doStop() throws Exception
+ {
+ client.disconnect();
+ super.doStop();
+ if (client instanceof LifeCycle)
+ ((LifeCycle)client).stop();
+ }
+
+ public void open()
+ {
+ client.addListener(this);
+ }
+
+ public void close()
+ {
+ client.removeListener(this);
+ }
+
+ public int getLocalPort()
+ {
+ return -1;
+ }
+
+ public Object getConnection()
+ {
+ return this;
+ }
+
+ @Override
+ protected void accept(int acceptorId) throws IOException, InterruptedException
+ {
+ RHTTPRequest request = requests.take();
+ IncomingRequest incomingRequest = new IncomingRequest(request);
+ getThreadPool().dispatch(incomingRequest);
+ }
+
+ @Override
+ public void persist(EndPoint endpoint) throws IOException
+ {
+ // Signals that the connection should not be closed
+ // Do nothing in this case, as we run from memory
+ }
+
+ public void onRequest(RHTTPRequest request) throws Exception
+ {
+ requests.add(request);
+ }
+
+ private class IncomingRequest implements Runnable
+ {
+ private final RHTTPRequest request;
+
+ private IncomingRequest(RHTTPRequest request)
+ {
+ this.request = request;
+ }
+
+ public void run()
+ {
+ byte[] requestBytes = request.getRequestBytes();
+
+ ByteArrayEndPoint endPoint = new ByteArrayEndPoint(requestBytes, 1024);
+ endPoint.setGrowOutput(true);
+
+ AbstractHttpConnection connection = new BlockingHttpConnection(ReverseHTTPConnector.this, endPoint, getServer());
+
+ connectionOpened(connection);
+ try
+ {
+ // Loop over the whole content, since handle() only
+ // reads up to the connection buffer's capacities
+ while (endPoint.getIn().length() > 0)
+ connection.handle();
+
+ byte[] responseBytes = endPoint.getOut().asArray();
+ RHTTPResponse response = RHTTPResponse.fromResponseBytes(request.getId(), responseBytes);
+ client.deliver(response);
+ }
+ catch (Exception x)
+ {
+ LOG.debug(x);
+ }
+ finally
+ {
+ connectionClosed(connection);
+ }
+ }
+ }
+}
diff --git a/jetty-rhttp/jetty-rhttp-connector/src/test/java/org/eclipse/jetty/rhttp/connector/ReverseHTTPConnectorTest.java b/jetty-rhttp/jetty-rhttp-connector/src/test/java/org/eclipse/jetty/rhttp/connector/ReverseHTTPConnectorTest.java
new file mode 100644
index 00000000000..900fdee44ef
--- /dev/null
+++ b/jetty-rhttp/jetty-rhttp-connector/src/test/java/org/eclipse/jetty/rhttp/connector/ReverseHTTPConnectorTest.java
@@ -0,0 +1,187 @@
+//
+// ========================================================================
+// Copyright (c) 1995-2012 Mort Bay Consulting Pty. Ltd.
+// ------------------------------------------------------------------------
+// All rights reserved. This program and the accompanying materials
+// are made available under the terms of the Eclipse Public License v1.0
+// and Apache License v2.0 which accompanies this distribution.
+//
+// The Eclipse Public License is available at
+// http://www.eclipse.org/legal/epl-v10.html
+//
+// The Apache License v2.0 is available at
+// http://www.opensource.org/licenses/apache2.0.php
+//
+// You may elect to redistribute this code under either of these licenses.
+// ========================================================================
+//
+
+package org.eclipse.jetty.rhttp.connector;
+
+import java.io.ByteArrayOutputStream;
+import java.io.IOException;
+import java.io.InputStream;
+import java.io.OutputStream;
+import java.util.Arrays;
+import java.util.HashMap;
+import java.util.concurrent.CountDownLatch;
+import java.util.concurrent.TimeUnit;
+import java.util.concurrent.atomic.AtomicReference;
+
+import javax.servlet.ServletException;
+import javax.servlet.http.HttpServletRequest;
+import javax.servlet.http.HttpServletResponse;
+
+import junit.framework.TestCase;
+
+import org.eclipse.jetty.rhttp.client.ClientListener;
+import org.eclipse.jetty.rhttp.client.RHTTPClient;
+import org.eclipse.jetty.rhttp.client.RHTTPListener;
+import org.eclipse.jetty.rhttp.client.RHTTPRequest;
+import org.eclipse.jetty.rhttp.client.RHTTPResponse;
+import org.eclipse.jetty.rhttp.connector.ReverseHTTPConnector;
+import org.eclipse.jetty.server.Server;
+import org.eclipse.jetty.server.handler.AbstractHandler;
+
+/**
+ * @version $Revision$ $Date$
+ */
+public class ReverseHTTPConnectorTest extends TestCase
+{
+ public void testGatewayConnectorWithoutRequestBody() throws Exception
+ {
+ testGatewayConnector(false);
+ }
+
+ public void testGatewayConnectorWithRequestBody() throws Exception
+ {
+ testGatewayConnector(true);
+ }
+
+ private void testGatewayConnector(boolean withRequestBody) throws Exception
+ {
+ Server server = new Server();
+ final CountDownLatch handlerLatch = new CountDownLatch(1);
+ CountDownLatch clientLatch = new CountDownLatch(1);
+ AtomicReference responseRef = new AtomicReference();
+ ReverseHTTPConnector connector = new ReverseHTTPConnector(new TestClient(clientLatch, responseRef));
+ server.addConnector(connector);
+ final String method = "POST";
+ final String uri = "/test";
+ final byte[] requestBody = withRequestBody ? "REQUEST-BODY".getBytes("UTF-8") : new byte[0];
+ final int statusCode = HttpServletResponse.SC_CREATED;
+ final String headerName = "foo";
+ final String headerValue = "bar";
+ final byte[] responseBody = "RESPONSE-BODY".getBytes("UTF-8");
+ server.setHandler(new AbstractHandler()
+ {
+ public void handle(String pathInfo, org.eclipse.jetty.server.Request request, HttpServletRequest httpRequest, HttpServletResponse httpResponse) throws IOException, ServletException
+ {
+ assertEquals(method, httpRequest.getMethod());
+ assertEquals(uri, httpRequest.getRequestURI());
+ assertEquals(headerValue, httpRequest.getHeader(headerName));
+ ByteArrayOutputStream baos = new ByteArrayOutputStream();
+ InputStream input = httpRequest.getInputStream();
+ int read;
+ while ((read = input.read()) >= 0)
+ baos.write(read);
+ baos.close();
+ assertTrue(Arrays.equals(requestBody, baos.toByteArray()));
+
+ httpResponse.setStatus(statusCode);
+ httpResponse.setHeader(headerName, headerValue);
+ OutputStream output = httpResponse.getOutputStream();
+ output.write(responseBody);
+ output.flush();
+ request.setHandled(true);
+ handlerLatch.countDown();
+ }
+ });
+ server.start();
+
+ HashMap headers = new HashMap();
+ headers.put("Host", "localhost");
+ headers.put(headerName, headerValue);
+ headers.put("Content-Length", String.valueOf(requestBody.length));
+ RHTTPRequest request = new RHTTPRequest(1, method, uri, headers, requestBody);
+ request = RHTTPRequest.fromRequestBytes(request.getId(), request.getRequestBytes());
+ connector.onRequest(request);
+
+ assertTrue(handlerLatch.await(1000, TimeUnit.MILLISECONDS));
+ assertTrue(clientLatch.await(1000, TimeUnit.MILLISECONDS));
+ RHTTPResponse response = responseRef.get();
+ assertEquals(request.getId(), response.getId());
+ assertEquals(statusCode, response.getStatusCode());
+ assertEquals(headerValue, response.getHeaders().get(headerName));
+ assertTrue(Arrays.equals(response.getBody(), responseBody));
+ }
+
+ private class TestClient implements RHTTPClient
+ {
+ private final CountDownLatch latch;
+ private final AtomicReference responseRef;
+
+ private TestClient(CountDownLatch latch, AtomicReference response)
+ {
+ this.latch = latch;
+ this.responseRef = response;
+ }
+
+ public String getTargetId()
+ {
+ return null;
+ }
+
+ public void connect() throws IOException
+ {
+ }
+
+ public void disconnect() throws IOException
+ {
+ }
+
+ public void deliver(RHTTPResponse response) throws IOException
+ {
+ responseRef.set(response);
+ latch.countDown();
+ }
+
+ public void addListener(RHTTPListener listener)
+ {
+ }
+
+ public void removeListener(RHTTPListener listener)
+ {
+ }
+
+ public void addClientListener(ClientListener listener)
+ {
+ }
+
+ public void removeClientListener(ClientListener listener)
+ {
+ }
+
+ public String getHost()
+ {
+ return null;
+ }
+
+ public int getPort()
+ {
+ return 0;
+ }
+
+ public String getGatewayURI()
+ {
+ // TODO Auto-generated method stub
+ return null;
+ }
+
+ public String getPath()
+ {
+ // TODO Auto-generated method stub
+ return null;
+ }
+ }
+}
diff --git a/jetty-rhttp/jetty-rhttp-connector/src/test/java/org/eclipse/jetty/rhttp/connector/TestReverseServer.java b/jetty-rhttp/jetty-rhttp-connector/src/test/java/org/eclipse/jetty/rhttp/connector/TestReverseServer.java
new file mode 100644
index 00000000000..628768c648a
--- /dev/null
+++ b/jetty-rhttp/jetty-rhttp-connector/src/test/java/org/eclipse/jetty/rhttp/connector/TestReverseServer.java
@@ -0,0 +1,58 @@
+//
+// ========================================================================
+// Copyright (c) 1995-2012 Mort Bay Consulting Pty. Ltd.
+// ------------------------------------------------------------------------
+// All rights reserved. This program and the accompanying materials
+// are made available under the terms of the Eclipse Public License v1.0
+// and Apache License v2.0 which accompanies this distribution.
+//
+// The Eclipse Public License is available at
+// http://www.eclipse.org/legal/epl-v10.html
+//
+// The Apache License v2.0 is available at
+// http://www.opensource.org/licenses/apache2.0.php
+//
+// You may elect to redistribute this code under either of these licenses.
+// ========================================================================
+//
+
+package org.eclipse.jetty.rhttp.connector;
+
+import org.eclipse.jetty.client.HttpClient;
+import org.eclipse.jetty.embedded.HelloHandler;
+import org.eclipse.jetty.rhttp.client.JettyClient;
+import org.eclipse.jetty.rhttp.client.RHTTPClient;
+import org.eclipse.jetty.rhttp.connector.ReverseHTTPConnector;
+import org.eclipse.jetty.server.Server;
+import org.eclipse.jetty.util.log.Log;
+
+/**
+ * A Test content server that uses a {@link ReverseHTTPConnector}.
+ * The main of this class starts 3 TestReversionServers with IDs A, B and C.
+ */
+public class TestReverseServer extends Server
+{
+ TestReverseServer(String targetId)
+ {
+ setHandler(new HelloHandler("Hello "+targetId,"Hi from "+targetId));
+
+ HttpClient httpClient = new HttpClient();
+ RHTTPClient client = new JettyClient(httpClient,"http://localhost:8080/__rhttp",targetId);
+ ReverseHTTPConnector connector = new ReverseHTTPConnector(client);
+
+ addConnector(connector);
+ }
+
+ public static void main(String... args) throws Exception
+ {
+ Log.getLogger("org.mortbay.jetty.rhttp.client").setDebugEnabled(true);
+
+ TestReverseServer[] node = new TestReverseServer[] { new TestReverseServer("A"),new TestReverseServer("B"),new TestReverseServer("C") };
+
+ for (TestReverseServer s : node)
+ s.start();
+
+ for (TestReverseServer s : node)
+ s.join();
+ }
+}
diff --git a/jetty-rhttp/jetty-rhttp-gateway/pom.xml b/jetty-rhttp/jetty-rhttp-gateway/pom.xml
new file mode 100644
index 00000000000..5905e02a58c
--- /dev/null
+++ b/jetty-rhttp/jetty-rhttp-gateway/pom.xml
@@ -0,0 +1,65 @@
+
+
+
+ org.eclipse.jetty.rhttp
+ jetty-rhttp-project
+ 9.0.0-SNAPSHOT
+
+
+ 4.0.0
+ reverse-http-gateway
+ jar
+ Jetty :: Reverse HTTP :: Gateway
+
+
+
+
+ org.codehaus.mojo
+ exec-maven-plugin
+
+ org.mortbay.jetty.rhttp.gateway.Main
+
+
+
+
+
+
+
+
+
+ org.eclipse.jetty
+ reverse-http-client
+ ${project.version}
+
+
+ javax.servlet
+ servlet-api
+
+
+ org.eclipse.jetty
+ jetty-io
+
+
+ org.eclipse.jetty
+ jetty-continuation
+ ${project.version}
+
+
+ org.eclipse.jetty
+ jetty-server
+
+
+ org.eclipse.jetty
+ jetty-servlet
+ ${project.version}
+
+
+ org.eclipse.jetty
+ jetty-client
+
+
+ junit
+ junit
+
+
+
diff --git a/jetty-rhttp/jetty-rhttp-gateway/src/main/java/org/eclipse/jetty/rhttp/gateway/ClientDelegate.java b/jetty-rhttp/jetty-rhttp-gateway/src/main/java/org/eclipse/jetty/rhttp/gateway/ClientDelegate.java
new file mode 100644
index 00000000000..a57ed09cc32
--- /dev/null
+++ b/jetty-rhttp/jetty-rhttp-gateway/src/main/java/org/eclipse/jetty/rhttp/gateway/ClientDelegate.java
@@ -0,0 +1,92 @@
+//
+// ========================================================================
+// Copyright (c) 1995-2012 Mort Bay Consulting Pty. Ltd.
+// ------------------------------------------------------------------------
+// All rights reserved. This program and the accompanying materials
+// are made available under the terms of the Eclipse Public License v1.0
+// and Apache License v2.0 which accompanies this distribution.
+//
+// The Eclipse Public License is available at
+// http://www.eclipse.org/legal/epl-v10.html
+//
+// The Apache License v2.0 is available at
+// http://www.opensource.org/licenses/apache2.0.php
+//
+// You may elect to redistribute this code under either of these licenses.
+// ========================================================================
+//
+
+package org.eclipse.jetty.rhttp.gateway;
+
+import java.io.IOException;
+import java.util.List;
+
+import javax.servlet.http.HttpServletRequest;
+
+import org.eclipse.jetty.rhttp.client.RHTTPRequest;
+
+
+/**
+ * A ClientDelegate is the server-side counterpart of a gateway client.
+ * The gateway client, the comet protocol and the ClientDelegate form the
+ * Half-Object plus Protocol pattern that is used between the gateway server
+ * and the gateway client.
+ * ClientDelegate offers a server-side API on top of the comet communication.
+ * The API allows to enqueue server-side events to the gateway client, allows to
+ * flush them to the gateway client, and allows to close and dispose server-side
+ * resources when the gateway client disconnects.
+ *
+ * @version $Revision$ $Date$
+ */
+public interface ClientDelegate
+{
+ /**
+ * @return the targetId that uniquely identifies this client delegate.
+ */
+ public String getTargetId();
+
+ /**
+ * Enqueues the given request to the delivery queue so that it will be sent to the
+ * gateway client on the first flush occasion.
+ * Requests may fail to be queued, for example because the gateway client disconnected
+ * concurrently.
+ *
+ * @param request the request to add to the delivery queue
+ * @return whether the request has been queued or not
+ * @see #process(HttpServletRequest)
+ */
+ public boolean enqueue(RHTTPRequest request);
+
+ /**
+ * Flushes the requests that have been {@link #enqueue(RHTTPRequest) enqueued}.
+ * If no requests have been enqueued, then this method may suspend the current request for
+ * the long poll timeout.
+ * The request is suspended only if all these conditions holds true:
+ *
+ * - it is not the first time that this method is called for this client delegate
+ * - no requests have been enqueued
+ * - this client delegate is not closed
+ * - the previous call to this method did not suspend the request
+ *
+ * In all other cases, a response if sent to the gateway client, possibly containing no requests.
+ *
+ * @param httpRequest the HTTP request for the long poll request from the gateway client
+ * @return the list of requests to send to the gateway client, or null if no response should be sent
+ * to the gateway client
+ * @throws IOException in case of I/O exception while flushing content to the gateway client
+ * @see #enqueue(RHTTPRequest)
+ */
+ public List process(HttpServletRequest httpRequest) throws IOException;
+
+ /**
+ * Closes this client delegate, in response to a gateway client request to disconnect.
+ * @see #isClosed()
+ */
+ public void close();
+
+ /**
+ * @return whether this delegate client is closed
+ * @see #close()
+ */
+ public boolean isClosed();
+}
diff --git a/jetty-rhttp/jetty-rhttp-gateway/src/main/java/org/eclipse/jetty/rhttp/gateway/ConnectorServlet.java b/jetty-rhttp/jetty-rhttp-gateway/src/main/java/org/eclipse/jetty/rhttp/gateway/ConnectorServlet.java
new file mode 100644
index 00000000000..6faf5e08783
--- /dev/null
+++ b/jetty-rhttp/jetty-rhttp-gateway/src/main/java/org/eclipse/jetty/rhttp/gateway/ConnectorServlet.java
@@ -0,0 +1,224 @@
+//
+// ========================================================================
+// Copyright (c) 1995-2012 Mort Bay Consulting Pty. Ltd.
+// ------------------------------------------------------------------------
+// All rights reserved. This program and the accompanying materials
+// are made available under the terms of the Eclipse Public License v1.0
+// and Apache License v2.0 which accompanies this distribution.
+//
+// The Eclipse Public License is available at
+// http://www.eclipse.org/legal/epl-v10.html
+//
+// The Apache License v2.0 is available at
+// http://www.opensource.org/licenses/apache2.0.php
+//
+// You may elect to redistribute this code under either of these licenses.
+// ========================================================================
+//
+
+package org.eclipse.jetty.rhttp.gateway;
+
+import java.io.IOException;
+import java.util.List;
+import java.util.concurrent.ConcurrentHashMap;
+import java.util.concurrent.ConcurrentMap;
+import java.util.concurrent.Executors;
+import java.util.concurrent.Future;
+import java.util.concurrent.ScheduledExecutorService;
+import java.util.concurrent.TimeUnit;
+
+import javax.servlet.ServletException;
+import javax.servlet.ServletOutputStream;
+import javax.servlet.http.HttpServlet;
+import javax.servlet.http.HttpServletRequest;
+import javax.servlet.http.HttpServletResponse;
+
+import org.eclipse.jetty.rhttp.client.RHTTPRequest;
+import org.eclipse.jetty.rhttp.client.RHTTPResponse;
+import org.eclipse.jetty.util.log.Log;
+import org.eclipse.jetty.util.log.Logger;
+
+/**
+ * The servlet that handles the communication with the gateway clients.
+ * @version $Revision$ $Date$
+ */
+public class ConnectorServlet extends HttpServlet
+{
+ private final Logger logger = Log.getLogger(getClass().toString());
+ private final TargetIdRetriever targetIdRetriever = new StandardTargetIdRetriever();
+ private final ScheduledExecutorService scheduler = Executors.newSingleThreadScheduledExecutor();
+ private final ConcurrentMap> expirations = new ConcurrentHashMap>();
+ private final Gateway gateway;
+ private long clientTimeout=15000;
+
+ public ConnectorServlet(Gateway gateway)
+ {
+ this.gateway = gateway;
+ }
+
+ @Override
+ public void init() throws ServletException
+ {
+ String t = getInitParameter("clientTimeout");
+ if (t!=null && !"".equals(t))
+ clientTimeout=Long.parseLong(t);
+ }
+
+ @Override
+ protected void service(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException
+ {
+ String targetId = targetIdRetriever.retrieveTargetId(request);
+
+ String uri = request.getRequestURI();
+ String path = uri.substring(request.getServletPath().length());
+ String[] segments = path.split("/");
+ if (segments.length < 3)
+ throw new ServletException("Invalid request to " + getClass().getSimpleName() + ": " + uri);
+
+ String action = segments[2];
+ if ("handshake".equals(action))
+ serviceHandshake(targetId, request, response);
+ else if ("connect".equals(action))
+ serviceConnect(targetId, request, response);
+ else if ("deliver".equals(action))
+ serviceDeliver(targetId, request, response);
+ else if ("disconnect".equals(action))
+ serviceDisconnect(targetId, request, response);
+ else
+ throw new ServletException("Invalid request to " + getClass().getSimpleName() + ": " + uri);
+ }
+
+ private void serviceHandshake(String targetId, HttpServletRequest httpRequest, HttpServletResponse httpResponse) throws IOException
+ {
+ ClientDelegate client = gateway.getClientDelegate(targetId);
+ if (client != null)
+ throw new IOException("Client with targetId " + targetId + " is already connected");
+
+ client = gateway.newClientDelegate(targetId);
+ ClientDelegate existing = gateway.addClientDelegate(targetId, client);
+ if (existing != null)
+ throw new IOException("Client with targetId " + targetId + " is already connected");
+
+ flush(client, httpRequest, httpResponse);
+ }
+
+ private void flush(ClientDelegate client, HttpServletRequest httpRequest, HttpServletResponse httpResponse) throws IOException
+ {
+ List requests = client.process(httpRequest);
+ if (requests != null)
+ {
+ // Schedule before sending the requests, to avoid that the remote client
+ // reconnects before we have scheduled the expiration timeout.
+ if (!client.isClosed())
+ schedule(client);
+
+ ServletOutputStream output = httpResponse.getOutputStream();
+ for (RHTTPRequest request : requests)
+ output.write(request.getFrameBytes());
+ // I could count the framed bytes of all requests and set a Content-Length header,
+ // but the implementation of ServletOutputStream takes care of everything:
+ // if the request was HTTP/1.1, then flushing result in a chunked response, but the
+ // client know how to handle it; if the request was HTTP/1.0, then no chunking.
+ // To avoid chunking in HTTP/1.1 I must set the Content-Length header.
+ output.flush();
+ logger.debug("Delivered to device {} requests {} ", client.getTargetId(), requests);
+ }
+ }
+
+ private void schedule(ClientDelegate client)
+ {
+ Future> task = scheduler.schedule(new ClientExpirationTask(client), clientTimeout, TimeUnit.MILLISECONDS);
+ Future> existing = expirations.put(client.getTargetId(), task);
+ assert existing == null;
+ }
+
+ private void unschedule(String targetId)
+ {
+ Future> task = expirations.remove(targetId);
+ if (task != null)
+ task.cancel(false);
+ }
+
+ private void serviceConnect(String targetId, HttpServletRequest httpRequest, HttpServletResponse httpResponse) throws IOException
+ {
+ unschedule(targetId);
+
+ ClientDelegate client = gateway.getClientDelegate(targetId);
+ if (client == null)
+ {
+ // Expired client tries to connect without handshake
+ httpResponse.sendError(HttpServletResponse.SC_UNAUTHORIZED);
+ return;
+ }
+
+ flush(client, httpRequest, httpResponse);
+
+ if (client.isClosed())
+ gateway.removeClientDelegate(targetId);
+ }
+
+ private void expireConnect(ClientDelegate client, long time)
+ {
+ String targetId = client.getTargetId();
+ logger.info("Client with targetId {} missing, last seen {} ms ago, closing it", targetId, System.currentTimeMillis() - time);
+ client.close();
+ // If the client expired, means that it did not connect,
+ // so there no request to resume, and we cleanup here
+ // (while normally this cleanup is done in serviceConnect())
+ unschedule(targetId);
+ gateway.removeClientDelegate(targetId);
+ }
+
+ private void serviceDeliver(String targetId, HttpServletRequest httpRequest, HttpServletResponse httpResponse) throws ServletException, IOException
+ {
+ if (gateway.getClientDelegate(targetId) == null)
+ {
+ // Expired client tries to deliver without handshake
+ httpResponse.sendError(HttpServletResponse.SC_UNAUTHORIZED);
+ return;
+ }
+
+ byte[] body = Utils.read(httpRequest.getInputStream());
+
+ RHTTPResponse response = RHTTPResponse.fromFrameBytes(body);
+
+ ExternalRequest externalRequest = gateway.removeExternalRequest(response.getId());
+ if (externalRequest != null)
+ {
+ externalRequest.respond(response);
+ logger.debug("Deliver request from device {}, gateway request {}, response {}", new Object[] {targetId, externalRequest, response});
+ }
+ else
+ {
+ // We can arrive here for a race with the continuation expiration, which expired just before
+ // the gateway client responded with a valid response; log this case ignore it.
+ logger.debug("Deliver request from device {}, missing gateway request, response {}", targetId, response);
+ }
+ }
+
+ private void serviceDisconnect(String targetId, HttpServletRequest request, HttpServletResponse response)
+ {
+ // Do not remove the ClientDelegate from the gateway here,
+ // since closing the ClientDelegate will resume the connect request
+ // and we remove the ClientDelegate from the gateway there
+ ClientDelegate client = gateway.getClientDelegate(targetId);
+ if (client != null)
+ client.close();
+ }
+
+ private class ClientExpirationTask implements Runnable
+ {
+ private final long time = System.currentTimeMillis();
+ private final ClientDelegate client;
+
+ public ClientExpirationTask(ClientDelegate client)
+ {
+ this.client = client;
+ }
+
+ public void run()
+ {
+ expireConnect(client, time);
+ }
+ }
+}
diff --git a/jetty-rhttp/jetty-rhttp-gateway/src/main/java/org/eclipse/jetty/rhttp/gateway/ExternalRequest.java b/jetty-rhttp/jetty-rhttp-gateway/src/main/java/org/eclipse/jetty/rhttp/gateway/ExternalRequest.java
new file mode 100644
index 00000000000..9b039bac165
--- /dev/null
+++ b/jetty-rhttp/jetty-rhttp-gateway/src/main/java/org/eclipse/jetty/rhttp/gateway/ExternalRequest.java
@@ -0,0 +1,54 @@
+//
+// ========================================================================
+// Copyright (c) 1995-2012 Mort Bay Consulting Pty. Ltd.
+// ------------------------------------------------------------------------
+// All rights reserved. This program and the accompanying materials
+// are made available under the terms of the Eclipse Public License v1.0
+// and Apache License v2.0 which accompanies this distribution.
+//
+// The Eclipse Public License is available at
+// http://www.eclipse.org/legal/epl-v10.html
+//
+// The Apache License v2.0 is available at
+// http://www.opensource.org/licenses/apache2.0.php
+//
+// You may elect to redistribute this code under either of these licenses.
+// ========================================================================
+//
+
+package org.eclipse.jetty.rhttp.gateway;
+
+import java.io.IOException;
+
+import org.eclipse.jetty.rhttp.client.RHTTPRequest;
+import org.eclipse.jetty.rhttp.client.RHTTPResponse;
+
+
+/**
+ * ExternalRequest represent an external request made to the gateway server.
+ * ExternalRequests that arrive to the gateway server are suspended, waiting
+ * for a response from the corresponding gateway client.
+ *
+ * @version $Revision$ $Date$
+ */
+public interface ExternalRequest
+{
+ /**
+ * Suspends this ExternalRequest waiting for a response from the gateway client.
+ * @return true if the ExternalRequest has been suspended, false if the
+ * ExternalRequest has already been responded.
+ */
+ public boolean suspend();
+
+ /**
+ * Responds to the original external request with the response arrived from the gateway client.
+ * @param response the response arrived from the gateway client
+ * @throws IOException if responding to the original external request fails
+ */
+ public void respond(RHTTPResponse response) throws IOException;
+
+ /**
+ * @return the request to be sent to the gateway client
+ */
+ public RHTTPRequest getRequest();
+}
diff --git a/jetty-rhttp/jetty-rhttp-gateway/src/main/java/org/eclipse/jetty/rhttp/gateway/ExternalServlet.java b/jetty-rhttp/jetty-rhttp-gateway/src/main/java/org/eclipse/jetty/rhttp/gateway/ExternalServlet.java
new file mode 100644
index 00000000000..a3585dfcc3b
--- /dev/null
+++ b/jetty-rhttp/jetty-rhttp-gateway/src/main/java/org/eclipse/jetty/rhttp/gateway/ExternalServlet.java
@@ -0,0 +1,88 @@
+//
+// ========================================================================
+// Copyright (c) 1995-2012 Mort Bay Consulting Pty. Ltd.
+// ------------------------------------------------------------------------
+// All rights reserved. This program and the accompanying materials
+// are made available under the terms of the Eclipse Public License v1.0
+// and Apache License v2.0 which accompanies this distribution.
+//
+// The Eclipse Public License is available at
+// http://www.eclipse.org/legal/epl-v10.html
+//
+// The Apache License v2.0 is available at
+// http://www.opensource.org/licenses/apache2.0.php
+//
+// You may elect to redistribute this code under either of these licenses.
+// ========================================================================
+//
+
+package org.eclipse.jetty.rhttp.gateway;
+
+import java.io.IOException;
+
+import javax.servlet.ServletException;
+import javax.servlet.http.HttpServlet;
+import javax.servlet.http.HttpServletRequest;
+import javax.servlet.http.HttpServletResponse;
+
+import org.eclipse.jetty.rhttp.client.RHTTPRequest;
+import org.eclipse.jetty.util.log.Log;
+import org.eclipse.jetty.util.log.Logger;
+
+/**
+ * The servlet that handles external requests.
+ *
+ * @version $Revision$ $Date$
+ */
+public class ExternalServlet extends HttpServlet
+{
+ private final Logger logger = Log.getLogger(getClass().toString());
+ private final Gateway gateway;
+ private TargetIdRetriever targetIdRetriever;
+
+ public ExternalServlet(Gateway gateway, TargetIdRetriever targetIdRetriever)
+ {
+ this.gateway = gateway;
+ this.targetIdRetriever = targetIdRetriever;
+ }
+
+ public TargetIdRetriever getTargetIdRetriever()
+ {
+ return targetIdRetriever;
+ }
+
+ public void setTargetIdRetriever(TargetIdRetriever targetIdRetriever)
+ {
+ this.targetIdRetriever = targetIdRetriever;
+ }
+
+ @Override
+ protected void service(HttpServletRequest httpRequest, HttpServletResponse httpResponse) throws ServletException, IOException
+ {
+ logger.debug("External http request: {}", httpRequest.getRequestURL());
+
+ String targetId = targetIdRetriever.retrieveTargetId(httpRequest);
+ if (targetId == null)
+ throw new ServletException("Invalid request to " + getClass().getSimpleName() + ": " + httpRequest.getRequestURI());
+
+ ClientDelegate client = gateway.getClientDelegate(targetId);
+ if (client == null) throw new ServletException("Client with targetId " + targetId + " is not connected");
+
+ ExternalRequest externalRequest = gateway.newExternalRequest(httpRequest, httpResponse);
+ RHTTPRequest request = externalRequest.getRequest();
+ ExternalRequest existing = gateway.addExternalRequest(request.getId(), externalRequest);
+ assert existing == null;
+ logger.debug("External request {} for device {}", request, targetId);
+
+ boolean delivered = client.enqueue(request);
+ if (delivered)
+ {
+ externalRequest.suspend();
+ }
+ else
+ {
+ // TODO: improve this: we can temporarly queue this request elsewhere and wait for the client to reconnect ?
+ throw new ServletException("Could not enqueue request to client with targetId " + targetId);
+ }
+ }
+}
diff --git a/jetty-rhttp/jetty-rhttp-gateway/src/main/java/org/eclipse/jetty/rhttp/gateway/Gateway.java b/jetty-rhttp/jetty-rhttp-gateway/src/main/java/org/eclipse/jetty/rhttp/gateway/Gateway.java
new file mode 100644
index 00000000000..79b0eec46dd
--- /dev/null
+++ b/jetty-rhttp/jetty-rhttp-gateway/src/main/java/org/eclipse/jetty/rhttp/gateway/Gateway.java
@@ -0,0 +1,99 @@
+//
+// ========================================================================
+// Copyright (c) 1995-2012 Mort Bay Consulting Pty. Ltd.
+// ------------------------------------------------------------------------
+// All rights reserved. This program and the accompanying materials
+// are made available under the terms of the Eclipse Public License v1.0
+// and Apache License v2.0 which accompanies this distribution.
+//
+// The Eclipse Public License is available at
+// http://www.eclipse.org/legal/epl-v10.html
+//
+// The Apache License v2.0 is available at
+// http://www.opensource.org/licenses/apache2.0.php
+//
+// You may elect to redistribute this code under either of these licenses.
+// ========================================================================
+//
+
+package org.eclipse.jetty.rhttp.gateway;
+
+import java.io.IOException;
+
+import javax.servlet.http.HttpServletRequest;
+import javax.servlet.http.HttpServletResponse;
+
+/**
+ * Gateway instances are responsible of holding the state of the gateway server.
+ * The state is composed by:
+ *
+ * - {@link ExternalRequest external requests} that are suspended waiting for the response
+ * - {@link ClientDelegate gateway clients} that are connected with the gateway server
+ *
+ * Instances of this class are created by the {@link GatewayServer}.
+ *
+ * @version $Revision$ $Date$
+ */
+public interface Gateway
+{
+ /**
+ * Returns the {@link ClientDelegate} with the given targetId.
+ * If there is no such ClientDelegate returns null.
+ *
+ * @param targetId the targetId of the ClientDelegate to return
+ * @return the ClientDelegate associated with the given targetId
+ */
+ public ClientDelegate getClientDelegate(String targetId);
+
+ /**
+ * Creates and configures a new {@link ClientDelegate} with the given targetId.
+ * @param targetId the targetId of the ClientDelegate to create
+ * @return a newly created ClientDelegate
+ * @see #addClientDelegate(String, ClientDelegate)
+ */
+ public ClientDelegate newClientDelegate(String targetId);
+
+ /**
+ * Maps the given ClientDelegate to the given targetId.
+ * @param targetId the targetId of the given ClientDelegate
+ * @param client the ClientDelegate to map
+ * @return the previously existing ClientDelegate mapped to the same targetId
+ * @see #removeClientDelegate(String)
+ */
+ public ClientDelegate addClientDelegate(String targetId, ClientDelegate client);
+
+ /**
+ * Removes the {@link ClientDelegate} associated with the given targetId.
+ * @param targetId the targetId of the ClientDelegate to remove
+ * @return the removed ClientDelegate, or null if no ClientDelegate was removed
+ * @see #addClientDelegate(String, ClientDelegate)
+ */
+ public ClientDelegate removeClientDelegate(String targetId);
+
+ /**
+ * Creates a new {@link ExternalRequest} from the given HTTP request and HTTP response.
+ * @param httpRequest the HTTP request of the external request
+ * @param httpResponse the HTTP response of the external request
+ * @return a newly created ExternalRequest
+ * @throws IOException in case of failures creating the ExternalRequest
+ * @see #addExternalRequest(int, ExternalRequest)
+ */
+ public ExternalRequest newExternalRequest(HttpServletRequest httpRequest, HttpServletResponse httpResponse) throws IOException;
+
+ /**
+ * Maps the given ExternalRequest with the given requestId into the gateway state.
+ * @param requestId the id of the ExternalRequest
+ * @param externalRequest the ExternalRequest to map
+ * @return the previously existing ExternalRequest mapped to the same requestId
+ * @see #removeExternalRequest(int)
+ */
+ public ExternalRequest addExternalRequest(int requestId, ExternalRequest externalRequest);
+
+ /**
+ * Removes the ExternalRequest mapped to the given requestId from the gateway state.
+ * @param requestId the id of the ExternalRequest
+ * @return the removed ExternalRequest
+ * @see #addExternalRequest(int, ExternalRequest)
+ */
+ public ExternalRequest removeExternalRequest(int requestId);
+}
diff --git a/jetty-rhttp/jetty-rhttp-gateway/src/main/java/org/eclipse/jetty/rhttp/gateway/GatewayProxyServer.java b/jetty-rhttp/jetty-rhttp-gateway/src/main/java/org/eclipse/jetty/rhttp/gateway/GatewayProxyServer.java
new file mode 100644
index 00000000000..6931636ad9e
--- /dev/null
+++ b/jetty-rhttp/jetty-rhttp-gateway/src/main/java/org/eclipse/jetty/rhttp/gateway/GatewayProxyServer.java
@@ -0,0 +1,228 @@
+//
+// ========================================================================
+// Copyright (c) 1995-2012 Mort Bay Consulting Pty. Ltd.
+// ------------------------------------------------------------------------
+// All rights reserved. This program and the accompanying materials
+// are made available under the terms of the Eclipse Public License v1.0
+// and Apache License v2.0 which accompanies this distribution.
+//
+// The Eclipse Public License is available at
+// http://www.eclipse.org/legal/epl-v10.html
+//
+// The Apache License v2.0 is available at
+// http://www.opensource.org/licenses/apache2.0.php
+//
+// You may elect to redistribute this code under either of these licenses.
+// ========================================================================
+//
+
+package org.eclipse.jetty.rhttp.gateway;
+
+import java.io.ByteArrayOutputStream;
+import java.io.IOException;
+import java.util.HashMap;
+import java.util.Map;
+import java.util.concurrent.TimeUnit;
+
+import javax.servlet.http.HttpServletRequest;
+import javax.servlet.http.HttpServletResponse;
+
+import org.eclipse.jetty.client.Address;
+import org.eclipse.jetty.client.ContentExchange;
+import org.eclipse.jetty.client.HttpClient;
+import org.eclipse.jetty.client.HttpExchange;
+import org.eclipse.jetty.io.Buffer;
+import org.eclipse.jetty.io.ByteArrayBuffer;
+import org.eclipse.jetty.rhttp.client.JettyClient;
+import org.eclipse.jetty.rhttp.client.RHTTPClient;
+import org.eclipse.jetty.rhttp.client.RHTTPListener;
+import org.eclipse.jetty.rhttp.client.RHTTPRequest;
+import org.eclipse.jetty.rhttp.client.RHTTPResponse;
+import org.eclipse.jetty.server.Connector;
+import org.eclipse.jetty.server.nio.SelectChannelConnector;
+import org.eclipse.jetty.util.log.Log;
+import org.eclipse.jetty.util.log.Logger;
+
+/**
+ * This class combines a gateway server and a gateway client to obtain the functionality of a simple proxy server.
+ * This gateway proxy server starts on port 8080 and can be set as http proxy in browsers such as Firefox, and used
+ * to browse the internet.
+ * Its functionality is limited (for example, it only supports http, and not https).
+ * @version $Revision$ $Date$
+ */
+public class GatewayProxyServer
+{
+ private static final Logger logger = Log.getLogger(GatewayProxyServer.class.toString());
+
+ public static void main(String[] args) throws Exception
+ {
+ GatewayServer server = new GatewayServer();
+
+ Connector plainConnector = new SelectChannelConnector();
+ plainConnector.setPort(8080);
+ server.addConnector(plainConnector);
+
+ ((StandardGateway)server.getGateway()).setExternalTimeout(180000);
+ ((StandardGateway)server.getGateway()).setGatewayTimeout(20000);
+ server.setTargetIdRetriever(new ProxyTargetIdRetriever());
+ server.start();
+
+ HttpClient httpClient = new HttpClient();
+ httpClient.setConnectorType(HttpClient.CONNECTOR_SOCKET);
+ httpClient.start();
+
+ RHTTPClient client = new JettyClient(httpClient, new Address("localhost", plainConnector.getPort()), server.getContext().getContextPath() + "/gw", "proxy");
+ client.addListener(new ProxyListener(httpClient, client));
+ client.connect();
+
+ Runtime.getRuntime().addShutdownHook(new Shutdown(server, httpClient, client));
+ logger.info("{} started", GatewayProxyServer.class.getSimpleName());
+ }
+
+ private static class Shutdown extends Thread
+ {
+ private final GatewayServer server;
+ private final HttpClient httpClient;
+ private final RHTTPClient client;
+
+ public Shutdown(GatewayServer server, HttpClient httpClient, RHTTPClient client)
+ {
+ this.server = server;
+ this.httpClient = httpClient;
+ this.client = client;
+ }
+
+ @Override
+ public void run()
+ {
+ try
+ {
+ client.disconnect();
+ httpClient.stop();
+ server.stop();
+ logger.info("{} stopped", GatewayProxyServer.class.getSimpleName());
+ }
+ catch (Exception x)
+ {
+ logger.debug("Exception while stopping " + GatewayProxyServer.class.getSimpleName(), x);
+ }
+ }
+ }
+
+ private static class ProxyListener implements RHTTPListener
+ {
+ private final HttpClient httpClient;
+ private final RHTTPClient client;
+
+ private ProxyListener(HttpClient httpClient, RHTTPClient client)
+ {
+ this.httpClient = httpClient;
+ this.client = client;
+ }
+
+ public void onRequest(RHTTPRequest request) throws Exception
+ {
+ ProxyExchange exchange = new ProxyExchange();
+ Address address = Address.from(request.getHeaders().get("Host"));
+ if (address.getPort() == 0) address = new Address(address.getHost(), 80);
+ exchange.setAddress(address);
+ exchange.setMethod(request.getMethod());
+ exchange.setURI(request.getURI());
+ for (Map.Entry header : request.getHeaders().entrySet())
+ exchange.setRequestHeader(header.getKey(), header.getValue());
+ exchange.setRequestContent(new ByteArrayBuffer(request.getBody()));
+ int status = syncSend(exchange);
+ if (status == HttpExchange.STATUS_COMPLETED)
+ {
+ int statusCode = exchange.getResponseStatus();
+ String statusMessage = exchange.getResponseMessage();
+ Map responseHeaders = exchange.getResponseHeaders();
+ byte[] responseBody = exchange.getResponseBody();
+ RHTTPResponse response = new RHTTPResponse(request.getId(), statusCode, statusMessage, responseHeaders, responseBody);
+ client.deliver(response);
+ }
+ else
+ {
+ int statusCode = HttpServletResponse.SC_SERVICE_UNAVAILABLE;
+ String statusMessage = "Gateway error";
+ HashMap responseHeaders = new HashMap();
+ responseHeaders.put("Connection", "close");
+ byte[] responseBody = new byte[0];
+ RHTTPResponse response = new RHTTPResponse(request.getId(), statusCode, statusMessage, responseHeaders, responseBody);
+ client.deliver(response);
+ }
+ }
+
+ private int syncSend(ProxyExchange exchange) throws Exception
+ {
+ long start = System.nanoTime();
+ httpClient.send(exchange);
+ int status = exchange.waitForDone();
+ long end = System.nanoTime();
+ long millis = TimeUnit.NANOSECONDS.toMillis(end - start);
+ long micros = TimeUnit.NANOSECONDS.toMicros(end - start - TimeUnit.MILLISECONDS.toNanos(millis));
+ logger.debug("Proxied request took {}.{} ms", millis, micros);
+ return status;
+ }
+ }
+
+ private static class ProxyExchange extends ContentExchange
+ {
+ private String responseMessage;
+ private Map responseHeaders = new HashMap();
+ private ByteArrayOutputStream responseBody = new ByteArrayOutputStream();
+
+ private ProxyExchange()
+ {
+ super(true);
+ }
+
+ public String getResponseMessage()
+ {
+ return responseMessage;
+ }
+
+ public Map getResponseHeaders()
+ {
+ return responseHeaders;
+ }
+
+ public byte[] getResponseBody()
+ {
+ return responseBody.toByteArray();
+ }
+
+ @Override
+ protected void onResponseStatus(Buffer version, int code, Buffer message) throws IOException
+ {
+ super.onResponseStatus(version, code, message);
+ this.responseMessage = message.toString("UTF-8");
+ }
+
+ @Override
+ protected void onResponseHeader(Buffer nameBuffer, Buffer valueBuffer) throws IOException
+ {
+ super.onResponseHeader(nameBuffer, valueBuffer);
+ String name = nameBuffer.toString("UTF-8");
+ String value = valueBuffer.toString("UTF-8");
+ // Skip chunked header, since we read the whole body and will not re-chunk it
+ if (!name.equalsIgnoreCase("Transfer-Encoding") || !value.equalsIgnoreCase("chunked"))
+ responseHeaders.put(name, value);
+ }
+
+ @Override
+ protected void onResponseContent(Buffer buffer) throws IOException
+ {
+ responseBody.write(buffer.asArray());
+ super.onResponseContent(buffer);
+ }
+ }
+
+ public static class ProxyTargetIdRetriever implements TargetIdRetriever
+ {
+ public String retrieveTargetId(HttpServletRequest httpRequest)
+ {
+ return "proxy";
+ }
+ }
+}
diff --git a/jetty-rhttp/jetty-rhttp-gateway/src/main/java/org/eclipse/jetty/rhttp/gateway/GatewayServer.java b/jetty-rhttp/jetty-rhttp-gateway/src/main/java/org/eclipse/jetty/rhttp/gateway/GatewayServer.java
new file mode 100644
index 00000000000..b9112d37fc2
--- /dev/null
+++ b/jetty-rhttp/jetty-rhttp-gateway/src/main/java/org/eclipse/jetty/rhttp/gateway/GatewayServer.java
@@ -0,0 +1,171 @@
+//
+// ========================================================================
+// Copyright (c) 1995-2012 Mort Bay Consulting Pty. Ltd.
+// ------------------------------------------------------------------------
+// All rights reserved. This program and the accompanying materials
+// are made available under the terms of the Eclipse Public License v1.0
+// and Apache License v2.0 which accompanies this distribution.
+//
+// The Eclipse Public License is available at
+// http://www.eclipse.org/legal/epl-v10.html
+//
+// The Apache License v2.0 is available at
+// http://www.opensource.org/licenses/apache2.0.php
+//
+// You may elect to redistribute this code under either of these licenses.
+// ========================================================================
+//
+
+package org.eclipse.jetty.rhttp.gateway;
+
+import java.io.File;
+import java.util.ArrayList;
+import java.util.List;
+
+import org.eclipse.jetty.http.MimeTypes;
+import org.eclipse.jetty.rhttp.client.RHTTPClient;
+import org.eclipse.jetty.rhttp.client.RHTTPRequest;
+import org.eclipse.jetty.rhttp.client.RHTTPResponse;
+import org.eclipse.jetty.server.Connector;
+import org.eclipse.jetty.server.Server;
+import org.eclipse.jetty.server.handler.HandlerCollection;
+import org.eclipse.jetty.servlet.DefaultServlet;
+import org.eclipse.jetty.servlet.ServletContextHandler;
+import org.eclipse.jetty.servlet.ServletHolder;
+import org.eclipse.jetty.util.log.Log;
+import org.eclipse.jetty.util.log.Logger;
+
+/**
+ * The gateway server is a server component that acts as intermediary between
+ * external clients which perform requests for resources, and the
+ * resource providers.
+ * The particularity of the gateway server is that the resource providers
+ * connect to the gateway using a comet protocol.
+ * The comet procotol functionality is implemented by a gateway client.
+ * This is quite different from a normal proxy server where it is the proxy that
+ * connects to the resource providers.
+ * Schematically, this is how the gateway server works:
+ *
+ * External Client Gateway Server Gateway Client Resource Provider
+ * | |
+ * | <-- comet req. 1 --- |
+ * | --- ext. req. 1 --> | |
+ * | | --- comet res. 1 --> |
+ * | | <-- comet req. 2 --- |
+ * | | --- ext. req. 1 --> |
+ * |
+ * | | <-- ext. res. 1 --- |
+ * | | <-- ext. res. 1 --- |
+ * | <-- ext. res. 1 --- |
+ *
+ * | --- ext. req. 2 --> |
+ * | | --- comet res. 2 --> |
+ * . . .
+ *
+ * The gateway server is made of two servlets:
+ *
+ * - the external servlet, that handles external requests
+ * - the gateway servlet, that handles the communication with the gateway client
+ *
+ *
+ * External requests are suspended using Jetty continuations until a response for
+ * that request arrives from the resource provider, or a
+ * {@link #getExternalTimeout() configurable timeout} expires.
+ * Comet requests made by the gateway client also expires after a (different)
+ * {@link #getGatewayTimeout() configurable timeout}.
+ * External requests are packed into {@link RHTTPRequest} objects, converted into an
+ * opaque byte array and sent as the body of the comet reponse to the gateway
+ * {@link RHTTPClient}.
+ * The gateway client uses a notification mechanism to alert listeners interested
+ * in external requests that have been forwarded through the gateway. It is up to the
+ * listeners to connect to the resource provider however they like.
+ * When the gateway client receives a response from the resource provider, it packs
+ * the response into a {@link RHTTPResponse} object, converts it into an opaque byte array
+ * and sends it as the body of a normal HTTP request to the gateway server.
+ * It is possible to connect more than one gateway client to a gateway server; each
+ * gateway client is identified by a unique targetId.
+ * External requests must specify a targetId that allows the gateway server to forward
+ * the requests to the specific gateway client; how the targetId is retrieved from an
+ * external request is handled by {@link TargetIdRetriever} implementations.
+ *
+ * @version $Revision$ $Date$
+ */
+public class GatewayServer extends Server
+{
+ public final static String DFT_EXT_PATH="/gw";
+ public final static String DFT_CONNECT_PATH="/__rhttp";
+ private final Logger logger = Log.getLogger(getClass().toString());
+ private final Gateway gateway;
+ private final ServletHolder externalServletHolder;
+ private final ServletHolder connectorServletHolder;
+ private final ServletContextHandler context;
+
+ public GatewayServer()
+ {
+ this("",DFT_EXT_PATH,DFT_CONNECT_PATH,new StandardTargetIdRetriever());
+ }
+
+ public GatewayServer(String contextPath, String externalServletPath,String gatewayServletPath, TargetIdRetriever targetIdRetriever)
+ {
+ HandlerCollection handlers = new HandlerCollection();
+ setHandler(handlers);
+ context = new ServletContextHandler(handlers, contextPath, ServletContextHandler.SESSIONS);
+
+ // Setup the gateway
+ gateway = createGateway();
+
+ // Setup external servlet
+ ExternalServlet externalServlet = new ExternalServlet(gateway, targetIdRetriever);
+ externalServletHolder = new ServletHolder(externalServlet);
+ context.addServlet(externalServletHolder, externalServletPath + "/*");
+ logger.debug("External servlet mapped to {}/*", externalServletPath);
+
+ // Setup gateway servlet
+ ConnectorServlet gatewayServlet = new ConnectorServlet(gateway);
+ connectorServletHolder = new ServletHolder(gatewayServlet);
+ connectorServletHolder.setInitParameter("clientTimeout", "15000");
+ context.addServlet(connectorServletHolder, gatewayServletPath + "/*");
+ logger.debug("Gateway servlet mapped to {}/*", gatewayServletPath);
+ }
+
+ /**
+ * Creates and configures a {@link Gateway} object.
+ * @return the newly created and configured Gateway object.
+ */
+ protected Gateway createGateway()
+ {
+ StandardGateway gateway = new StandardGateway();
+ return gateway;
+ }
+
+ public ServletContextHandler getContext()
+ {
+ return context;
+ }
+
+ public Gateway getGateway()
+ {
+ return gateway;
+ }
+
+ public ServletHolder getExternalServlet()
+ {
+ return externalServletHolder;
+ }
+
+ public ServletHolder getConnectorServlet()
+ {
+ return connectorServletHolder;
+ }
+
+ public void setTargetIdRetriever(TargetIdRetriever retriever)
+ {
+ ((ExternalServlet)externalServletHolder.getServletInstance()).setTargetIdRetriever(retriever);
+ }
+
+ public TargetIdRetriever getTargetIdRetriever()
+ {
+ return ((ExternalServlet)externalServletHolder.getServletInstance()).getTargetIdRetriever();
+ }
+
+}
diff --git a/jetty-rhttp/jetty-rhttp-gateway/src/main/java/org/eclipse/jetty/rhttp/gateway/HostTargetIdRetriever.java b/jetty-rhttp/jetty-rhttp-gateway/src/main/java/org/eclipse/jetty/rhttp/gateway/HostTargetIdRetriever.java
new file mode 100644
index 00000000000..01c107fad37
--- /dev/null
+++ b/jetty-rhttp/jetty-rhttp-gateway/src/main/java/org/eclipse/jetty/rhttp/gateway/HostTargetIdRetriever.java
@@ -0,0 +1,54 @@
+//
+// ========================================================================
+// Copyright (c) 1995-2012 Mort Bay Consulting Pty. Ltd.
+// ------------------------------------------------------------------------
+// All rights reserved. This program and the accompanying materials
+// are made available under the terms of the Eclipse Public License v1.0
+// and Apache License v2.0 which accompanies this distribution.
+//
+// The Eclipse Public License is available at
+// http://www.eclipse.org/legal/epl-v10.html
+//
+// The Apache License v2.0 is available at
+// http://www.opensource.org/licenses/apache2.0.php
+//
+// You may elect to redistribute this code under either of these licenses.
+// ========================================================================
+//
+
+package org.eclipse.jetty.rhttp.gateway;
+
+import javax.servlet.http.HttpServletRequest;
+
+/**
+ * @version $Revision$ $Date$
+ */
+public class HostTargetIdRetriever implements TargetIdRetriever
+{
+ private final String suffix;
+
+ public HostTargetIdRetriever(String suffix)
+ {
+ this.suffix = suffix;
+ }
+
+ public String retrieveTargetId(HttpServletRequest httpRequest)
+ {
+ String host = httpRequest.getHeader("Host");
+ if (host != null)
+ {
+ // Strip the port
+ int colon = host.indexOf(':');
+ if (colon > 0)
+ {
+ host = host.substring(0, colon);
+ }
+
+ if (suffix != null && host.endsWith(suffix))
+ {
+ return host.substring(0, host.length() - suffix.length());
+ }
+ }
+ return host;
+ }
+}
diff --git a/jetty-rhttp/jetty-rhttp-gateway/src/main/java/org/eclipse/jetty/rhttp/gateway/Main.java b/jetty-rhttp/jetty-rhttp-gateway/src/main/java/org/eclipse/jetty/rhttp/gateway/Main.java
new file mode 100644
index 00000000000..bfb7b0804ae
--- /dev/null
+++ b/jetty-rhttp/jetty-rhttp-gateway/src/main/java/org/eclipse/jetty/rhttp/gateway/Main.java
@@ -0,0 +1,128 @@
+//
+// ========================================================================
+// Copyright (c) 1995-2012 Mort Bay Consulting Pty. Ltd.
+// ------------------------------------------------------------------------
+// All rights reserved. This program and the accompanying materials
+// are made available under the terms of the Eclipse Public License v1.0
+// and Apache License v2.0 which accompanies this distribution.
+//
+// The Eclipse Public License is available at
+// http://www.eclipse.org/legal/epl-v10.html
+//
+// The Apache License v2.0 is available at
+// http://www.opensource.org/licenses/apache2.0.php
+//
+// You may elect to redistribute this code under either of these licenses.
+// ========================================================================
+//
+
+package org.eclipse.jetty.rhttp.gateway;
+
+import java.util.HashMap;
+import java.util.Map;
+import java.util.regex.Matcher;
+import java.util.regex.Pattern;
+
+import org.eclipse.jetty.server.Connector;
+import org.eclipse.jetty.server.nio.SelectChannelConnector;
+import org.eclipse.jetty.servlet.DefaultServlet;
+import org.eclipse.jetty.servlet.ServletHolder;
+
+/**
+ * Main class that starts the gateway server.
+ * This class supports the following arguments:
+ *
+ * - --port=<port> specifies the port on which the gateway server listens to, by default 8080
+ * - --retriever=<retriever> specifies the
+ * {@link GatewayServer#setTargetIdRetriever(TargetIdRetriever) target id retriever}
+ * - --resources=<resources file path> specifies the resource file path for the gateway
+ *
+ * Examples
+ * java --port=8080
+ * java --port=8080 --resources=/tmp/gateway-resources
+ * java --port=8080 --retriever=standard
+ * java --port=8080 --retriever=host,.rhttp.example.com
+ * The latter example specifies the {@link HostTargetIdRetriever} with a suffix of .rhttp.example.com
+ *
+ * @see GatewayServer
+ * @version $Revision$ $Date$
+ */
+public class Main
+{
+ private static final String PORT_ARG = "port";
+ private static final String RESOURCES_ARG = "resources";
+ private static final String RETRIEVER_ARG = "retriever";
+
+ public static void main(String[] args) throws Exception
+ {
+ Map arguments = parse(args);
+
+ int port = 8080;
+ if (arguments.containsKey(PORT_ARG))
+ port = (Integer)arguments.get(PORT_ARG);
+
+ String resources = null;
+ if (arguments.containsKey(RESOURCES_ARG))
+ resources = (String)arguments.get(RESOURCES_ARG);
+
+ TargetIdRetriever retriever = null;
+ if (arguments.containsKey(RETRIEVER_ARG))
+ retriever = (TargetIdRetriever)arguments.get(RETRIEVER_ARG);
+
+ GatewayServer server = new GatewayServer();
+
+ Connector connector = new SelectChannelConnector();
+ connector.setPort(port);
+ server.addConnector(connector);
+
+ if (resources != null)
+ {
+ server.getContext().setResourceBase(resources);
+ ServletHolder resourcesServletHolder = server.getContext().addServlet(DefaultServlet.class,"__r/*");
+ resourcesServletHolder.setInitParameter("dirAllowed", "true");
+ }
+
+ if (retriever != null)
+ server.setTargetIdRetriever(retriever);
+
+ server.start();
+ }
+
+ private static Map parse(String[] args)
+ {
+ Map result = new HashMap();
+
+ Pattern pattern = Pattern.compile("--([^=]+)=(.+)");
+ for (String arg : args)
+ {
+ Matcher matcher = pattern.matcher(arg);
+ if (matcher.matches())
+ {
+ String argName = matcher.group(1);
+ if (PORT_ARG.equals(argName))
+ {
+ result.put(PORT_ARG, Integer.parseInt(matcher.group(2)));
+ }
+ else if (RESOURCES_ARG.equals(argName))
+ {
+ String argValue = matcher.group(2);
+ result.put(RESOURCES_ARG, argValue);
+ }
+ else if (RETRIEVER_ARG.equals(argName))
+ {
+ String argValue = matcher.group(2);
+ if (argValue.startsWith("host,"))
+ {
+ String[] typeAndSuffix = argValue.split(",");
+ if (typeAndSuffix.length != 2)
+ throw new IllegalArgumentException("Invalid option " + arg + ", must be of the form --" + RETRIEVER_ARG + "=host,suffix");
+
+ result.put(RETRIEVER_ARG, new HostTargetIdRetriever(typeAndSuffix[1]));
+ }
+ }
+ }
+ }
+
+ return result;
+ }
+}
diff --git a/jetty-rhttp/jetty-rhttp-gateway/src/main/java/org/eclipse/jetty/rhttp/gateway/StandardClientDelegate.java b/jetty-rhttp/jetty-rhttp-gateway/src/main/java/org/eclipse/jetty/rhttp/gateway/StandardClientDelegate.java
new file mode 100644
index 00000000000..1afd85d7652
--- /dev/null
+++ b/jetty-rhttp/jetty-rhttp-gateway/src/main/java/org/eclipse/jetty/rhttp/gateway/StandardClientDelegate.java
@@ -0,0 +1,172 @@
+//
+// ========================================================================
+// Copyright (c) 1995-2012 Mort Bay Consulting Pty. Ltd.
+// ------------------------------------------------------------------------
+// All rights reserved. This program and the accompanying materials
+// are made available under the terms of the Eclipse Public License v1.0
+// and Apache License v2.0 which accompanies this distribution.
+//
+// The Eclipse Public License is available at
+// http://www.eclipse.org/legal/epl-v10.html
+//
+// The Apache License v2.0 is available at
+// http://www.opensource.org/licenses/apache2.0.php
+//
+// You may elect to redistribute this code under either of these licenses.
+// ========================================================================
+//
+
+package org.eclipse.jetty.rhttp.gateway;
+
+import java.io.IOException;
+import java.util.ArrayList;
+import java.util.Collections;
+import java.util.List;
+
+import javax.servlet.http.HttpServletRequest;
+
+import org.eclipse.jetty.continuation.Continuation;
+import org.eclipse.jetty.continuation.ContinuationSupport;
+import org.eclipse.jetty.rhttp.client.RHTTPRequest;
+import org.eclipse.jetty.util.log.Log;
+import org.eclipse.jetty.util.log.Logger;
+
+/**
+ * Default implementation of {@link ClientDelegate}.
+ *
+ * @version $Revision$ $Date$
+ */
+public class StandardClientDelegate implements ClientDelegate
+{
+ private final Logger logger = Log.getLogger(getClass().toString());
+ private final Object lock = new Object();
+ private final List requests = new ArrayList();
+ private final String targetId;
+ private volatile boolean firstFlush = true;
+ private volatile long timeout;
+ private volatile boolean closed;
+ private Continuation continuation;
+
+ public StandardClientDelegate(String targetId)
+ {
+ this.targetId = targetId;
+ }
+
+ public String getTargetId()
+ {
+ return targetId;
+ }
+
+ public long getTimeout()
+ {
+ return timeout;
+ }
+
+ public void setTimeout(long timeout)
+ {
+ this.timeout = timeout;
+ }
+
+ public boolean enqueue(RHTTPRequest request)
+ {
+ if (isClosed())
+ return false;
+
+ synchronized (lock)
+ {
+ requests.add(request);
+ resume();
+ }
+
+ return true;
+ }
+
+ private void resume()
+ {
+ synchronized (lock)
+ {
+ // Continuation may be null in several cases:
+ // 1. there always is something to deliver so we never suspend
+ // 2. concurrent calls to add() and close()
+ // 3. concurrent close() with a long poll that expired
+ // 4. concurrent close() with a long poll that resumed
+ if (continuation != null)
+ {
+ continuation.resume();
+ // Null the continuation, as there is no point is resuming multiple times
+ continuation = null;
+ }
+ }
+ }
+
+ public List process(HttpServletRequest httpRequest) throws IOException
+ {
+ // We want to respond in the following cases:
+ // 1. It's the first time we process: the client will wait for a response before issuing another connect.
+ // 2. The client disconnected, so we want to return from this connect before it times out.
+ // 3. We've been woken up because there are responses to send.
+ // 4. The continuation was suspended but timed out.
+ // The timeout case is different from a non-first connect, in that we want to return
+ // a (most of the times empty) response and we do not want to wait again.
+ // The order of these if statements is important, as the continuation timed out only if
+ // the client is not closed and there are no responses to send
+ List result = Collections.emptyList();
+ if (firstFlush)
+ {
+ firstFlush = false;
+ logger.debug("Connect request (first) from device {}, delivering requests {}", targetId, result);
+ }
+ else
+ {
+ // Synchronization is crucial here, since we don't want to suspend if there is something to deliver
+ synchronized (lock)
+ {
+ int size = requests.size();
+ if (size > 0)
+ {
+ assert continuation == null;
+ result = new ArrayList(size);
+ result.addAll(requests);
+ requests.clear();
+ logger.debug("Connect request (resumed) from device {}, delivering requests {}", targetId, result);
+ }
+ else
+ {
+ if (continuation != null)
+ {
+ continuation = null;
+ logger.debug("Connect request (expired) from device {}, delivering requests {}", targetId, result);
+ }
+ else
+ {
+ if (isClosed())
+ {
+ logger.debug("Connect request (closed) from device {}, delivering requests {}", targetId, result);
+ }
+ else
+ {
+ // Here we need to suspend
+ continuation = ContinuationSupport.getContinuation(httpRequest);
+ continuation.setTimeout(getTimeout());
+ continuation.suspend();
+ result = null;
+ logger.debug("Connect request (suspended) from device {}", targetId);
+ }
+ }
+ }
+ }
+ }
+ return result;
+ }
+
+ public void close()
+ {
+ closed = true;
+ resume();
+ }
+
+ public boolean isClosed()
+ {
+ return closed;
+ }
+}
diff --git a/jetty-rhttp/jetty-rhttp-gateway/src/main/java/org/eclipse/jetty/rhttp/gateway/StandardExternalRequest.java b/jetty-rhttp/jetty-rhttp-gateway/src/main/java/org/eclipse/jetty/rhttp/gateway/StandardExternalRequest.java
new file mode 100644
index 00000000000..4f905b87e4a
--- /dev/null
+++ b/jetty-rhttp/jetty-rhttp-gateway/src/main/java/org/eclipse/jetty/rhttp/gateway/StandardExternalRequest.java
@@ -0,0 +1,189 @@
+//
+// ========================================================================
+// Copyright (c) 1995-2012 Mort Bay Consulting Pty. Ltd.
+// ------------------------------------------------------------------------
+// All rights reserved. This program and the accompanying materials
+// are made available under the terms of the Eclipse Public License v1.0
+// and Apache License v2.0 which accompanies this distribution.
+//
+// The Eclipse Public License is available at
+// http://www.eclipse.org/legal/epl-v10.html
+//
+// The Apache License v2.0 is available at
+// http://www.opensource.org/licenses/apache2.0.php
+//
+// You may elect to redistribute this code under either of these licenses.
+// ========================================================================
+//
+
+package org.eclipse.jetty.rhttp.gateway;
+
+import java.io.IOException;
+import java.util.Map;
+
+import javax.servlet.ServletOutputStream;
+import javax.servlet.http.HttpServletRequest;
+import javax.servlet.http.HttpServletResponse;
+
+import org.eclipse.jetty.continuation.Continuation;
+import org.eclipse.jetty.continuation.ContinuationListener;
+import org.eclipse.jetty.continuation.ContinuationSupport;
+import org.eclipse.jetty.rhttp.client.RHTTPRequest;
+import org.eclipse.jetty.rhttp.client.RHTTPResponse;
+import org.eclipse.jetty.util.log.Log;
+import org.eclipse.jetty.util.log.Logger;
+
+/**
+ * Default implementation of {@link ExternalRequest}.
+ *
+ * @version $Revision$ $Date$
+ */
+public class StandardExternalRequest implements ExternalRequest
+{
+ private final Logger logger = Log.getLogger(getClass().toString());
+ private final RHTTPRequest request;
+ private final HttpServletRequest httpRequest;
+ private final HttpServletResponse httpResponse;
+ private final Gateway gateway;
+ private final Object lock = new Object();
+ private volatile long timeout;
+ private Continuation continuation;
+ private boolean responded;
+
+ public StandardExternalRequest(RHTTPRequest request, HttpServletRequest httpRequest, HttpServletResponse httpResponse, Gateway gateway)
+ {
+ this.request = request;
+ this.httpRequest = httpRequest;
+ this.httpResponse = httpResponse;
+ this.gateway = gateway;
+ }
+
+ public long getTimeout()
+ {
+ return timeout;
+ }
+
+ public void setTimeout(long timeout)
+ {
+ this.timeout = timeout;
+ }
+
+ public boolean suspend()
+ {
+ synchronized (lock)
+ {
+ // We suspend only if we have no responded yet
+ if (!responded)
+ {
+ assert continuation == null;
+ continuation = ContinuationSupport.getContinuation(httpRequest);
+ continuation.setTimeout(getTimeout());
+ continuation.addContinuationListener(new TimeoutListener());
+ continuation.suspend(httpResponse);
+ logger.debug("Request {} suspended", getRequest());
+ }
+ else
+ {
+ logger.debug("Request {} already responded", getRequest());
+ }
+ return !responded;
+ }
+ }
+
+ public void respond(RHTTPResponse response) throws IOException
+ {
+ responseCompleted(response);
+ }
+
+ private void responseCompleted(RHTTPResponse response) throws IOException
+ {
+ synchronized (lock)
+ {
+ // Could be that we complete exactly when the response is being expired
+ if (!responded)
+ {
+ httpResponse.setStatus(response.getStatusCode());
+
+ for (Map.Entry header : response.getHeaders().entrySet())
+ httpResponse.setHeader(header.getKey(), header.getValue());
+
+ ServletOutputStream output = httpResponse.getOutputStream();
+ output.write(response.getBody());
+ output.flush();
+
+ // It may happen that the continuation is null,
+ // because the response arrived before we had the chance to suspend
+ if (continuation != null)
+ {
+ continuation.complete();
+ continuation = null;
+ }
+
+ // Mark as responded, so we know we don't have to suspend
+ // or respond with an expired response
+ responded = true;
+
+ if (logger.isDebugEnabled())
+ {
+ String eol = System.getProperty("line.separator");
+ logger.debug("Request {} responded {}{}{}{}{}", new Object[]{request, response, eol, request.toLongString(), eol, response.toLongString()});
+ }
+ }
+ }
+ }
+
+ private void responseExpired() throws IOException
+ {
+ synchronized (lock)
+ {
+ // Could be that we expired exactly when the response is being completed
+ if (!responded)
+ {
+ httpResponse.sendError(HttpServletResponse.SC_GATEWAY_TIMEOUT, "Gateway Time-out");
+
+ continuation.complete();
+ continuation = null;
+
+ // Mark as responded, so we know we don't have to respond with a completed response
+ responded = true;
+
+ logger.debug("Request {} expired", getRequest());
+ }
+ }
+ }
+
+ public RHTTPRequest getRequest()
+ {
+ return request;
+ }
+
+ @Override
+ public String toString()
+ {
+ return request.toString();
+ }
+
+ private class TimeoutListener implements ContinuationListener
+ {
+ public void onComplete(Continuation continuation)
+ {
+ }
+
+ public void onTimeout(Continuation continuation)
+ {
+ ExternalRequest externalRequest = gateway.removeExternalRequest(getRequest().getId());
+ // The gateway request can be null for a race with delivery
+ if (externalRequest != null)
+ {
+ try
+ {
+ responseExpired();
+ }
+ catch (Exception x)
+ {
+ logger.warn("Request " + getRequest() + " expired but failed", x);
+ }
+ }
+ }
+ }
+}
diff --git a/jetty-rhttp/jetty-rhttp-gateway/src/main/java/org/eclipse/jetty/rhttp/gateway/StandardGateway.java b/jetty-rhttp/jetty-rhttp-gateway/src/main/java/org/eclipse/jetty/rhttp/gateway/StandardGateway.java
new file mode 100644
index 00000000000..871aea6c85a
--- /dev/null
+++ b/jetty-rhttp/jetty-rhttp-gateway/src/main/java/org/eclipse/jetty/rhttp/gateway/StandardGateway.java
@@ -0,0 +1,131 @@
+//
+// ========================================================================
+// Copyright (c) 1995-2012 Mort Bay Consulting Pty. Ltd.
+// ------------------------------------------------------------------------
+// All rights reserved. This program and the accompanying materials
+// are made available under the terms of the Eclipse Public License v1.0
+// and Apache License v2.0 which accompanies this distribution.
+//
+// The Eclipse Public License is available at
+// http://www.eclipse.org/legal/epl-v10.html
+//
+// The Apache License v2.0 is available at
+// http://www.opensource.org/licenses/apache2.0.php
+//
+// You may elect to redistribute this code under either of these licenses.
+// ========================================================================
+//
+
+package org.eclipse.jetty.rhttp.gateway;
+
+import java.io.IOException;
+import java.util.Enumeration;
+import java.util.HashMap;
+import java.util.Map;
+import java.util.concurrent.ConcurrentHashMap;
+import java.util.concurrent.ConcurrentMap;
+import java.util.concurrent.atomic.AtomicInteger;
+
+import javax.servlet.http.HttpServletRequest;
+import javax.servlet.http.HttpServletResponse;
+
+import org.eclipse.jetty.rhttp.client.RHTTPRequest;
+import org.eclipse.jetty.util.log.Log;
+import org.eclipse.jetty.util.log.Logger;
+
+/**
+ * Default implementation of {@link Gateway}.
+ *
+ * @version $Revision$ $Date$
+ */
+public class StandardGateway implements Gateway
+{
+ private final Logger logger = Log.getLogger(getClass().toString());
+ private final ConcurrentMap clients = new ConcurrentHashMap();
+ private final ConcurrentMap requests = new ConcurrentHashMap();
+ private final AtomicInteger requestIds = new AtomicInteger();
+ private volatile long gatewayTimeout=20000;
+ private volatile long externalTimeout=60000;
+
+ public long getGatewayTimeout()
+ {
+ return gatewayTimeout;
+ }
+
+ public void setGatewayTimeout(long timeout)
+ {
+ this.gatewayTimeout = timeout;
+ }
+
+ public long getExternalTimeout()
+ {
+ return externalTimeout;
+ }
+
+ public void setExternalTimeout(long externalTimeout)
+ {
+ this.externalTimeout = externalTimeout;
+ }
+
+ public ClientDelegate getClientDelegate(String targetId)
+ {
+ return clients.get(targetId);
+ }
+
+ public ClientDelegate newClientDelegate(String targetId)
+ {
+ StandardClientDelegate client = new StandardClientDelegate(targetId);
+ client.setTimeout(getGatewayTimeout());
+ return client;
+ }
+
+ public ClientDelegate addClientDelegate(String targetId, ClientDelegate client)
+ {
+ return clients.putIfAbsent(targetId, client);
+ }
+
+ public ClientDelegate removeClientDelegate(String targetId)
+ {
+ return clients.remove(targetId);
+ }
+
+ public ExternalRequest newExternalRequest(HttpServletRequest httpRequest, HttpServletResponse httpResponse) throws IOException
+ {
+ int requestId = requestIds.incrementAndGet();
+ RHTTPRequest request = convertHttpRequest(requestId, httpRequest);
+ StandardExternalRequest gatewayRequest = new StandardExternalRequest(request, httpRequest, httpResponse, this);
+ gatewayRequest.setTimeout(getExternalTimeout());
+ return gatewayRequest;
+ }
+
+ protected RHTTPRequest convertHttpRequest(int requestId, HttpServletRequest httpRequest) throws IOException
+ {
+ Map headers = new HashMap();
+ for (Enumeration headerNames = httpRequest.getHeaderNames(); headerNames.hasMoreElements();)
+ {
+ String name = (String)headerNames.nextElement();
+ // TODO: improve by supporting getHeaders(name)
+ String value = httpRequest.getHeader(name);
+ headers.put(name, value);
+ }
+
+ byte[] body = Utils.read(httpRequest.getInputStream());
+ return new RHTTPRequest(requestId, httpRequest.getMethod(), httpRequest.getRequestURI(), headers, body);
+ }
+
+ public ExternalRequest addExternalRequest(int requestId, ExternalRequest externalRequest)
+ {
+ ExternalRequest existing = requests.putIfAbsent(requestId, externalRequest);
+ if (existing == null)
+ logger.debug("Added external request {}/{} - {}", new Object[]{requestId, requests.size(), externalRequest});
+ return existing;
+ }
+
+ public ExternalRequest removeExternalRequest(int requestId)
+ {
+ ExternalRequest externalRequest = requests.remove(requestId);
+ if (externalRequest != null)
+ logger.debug("Removed external request {}/{} - {}", new Object[]{requestId, requests.size(), externalRequest});
+ return externalRequest;
+ }
+}
diff --git a/jetty-rhttp/jetty-rhttp-gateway/src/main/java/org/eclipse/jetty/rhttp/gateway/StandardTargetIdRetriever.java b/jetty-rhttp/jetty-rhttp-gateway/src/main/java/org/eclipse/jetty/rhttp/gateway/StandardTargetIdRetriever.java
new file mode 100644
index 00000000000..114747f2f5a
--- /dev/null
+++ b/jetty-rhttp/jetty-rhttp-gateway/src/main/java/org/eclipse/jetty/rhttp/gateway/StandardTargetIdRetriever.java
@@ -0,0 +1,40 @@
+//
+// ========================================================================
+// Copyright (c) 1995-2012 Mort Bay Consulting Pty. Ltd.
+// ------------------------------------------------------------------------
+// All rights reserved. This program and the accompanying materials
+// are made available under the terms of the Eclipse Public License v1.0
+// and Apache License v2.0 which accompanies this distribution.
+//
+// The Eclipse Public License is available at
+// http://www.eclipse.org/legal/epl-v10.html
+//
+// The Apache License v2.0 is available at
+// http://www.opensource.org/licenses/apache2.0.php
+//
+// You may elect to redistribute this code under either of these licenses.
+// ========================================================================
+//
+
+package org.eclipse.jetty.rhttp.gateway;
+
+import javax.servlet.http.HttpServletRequest;
+
+/**
+ * This implementation retrieves the targetId from the request URI following this pattern:
+ *
+ * /contextPath/servletPath/<targetId>/other/paths
+ *
+ * @version $Revision$ $Date$
+ */
+public class StandardTargetIdRetriever implements TargetIdRetriever
+{
+ public String retrieveTargetId(HttpServletRequest httpRequest)
+ {
+ String uri = httpRequest.getRequestURI();
+ String path = uri.substring(httpRequest.getServletPath().length());
+ String[] segments = path.split("/");
+ if (segments.length < 2) return null;
+ return segments[1];
+ }
+}
diff --git a/jetty-rhttp/jetty-rhttp-gateway/src/main/java/org/eclipse/jetty/rhttp/gateway/TargetIdRetriever.java b/jetty-rhttp/jetty-rhttp-gateway/src/main/java/org/eclipse/jetty/rhttp/gateway/TargetIdRetriever.java
new file mode 100644
index 00000000000..2348d3c1993
--- /dev/null
+++ b/jetty-rhttp/jetty-rhttp-gateway/src/main/java/org/eclipse/jetty/rhttp/gateway/TargetIdRetriever.java
@@ -0,0 +1,40 @@
+//
+// ========================================================================
+// Copyright (c) 1995-2012 Mort Bay Consulting Pty. Ltd.
+// ------------------------------------------------------------------------
+// All rights reserved. This program and the accompanying materials
+// are made available under the terms of the Eclipse Public License v1.0
+// and Apache License v2.0 which accompanies this distribution.
+//
+// The Eclipse Public License is available at
+// http://www.eclipse.org/legal/epl-v10.html
+//
+// The Apache License v2.0 is available at
+// http://www.opensource.org/licenses/apache2.0.php
+//
+// You may elect to redistribute this code under either of these licenses.
+// ========================================================================
+//
+
+package org.eclipse.jetty.rhttp.gateway;
+
+import javax.servlet.http.HttpServletRequest;
+
+/**
+ * Implementations should retrieve a targetId from an external request.
+ * Implementations of this class may return a fixed value, or inspect the request
+ * looking for URL patterns (e.g. "/<targetId>/resource.jsp"), or looking for request
+ * parameters (e.g. "/resource.jsp?targetId=<targetId>), or looking for virtual host
+ * naming patterns (e.g. "http://<targetId>.host.com/resource.jsp"), etc.
+ *
+ * @version $Revision$ $Date$
+ */
+public interface TargetIdRetriever
+{
+ /**
+ * Extracts and returns the targetId.
+ * @param httpRequest the external request from where the targetId could be extracted
+ * @return the extracted targetId
+ */
+ public String retrieveTargetId(HttpServletRequest httpRequest);
+}
diff --git a/jetty-rhttp/jetty-rhttp-gateway/src/main/java/org/eclipse/jetty/rhttp/gateway/Utils.java b/jetty-rhttp/jetty-rhttp-gateway/src/main/java/org/eclipse/jetty/rhttp/gateway/Utils.java
new file mode 100644
index 00000000000..b9361fe750b
--- /dev/null
+++ b/jetty-rhttp/jetty-rhttp-gateway/src/main/java/org/eclipse/jetty/rhttp/gateway/Utils.java
@@ -0,0 +1,40 @@
+//
+// ========================================================================
+// Copyright (c) 1995-2012 Mort Bay Consulting Pty. Ltd.
+// ------------------------------------------------------------------------
+// All rights reserved. This program and the accompanying materials
+// are made available under the terms of the Eclipse Public License v1.0
+// and Apache License v2.0 which accompanies this distribution.
+//
+// The Eclipse Public License is available at
+// http://www.eclipse.org/legal/epl-v10.html
+//
+// The Apache License v2.0 is available at
+// http://www.opensource.org/licenses/apache2.0.php
+//
+// You may elect to redistribute this code under either of these licenses.
+// ========================================================================
+//
+
+package org.eclipse.jetty.rhttp.gateway;
+
+import java.io.ByteArrayOutputStream;
+import java.io.IOException;
+import java.io.InputStream;
+
+/**
+ * @version $Revision$ $Date$
+ */
+class Utils
+{
+ static byte[] read(InputStream input) throws IOException
+ {
+ ByteArrayOutputStream body = new ByteArrayOutputStream();
+ byte[] buffer = new byte[1024];
+ int read;
+ while ((read = input.read(buffer)) >= 0)
+ body.write(buffer, 0, read);
+ body.close();
+ return body.toByteArray();
+ }
+}
diff --git a/jetty-rhttp/jetty-rhttp-gateway/src/test/java/org/eclipse/jetty/rhttp/gateway/ClientTimeoutTest.java b/jetty-rhttp/jetty-rhttp-gateway/src/test/java/org/eclipse/jetty/rhttp/gateway/ClientTimeoutTest.java
new file mode 100644
index 00000000000..1021d628fbf
--- /dev/null
+++ b/jetty-rhttp/jetty-rhttp-gateway/src/test/java/org/eclipse/jetty/rhttp/gateway/ClientTimeoutTest.java
@@ -0,0 +1,115 @@
+//
+// ========================================================================
+// Copyright (c) 1995-2012 Mort Bay Consulting Pty. Ltd.
+// ------------------------------------------------------------------------
+// All rights reserved. This program and the accompanying materials
+// are made available under the terms of the Eclipse Public License v1.0
+// and Apache License v2.0 which accompanies this distribution.
+//
+// The Eclipse Public License is available at
+// http://www.eclipse.org/legal/epl-v10.html
+//
+// The Apache License v2.0 is available at
+// http://www.opensource.org/licenses/apache2.0.php
+//
+// You may elect to redistribute this code under either of these licenses.
+// ========================================================================
+//
+
+package org.eclipse.jetty.rhttp.gateway;
+
+import java.util.concurrent.CountDownLatch;
+import java.util.concurrent.TimeUnit;
+import java.util.concurrent.atomic.AtomicInteger;
+
+import junit.framework.TestCase;
+
+import org.eclipse.jetty.client.Address;
+import org.eclipse.jetty.client.HttpClient;
+import org.eclipse.jetty.rhttp.client.ClientListener;
+import org.eclipse.jetty.rhttp.client.JettyClient;
+import org.eclipse.jetty.rhttp.client.RHTTPClient;
+import org.eclipse.jetty.rhttp.gateway.GatewayServer;
+import org.eclipse.jetty.rhttp.gateway.StandardGateway;
+import org.eclipse.jetty.server.Connector;
+import org.eclipse.jetty.server.nio.SelectChannelConnector;
+
+
+/**
+ * @version $Revision$ $Date$
+ */
+public class ClientTimeoutTest extends TestCase
+{
+ public void testClientTimeout() throws Exception
+ {
+ GatewayServer server = new GatewayServer();
+ Connector connector = new SelectChannelConnector();
+ server.addConnector(connector);
+ final long clientTimeout = 2000L;
+ server.getConnectorServlet().setInitParameter("clientTimeout",""+clientTimeout);
+ final long gatewayTimeout = 4000L;
+ ((StandardGateway)server.getGateway()).setGatewayTimeout(gatewayTimeout);
+ server.start();
+ try
+ {
+ Address address = new Address("localhost", connector.getLocalPort());
+
+ HttpClient httpClient = new HttpClient();
+ httpClient.setConnectorType(HttpClient.CONNECTOR_SELECT_CHANNEL);
+ httpClient.start();
+ try
+ {
+ String targetId = "1";
+ final RHTTPClient client = new JettyClient(httpClient, address, server.getContext().getContextPath()+GatewayServer.DFT_CONNECT_PATH, targetId)
+ {
+ private final AtomicInteger connects = new AtomicInteger();
+
+ @Override
+ protected void asyncConnect()
+ {
+ if (connects.incrementAndGet() == 2)
+ {
+ try
+ {
+ // Wait here instead of connecting, so that the client expires on the server
+ Thread.sleep(clientTimeout * 2);
+ }
+ catch (InterruptedException x)
+ {
+ throw new RuntimeException(x);
+ }
+ }
+ super.asyncConnect();
+ }
+ };
+
+ final CountDownLatch connectLatch = new CountDownLatch(1);
+ client.addClientListener(new ClientListener.Adapter()
+ {
+ @Override
+ public void connectRequired()
+ {
+ connectLatch.countDown();
+ }
+ });
+ client.connect();
+ try
+ {
+ assertTrue(connectLatch.await(gatewayTimeout + clientTimeout * 3, TimeUnit.MILLISECONDS));
+ }
+ finally
+ {
+ client.disconnect();
+ }
+ }
+ finally
+ {
+ httpClient.stop();
+ }
+ }
+ finally
+ {
+ server.stop();
+ }
+ }
+}
diff --git a/jetty-rhttp/jetty-rhttp-gateway/src/test/java/org/eclipse/jetty/rhttp/gateway/DisconnectClientTest.java b/jetty-rhttp/jetty-rhttp-gateway/src/test/java/org/eclipse/jetty/rhttp/gateway/DisconnectClientTest.java
new file mode 100644
index 00000000000..6c8f9d03701
--- /dev/null
+++ b/jetty-rhttp/jetty-rhttp-gateway/src/test/java/org/eclipse/jetty/rhttp/gateway/DisconnectClientTest.java
@@ -0,0 +1,93 @@
+//
+// ========================================================================
+// Copyright (c) 1995-2012 Mort Bay Consulting Pty. Ltd.
+// ------------------------------------------------------------------------
+// All rights reserved. This program and the accompanying materials
+// are made available under the terms of the Eclipse Public License v1.0
+// and Apache License v2.0 which accompanies this distribution.
+//
+// The Eclipse Public License is available at
+// http://www.eclipse.org/legal/epl-v10.html
+//
+// The Apache License v2.0 is available at
+// http://www.opensource.org/licenses/apache2.0.php
+//
+// You may elect to redistribute this code under either of these licenses.
+// ========================================================================
+//
+
+package org.eclipse.jetty.rhttp.gateway;
+
+import java.io.IOException;
+import java.util.concurrent.CountDownLatch;
+import java.util.concurrent.TimeUnit;
+
+import junit.framework.TestCase;
+
+import org.eclipse.jetty.client.Address;
+import org.eclipse.jetty.client.HttpClient;
+import org.eclipse.jetty.rhttp.client.JettyClient;
+import org.eclipse.jetty.rhttp.client.RHTTPClient;
+import org.eclipse.jetty.rhttp.gateway.GatewayServer;
+import org.eclipse.jetty.server.Connector;
+import org.eclipse.jetty.server.nio.SelectChannelConnector;
+
+
+/**
+ * @version $Revision$ $Date$
+ */
+public class DisconnectClientTest extends TestCase
+{
+ public void testDifferentClientDisconnects() throws Exception
+ {
+ GatewayServer server = new GatewayServer();
+ Connector connector = new SelectChannelConnector();
+ server.addConnector(connector);
+ server.start();
+ try
+ {
+ Address address = new Address("localhost", connector.getLocalPort());
+
+ HttpClient httpClient = new HttpClient();
+ httpClient.setConnectorType(HttpClient.CONNECTOR_SELECT_CHANNEL);
+ httpClient.start();
+ try
+ {
+ final CountDownLatch latch = new CountDownLatch(1);
+ String targetId = "1";
+ final RHTTPClient client1 = new JettyClient(httpClient, address, server.getContext().getContextPath()+GatewayServer.DFT_CONNECT_PATH, targetId)
+ {
+ @Override
+ protected void connectComplete(byte[] responseContent) throws IOException
+ {
+ // If the other client can disconnect this one, this method is called soon after it disconnected
+ latch.countDown();
+ super.connectComplete(responseContent);
+ }
+ };
+ client1.connect();
+ try
+ {
+ final RHTTPClient client2 = new JettyClient(httpClient, address, server.getContext().getContextPath()+GatewayServer.DFT_CONNECT_PATH, targetId);
+ // Disconnect client 2, this should not disconnect client1
+ client2.disconnect();
+
+ // We want the await() to expire, it means it has not disconnected
+ assertFalse(latch.await(1000, TimeUnit.MILLISECONDS));
+ }
+ finally
+ {
+ client1.disconnect();
+ }
+ }
+ finally
+ {
+ httpClient.stop();
+ }
+ }
+ finally
+ {
+ server.stop();
+ }
+ }
+}
diff --git a/jetty-rhttp/jetty-rhttp-gateway/src/test/java/org/eclipse/jetty/rhttp/gateway/DuplicateClientTest.java b/jetty-rhttp/jetty-rhttp-gateway/src/test/java/org/eclipse/jetty/rhttp/gateway/DuplicateClientTest.java
new file mode 100644
index 00000000000..50eb45ad0bc
--- /dev/null
+++ b/jetty-rhttp/jetty-rhttp-gateway/src/test/java/org/eclipse/jetty/rhttp/gateway/DuplicateClientTest.java
@@ -0,0 +1,84 @@
+//
+// ========================================================================
+// Copyright (c) 1995-2012 Mort Bay Consulting Pty. Ltd.
+// ------------------------------------------------------------------------
+// All rights reserved. This program and the accompanying materials
+// are made available under the terms of the Eclipse Public License v1.0
+// and Apache License v2.0 which accompanies this distribution.
+//
+// The Eclipse Public License is available at
+// http://www.eclipse.org/legal/epl-v10.html
+//
+// The Apache License v2.0 is available at
+// http://www.opensource.org/licenses/apache2.0.php
+//
+// You may elect to redistribute this code under either of these licenses.
+// ========================================================================
+//
+
+package org.eclipse.jetty.rhttp.gateway;
+
+import java.io.IOException;
+
+import junit.framework.TestCase;
+
+import org.eclipse.jetty.client.Address;
+import org.eclipse.jetty.client.HttpClient;
+import org.eclipse.jetty.rhttp.client.JettyClient;
+import org.eclipse.jetty.rhttp.client.RHTTPClient;
+import org.eclipse.jetty.rhttp.gateway.GatewayServer;
+import org.eclipse.jetty.server.Connector;
+import org.eclipse.jetty.server.nio.SelectChannelConnector;
+
+
+/**
+ * @version $Revision$ $Date$
+ */
+public class DuplicateClientTest extends TestCase
+{
+ public void testDuplicateClient() throws Exception
+ {
+ GatewayServer server = new GatewayServer();
+ Connector connector = new SelectChannelConnector();
+ server.addConnector(connector);
+ server.start();
+ try
+ {
+ Address address = new Address("localhost", connector.getLocalPort());
+
+ HttpClient httpClient = new HttpClient();
+ httpClient.setConnectorType(HttpClient.CONNECTOR_SELECT_CHANNEL);
+ httpClient.start();
+ try
+ {
+ String targetId = "1";
+ final RHTTPClient client1 = new JettyClient(httpClient, address, server.getContext().getContextPath()+GatewayServer.DFT_CONNECT_PATH, targetId);
+ client1.connect();
+ try
+ {
+ final RHTTPClient client2 = new JettyClient(httpClient, address, server.getContext().getContextPath()+GatewayServer.DFT_CONNECT_PATH, targetId);
+ try
+ {
+ client2.connect();
+ fail();
+ }
+ catch (IOException x)
+ {
+ }
+ }
+ finally
+ {
+ client1.disconnect();
+ }
+ }
+ finally
+ {
+ httpClient.stop();
+ }
+ }
+ finally
+ {
+ server.stop();
+ }
+ }
+}
diff --git a/jetty-rhttp/jetty-rhttp-gateway/src/test/java/org/eclipse/jetty/rhttp/gateway/ExternalRequestNotSuspendedTest.java b/jetty-rhttp/jetty-rhttp-gateway/src/test/java/org/eclipse/jetty/rhttp/gateway/ExternalRequestNotSuspendedTest.java
new file mode 100644
index 00000000000..0cd05d3c0f5
--- /dev/null
+++ b/jetty-rhttp/jetty-rhttp-gateway/src/test/java/org/eclipse/jetty/rhttp/gateway/ExternalRequestNotSuspendedTest.java
@@ -0,0 +1,186 @@
+//
+// ========================================================================
+// Copyright (c) 1995-2012 Mort Bay Consulting Pty. Ltd.
+// ------------------------------------------------------------------------
+// All rights reserved. This program and the accompanying materials
+// are made available under the terms of the Eclipse Public License v1.0
+// and Apache License v2.0 which accompanies this distribution.
+//
+// The Eclipse Public License is available at
+// http://www.eclipse.org/legal/epl-v10.html
+//
+// The Apache License v2.0 is available at
+// http://www.opensource.org/licenses/apache2.0.php
+//
+// You may elect to redistribute this code under either of these licenses.
+// ========================================================================
+//
+
+package org.eclipse.jetty.rhttp.gateway;
+
+import java.io.IOException;
+import java.net.URLEncoder;
+import java.util.HashMap;
+import java.util.concurrent.CountDownLatch;
+import java.util.concurrent.atomic.AtomicBoolean;
+import java.util.concurrent.atomic.AtomicReference;
+
+import javax.servlet.http.HttpServletRequest;
+import javax.servlet.http.HttpServletResponse;
+
+import junit.framework.TestCase;
+
+import org.eclipse.jetty.client.Address;
+import org.eclipse.jetty.client.ContentExchange;
+import org.eclipse.jetty.client.HttpClient;
+import org.eclipse.jetty.client.HttpExchange;
+import org.eclipse.jetty.http.HttpMethods;
+import org.eclipse.jetty.io.ByteArrayBuffer;
+import org.eclipse.jetty.rhttp.client.JettyClient;
+import org.eclipse.jetty.rhttp.client.RHTTPClient;
+import org.eclipse.jetty.rhttp.client.RHTTPListener;
+import org.eclipse.jetty.rhttp.client.RHTTPRequest;
+import org.eclipse.jetty.rhttp.client.RHTTPResponse;
+import org.eclipse.jetty.rhttp.gateway.ExternalRequest;
+import org.eclipse.jetty.rhttp.gateway.Gateway;
+import org.eclipse.jetty.rhttp.gateway.GatewayServer;
+import org.eclipse.jetty.rhttp.gateway.StandardGateway;
+import org.eclipse.jetty.server.nio.SelectChannelConnector;
+
+
+/**
+ * @version $Revision$ $Date$
+ */
+public class ExternalRequestNotSuspendedTest extends TestCase
+{
+ public void testExternalRequestNotSuspended() throws Exception
+ {
+ final CountDownLatch respondLatch = new CountDownLatch(1);
+ final CountDownLatch suspendLatch = new CountDownLatch(1);
+ final AtomicBoolean suspended = new AtomicBoolean(true);
+ GatewayServer server = new GatewayServer()
+ {
+ @Override
+ protected Gateway createGateway()
+ {
+ StandardGateway gateway = new StandardGateway()
+ {
+ @Override
+ public ExternalRequest newExternalRequest(HttpServletRequest httpRequest, HttpServletResponse httpResponse) throws IOException
+ {
+ return new SlowToSuspendExternalRequest(super.newExternalRequest(httpRequest, httpResponse), respondLatch, suspendLatch, suspended);
+ }
+ };
+ return gateway;
+ }
+ };
+ SelectChannelConnector connector = new SelectChannelConnector();
+ server.addConnector(connector);
+ server.start();
+ try
+ {
+ Address address = new Address("localhost", connector.getLocalPort());
+
+ HttpClient httpClient = new HttpClient();
+ httpClient.setConnectorType(HttpClient.CONNECTOR_SELECT_CHANNEL);
+ httpClient.start();
+ try
+ {
+ String targetId = "1";
+ final RHTTPClient client = new JettyClient(httpClient, address, server.getContext().getContextPath()+GatewayServer.DFT_CONNECT_PATH, targetId);
+ final AtomicReference exception = new AtomicReference();
+ client.addListener(new RHTTPListener()
+ {
+ public void onRequest(RHTTPRequest request)
+ {
+ try
+ {
+ RHTTPResponse response = new RHTTPResponse(request.getId(), 200, "OK", new HashMap(), request.getBody());
+ client.deliver(response);
+ }
+ catch (Exception x)
+ {
+ exception.set(x);
+ }
+ }
+ });
+
+ client.connect();
+ try
+ {
+ // Make a request to the gateway and check response
+ ContentExchange exchange = new ContentExchange(true);
+ exchange.setMethod(HttpMethods.POST);
+ exchange.setAddress(address);
+ exchange.setURI(server.getContext().getContextPath()+GatewayServer.DFT_EXT_PATH + "/" + URLEncoder.encode(targetId, "UTF-8"));
+ String requestContent = "body";
+ exchange.setRequestContent(new ByteArrayBuffer(requestContent.getBytes("UTF-8")));
+ httpClient.send(exchange);
+
+ int status = exchange.waitForDone();
+ assertEquals(HttpExchange.STATUS_COMPLETED, status);
+ assertEquals(HttpServletResponse.SC_OK, exchange.getResponseStatus());
+ assertNull(exception.get());
+
+ suspendLatch.await();
+ assertFalse(suspended.get());
+ }
+ finally
+ {
+ client.disconnect();
+ }
+ }
+ finally
+ {
+ httpClient.stop();
+ }
+ }
+ finally
+ {
+ server.stop();
+ }
+ }
+
+ private class SlowToSuspendExternalRequest implements ExternalRequest
+ {
+ private final ExternalRequest delegate;
+ private final CountDownLatch respondLatch;
+ private final CountDownLatch suspendLatch;
+ private final AtomicBoolean suspended;
+
+ private SlowToSuspendExternalRequest(ExternalRequest delegate, CountDownLatch respondLatch, CountDownLatch suspendLatch, AtomicBoolean suspended)
+ {
+ this.delegate = delegate;
+ this.respondLatch = respondLatch;
+ this.suspendLatch = suspendLatch;
+ this.suspended = suspended;
+ }
+
+ public boolean suspend()
+ {
+ try
+ {
+ respondLatch.await();
+ boolean result = delegate.suspend();
+ suspended.set(result);
+ suspendLatch.countDown();
+ return result;
+ }
+ catch (InterruptedException x)
+ {
+ throw new AssertionError(x);
+ }
+ }
+
+ public void respond(RHTTPResponse response) throws IOException
+ {
+ delegate.respond(response);
+ respondLatch.countDown();
+ }
+
+ public RHTTPRequest getRequest()
+ {
+ return delegate.getRequest();
+ }
+ }
+}
diff --git a/jetty-rhttp/jetty-rhttp-gateway/src/test/java/org/eclipse/jetty/rhttp/gateway/ExternalTimeoutTest.java b/jetty-rhttp/jetty-rhttp-gateway/src/test/java/org/eclipse/jetty/rhttp/gateway/ExternalTimeoutTest.java
new file mode 100644
index 00000000000..4b88dc3e45a
--- /dev/null
+++ b/jetty-rhttp/jetty-rhttp-gateway/src/test/java/org/eclipse/jetty/rhttp/gateway/ExternalTimeoutTest.java
@@ -0,0 +1,126 @@
+//
+// ========================================================================
+// Copyright (c) 1995-2012 Mort Bay Consulting Pty. Ltd.
+// ------------------------------------------------------------------------
+// All rights reserved. This program and the accompanying materials
+// are made available under the terms of the Eclipse Public License v1.0
+// and Apache License v2.0 which accompanies this distribution.
+//
+// The Eclipse Public License is available at
+// http://www.eclipse.org/legal/epl-v10.html
+//
+// The Apache License v2.0 is available at
+// http://www.opensource.org/licenses/apache2.0.php
+//
+// You may elect to redistribute this code under either of these licenses.
+// ========================================================================
+//
+
+package org.eclipse.jetty.rhttp.gateway;
+
+import java.net.URLEncoder;
+import java.util.HashMap;
+import java.util.concurrent.atomic.AtomicReference;
+
+import javax.servlet.http.HttpServletResponse;
+
+import junit.framework.TestCase;
+
+import org.eclipse.jetty.client.Address;
+import org.eclipse.jetty.client.ContentExchange;
+import org.eclipse.jetty.client.HttpClient;
+import org.eclipse.jetty.client.HttpExchange;
+import org.eclipse.jetty.http.HttpMethods;
+import org.eclipse.jetty.io.ByteArrayBuffer;
+import org.eclipse.jetty.rhttp.client.JettyClient;
+import org.eclipse.jetty.rhttp.client.RHTTPClient;
+import org.eclipse.jetty.rhttp.client.RHTTPListener;
+import org.eclipse.jetty.rhttp.client.RHTTPRequest;
+import org.eclipse.jetty.rhttp.client.RHTTPResponse;
+import org.eclipse.jetty.rhttp.gateway.GatewayServer;
+import org.eclipse.jetty.rhttp.gateway.StandardGateway;
+import org.eclipse.jetty.server.nio.SelectChannelConnector;
+
+
+/**
+ * @version $Revision$ $Date$
+ */
+public class ExternalTimeoutTest extends TestCase
+{
+ public void testExternalTimeout() throws Exception
+ {
+ GatewayServer server = new GatewayServer();
+ SelectChannelConnector connector = new SelectChannelConnector();
+ server.addConnector(connector);
+ final long externalTimeout = 5000L;
+ ((StandardGateway)server.getGateway()).setExternalTimeout(externalTimeout);
+ server.start();
+ try
+ {
+ Address address = new Address("localhost", connector.getLocalPort());
+
+ HttpClient httpClient = new HttpClient();
+ httpClient.setConnectorType(HttpClient.CONNECTOR_SELECT_CHANNEL);
+ httpClient.start();
+ try
+ {
+ String targetId = "1";
+ final RHTTPClient client = new JettyClient(httpClient, address, server.getContext().getContextPath()+GatewayServer.DFT_CONNECT_PATH, targetId);
+ final AtomicReference requestId = new AtomicReference();
+ final AtomicReference exceptionRef = new AtomicReference();
+ client.addListener(new RHTTPListener()
+ {
+ public void onRequest(RHTTPRequest request)
+ {
+ try
+ {
+ // Save the request id
+ requestId.set(request.getId());
+
+ // Wait until external timeout expires
+ Thread.sleep(externalTimeout + 1000L);
+ RHTTPResponse response = new RHTTPResponse(request.getId(), 200, "OK", new HashMap(), request.getBody());
+ client.deliver(response);
+ }
+ catch (Exception x)
+ {
+ exceptionRef.set(x);
+ }
+ }
+ });
+
+ client.connect();
+ try
+ {
+ // Make a request to the gateway and check response
+ ContentExchange exchange = new ContentExchange(true);
+ exchange.setMethod(HttpMethods.POST);
+ exchange.setAddress(address);
+ exchange.setURI(server.getContext().getContextPath()+GatewayServer.DFT_EXT_PATH + "/" + URLEncoder.encode(targetId, "UTF-8"));
+ String requestContent = "body";
+ exchange.setRequestContent(new ByteArrayBuffer(requestContent.getBytes("UTF-8")));
+ httpClient.send(exchange);
+
+ int status = exchange.waitForDone();
+ assertEquals(HttpExchange.STATUS_COMPLETED, status);
+ assertEquals(HttpServletResponse.SC_GATEWAY_TIMEOUT, exchange.getResponseStatus());
+ assertNull(exceptionRef.get());
+
+ assertNull(server.getGateway().removeExternalRequest(requestId.get()));
+ }
+ finally
+ {
+ client.disconnect();
+ }
+ }
+ finally
+ {
+ httpClient.stop();
+ }
+ }
+ finally
+ {
+ server.stop();
+ }
+ }
+}
diff --git a/jetty-rhttp/jetty-rhttp-gateway/src/test/java/org/eclipse/jetty/rhttp/gateway/GatewayEchoServer.java b/jetty-rhttp/jetty-rhttp-gateway/src/test/java/org/eclipse/jetty/rhttp/gateway/GatewayEchoServer.java
new file mode 100644
index 00000000000..a3862b5f522
--- /dev/null
+++ b/jetty-rhttp/jetty-rhttp-gateway/src/test/java/org/eclipse/jetty/rhttp/gateway/GatewayEchoServer.java
@@ -0,0 +1,109 @@
+//
+// ========================================================================
+// Copyright (c) 1995-2012 Mort Bay Consulting Pty. Ltd.
+// ------------------------------------------------------------------------
+// All rights reserved. This program and the accompanying materials
+// are made available under the terms of the Eclipse Public License v1.0
+// and Apache License v2.0 which accompanies this distribution.
+//
+// The Eclipse Public License is available at
+// http://www.eclipse.org/legal/epl-v10.html
+//
+// The Apache License v2.0 is available at
+// http://www.opensource.org/licenses/apache2.0.php
+//
+// You may elect to redistribute this code under either of these licenses.
+// ========================================================================
+//
+
+package org.eclipse.jetty.rhttp.gateway;
+
+import java.util.HashMap;
+
+import javax.servlet.http.HttpServletRequest;
+
+import org.eclipse.jetty.client.Address;
+import org.eclipse.jetty.client.HttpClient;
+import org.eclipse.jetty.rhttp.client.JettyClient;
+import org.eclipse.jetty.rhttp.client.RHTTPClient;
+import org.eclipse.jetty.rhttp.client.RHTTPListener;
+import org.eclipse.jetty.rhttp.client.RHTTPRequest;
+import org.eclipse.jetty.rhttp.client.RHTTPResponse;
+import org.eclipse.jetty.rhttp.gateway.GatewayServer;
+import org.eclipse.jetty.rhttp.gateway.TargetIdRetriever;
+import org.eclipse.jetty.server.Connector;
+import org.eclipse.jetty.server.nio.SelectChannelConnector;
+
+
+/**
+ * @version $Revision$ $Date$
+ */
+public class GatewayEchoServer
+{
+ private volatile GatewayServer server;
+ private volatile Address address;
+ private volatile String uri;
+ private volatile HttpClient httpClient;
+ private volatile RHTTPClient client;
+
+ public void start() throws Exception
+ {
+ server = new GatewayServer();
+ Connector connector = new SelectChannelConnector();
+ server.addConnector(connector);
+ server.setTargetIdRetriever(new EchoTargetIdRetriever());
+ server.start();
+ server.dumpStdErr();
+ address = new Address("localhost", connector.getLocalPort());
+ uri = server.getContext().getContextPath()+GatewayServer.DFT_EXT_PATH;
+
+ httpClient = new HttpClient();
+ httpClient.setConnectorType(HttpClient.CONNECTOR_SELECT_CHANNEL);
+ httpClient.start();
+
+ client = new JettyClient(httpClient, new Address("localhost", connector.getLocalPort()), server.getContext().getContextPath()+GatewayServer.DFT_CONNECT_PATH, "echo");
+ client.addListener(new EchoListener(client));
+ client.connect();
+ }
+
+ public void stop() throws Exception
+ {
+ client.disconnect();
+ httpClient.stop();
+ server.stop();
+ }
+
+ public Address getAddress()
+ {
+ return address;
+ }
+
+ public String getURI()
+ {
+ return uri;
+ }
+
+ public static class EchoTargetIdRetriever implements TargetIdRetriever
+ {
+ public String retrieveTargetId(HttpServletRequest httpRequest)
+ {
+ return "echo";
+ }
+ }
+
+ private static class EchoListener implements RHTTPListener
+ {
+ private final RHTTPClient client;
+
+ public EchoListener(RHTTPClient client)
+ {
+ this.client = client;
+ }
+
+ public void onRequest(RHTTPRequest request) throws Exception
+ {
+ RHTTPResponse response = new RHTTPResponse(request.getId(), 200, "OK", new HashMap(), request.getBody());
+ client.deliver(response);
+ }
+ }
+}
diff --git a/jetty-rhttp/jetty-rhttp-gateway/src/test/java/org/eclipse/jetty/rhttp/gateway/GatewayEchoTest.java b/jetty-rhttp/jetty-rhttp-gateway/src/test/java/org/eclipse/jetty/rhttp/gateway/GatewayEchoTest.java
new file mode 100644
index 00000000000..d4c34be2293
--- /dev/null
+++ b/jetty-rhttp/jetty-rhttp-gateway/src/test/java/org/eclipse/jetty/rhttp/gateway/GatewayEchoTest.java
@@ -0,0 +1,77 @@
+//
+// ========================================================================
+// Copyright (c) 1995-2012 Mort Bay Consulting Pty. Ltd.
+// ------------------------------------------------------------------------
+// All rights reserved. This program and the accompanying materials
+// are made available under the terms of the Eclipse Public License v1.0
+// and Apache License v2.0 which accompanies this distribution.
+//
+// The Eclipse Public License is available at
+// http://www.eclipse.org/legal/epl-v10.html
+//
+// The Apache License v2.0 is available at
+// http://www.opensource.org/licenses/apache2.0.php
+//
+// You may elect to redistribute this code under either of these licenses.
+// ========================================================================
+//
+
+package org.eclipse.jetty.rhttp.gateway;
+
+import javax.servlet.http.HttpServletResponse;
+
+import junit.framework.TestCase;
+
+import org.eclipse.jetty.client.ContentExchange;
+import org.eclipse.jetty.client.HttpClient;
+import org.eclipse.jetty.client.HttpExchange;
+import org.eclipse.jetty.http.HttpMethods;
+import org.eclipse.jetty.io.ByteArrayBuffer;
+
+/**
+ * @version $Revision$ $Date$
+ */
+public class GatewayEchoTest extends TestCase
+{
+ /**
+ * Tests that the basic functionality of the gateway works,
+ * by issuing a request and by replying with the same body.
+ *
+ * @throws Exception in case of test exceptions
+ */
+ public void testEcho() throws Exception
+ {
+ GatewayEchoServer server = new GatewayEchoServer();
+ server.start();
+ try
+ {
+ HttpClient httpClient = new HttpClient();
+ httpClient.setConnectorType(HttpClient.CONNECTOR_SELECT_CHANNEL);
+ httpClient.start();
+ try
+ {
+ // Make a request to the gateway and check response
+ ContentExchange exchange = new ContentExchange(true);
+ exchange.setMethod(HttpMethods.POST);
+ exchange.setAddress(server.getAddress());
+ exchange.setURI(server.getURI() + "/");
+ String requestBody = "body";
+ exchange.setRequestContent(new ByteArrayBuffer(requestBody.getBytes("UTF-8")));
+ httpClient.send(exchange);
+ int status = exchange.waitForDone();
+ assertEquals(HttpExchange.STATUS_COMPLETED, status);
+ assertEquals(HttpServletResponse.SC_OK, exchange.getResponseStatus());
+ String responseContent = exchange.getResponseContent();
+ assertEquals(responseContent, requestBody);
+ }
+ finally
+ {
+ httpClient.stop();
+ }
+ }
+ finally
+ {
+ server.stop();
+ }
+ }
+}
diff --git a/jetty-rhttp/jetty-rhttp-gateway/src/test/java/org/eclipse/jetty/rhttp/gateway/GatewayLoadTest.java b/jetty-rhttp/jetty-rhttp-gateway/src/test/java/org/eclipse/jetty/rhttp/gateway/GatewayLoadTest.java
new file mode 100644
index 00000000000..7c65db22403
--- /dev/null
+++ b/jetty-rhttp/jetty-rhttp-gateway/src/test/java/org/eclipse/jetty/rhttp/gateway/GatewayLoadTest.java
@@ -0,0 +1,202 @@
+//
+// ========================================================================
+// Copyright (c) 1995-2012 Mort Bay Consulting Pty. Ltd.
+// ------------------------------------------------------------------------
+// All rights reserved. This program and the accompanying materials
+// are made available under the terms of the Eclipse Public License v1.0
+// and Apache License v2.0 which accompanies this distribution.
+//
+// The Eclipse Public License is available at
+// http://www.eclipse.org/legal/epl-v10.html
+//
+// The Apache License v2.0 is available at
+// http://www.opensource.org/licenses/apache2.0.php
+//
+// You may elect to redistribute this code under either of these licenses.
+// ========================================================================
+//
+
+package org.eclipse.jetty.rhttp.gateway;
+
+import java.io.IOException;
+import java.util.Arrays;
+import java.util.Iterator;
+import java.util.Map;
+import java.util.concurrent.ConcurrentHashMap;
+import java.util.concurrent.ConcurrentMap;
+import java.util.concurrent.CountDownLatch;
+import java.util.concurrent.TimeUnit;
+import java.util.concurrent.atomic.AtomicLong;
+
+import javax.servlet.http.HttpServletResponse;
+
+import junit.framework.TestCase;
+
+import org.eclipse.jetty.client.ContentExchange;
+import org.eclipse.jetty.client.HttpClient;
+import org.eclipse.jetty.http.HttpMethods;
+import org.eclipse.jetty.io.ByteArrayBuffer;
+
+/**
+ * @version $Revision$ $Date$
+ */
+public class GatewayLoadTest extends TestCase
+{
+ private final ConcurrentMap latencies = new ConcurrentHashMap();
+ private final AtomicLong responses = new AtomicLong(0L);
+ private final AtomicLong failures = new AtomicLong(0L);
+ private final AtomicLong minLatency = new AtomicLong(Long.MAX_VALUE);
+ private final AtomicLong maxLatency = new AtomicLong(0L);
+ private final AtomicLong totLatency = new AtomicLong(0L);
+
+ public void testEcho() throws Exception
+ {
+ GatewayEchoServer server = new GatewayEchoServer();
+ server.start();
+
+ HttpClient httpClient = new HttpClient();
+ httpClient.setConnectorType(HttpClient.CONNECTOR_SELECT_CHANNEL);
+ httpClient.start();
+
+ String uri = server.getURI() + "/";
+
+ char[] chars = new char[1024];
+ Arrays.fill(chars, 'x');
+ String requestBody = new String(chars);
+
+ int count = 1000;
+ CountDownLatch latch = new CountDownLatch(count);
+ for (int i = 0; i < count; ++i)
+ {
+ GatewayLoadTestExchange exchange = new GatewayLoadTestExchange(latch);
+ exchange.setMethod(HttpMethods.POST);
+ exchange.setAddress(server.getAddress());
+ exchange.setURI(uri + i);
+ exchange.setRequestContent(new ByteArrayBuffer(requestBody.getBytes("UTF-8")));
+ exchange.setStartNanos(System.nanoTime());
+ httpClient.send(exchange);
+ Thread.sleep(5);
+ }
+ latch.await();
+ printLatencies(count);
+ assertEquals(count, responses.get() + failures.get());
+ }
+
+ private void updateLatencies(long start, long end)
+ {
+ long latency = end - start;
+
+ // Update the latencies using a non-blocking algorithm
+ long oldMinLatency = minLatency.get();
+ while (latency < oldMinLatency)
+ {
+ if (minLatency.compareAndSet(oldMinLatency, latency)) break;
+ oldMinLatency = minLatency.get();
+ }
+ long oldMaxLatency = maxLatency.get();
+ while (latency > oldMaxLatency)
+ {
+ if (maxLatency.compareAndSet(oldMaxLatency, latency)) break;
+ oldMaxLatency = maxLatency.get();
+ }
+ totLatency.addAndGet(latency);
+
+ latencies.putIfAbsent(latency, new AtomicLong(0L));
+ latencies.get(latency).incrementAndGet();
+ }
+
+ public void printLatencies(long expectedCount)
+ {
+ if (latencies.size() > 1)
+ {
+ long maxLatencyBucketFrequency = 0L;
+ long[] latencyBucketFrequencies = new long[20];
+ long latencyRange = maxLatency.get() - minLatency.get();
+ for (Iterator> entries = latencies.entrySet().iterator(); entries.hasNext();)
+ {
+ Map.Entry entry = entries.next();
+ long latency = entry.getKey();
+ Long bucketIndex = (latency - minLatency.get()) * latencyBucketFrequencies.length / latencyRange;
+ int index = bucketIndex.intValue() == latencyBucketFrequencies.length ? latencyBucketFrequencies.length - 1 : bucketIndex.intValue();
+ long value = entry.getValue().get();
+ latencyBucketFrequencies[index] += value;
+ if (latencyBucketFrequencies[index] > maxLatencyBucketFrequency) maxLatencyBucketFrequency = latencyBucketFrequencies[index];
+ entries.remove();
+ }
+
+ System.out.println("Messages - Latency Distribution Curve (X axis: Frequency, Y axis: Latency):");
+ for (int i = 0; i < latencyBucketFrequencies.length; i++)
+ {
+ long latencyBucketFrequency = latencyBucketFrequencies[i];
+ int value = Math.round(latencyBucketFrequency * (float) latencyBucketFrequencies.length / maxLatencyBucketFrequency);
+ if (value == latencyBucketFrequencies.length) value = value - 1;
+ for (int j = 0; j < value; ++j) System.out.print(" ");
+ System.out.print("@");
+ for (int j = value + 1; j < latencyBucketFrequencies.length; ++j) System.out.print(" ");
+ System.out.print(" _ ");
+ System.out.print(TimeUnit.NANOSECONDS.toMillis((latencyRange * (i + 1) / latencyBucketFrequencies.length) + minLatency.get()));
+ System.out.print(" (" + latencyBucketFrequency + ")");
+ System.out.println(" ms");
+ }
+ }
+
+ long responseCount = responses.get();
+ System.out.print("Messages success/failed/expected = ");
+ System.out.print(responseCount);
+ System.out.print("/");
+ System.out.print(failures.get());
+ System.out.print("/");
+ System.out.print(expectedCount);
+ System.out.print(" - Latency min/ave/max = ");
+ System.out.print(TimeUnit.NANOSECONDS.toMillis(minLatency.get()) + "/");
+ System.out.print(responseCount == 0 ? "-/" : TimeUnit.NANOSECONDS.toMillis(totLatency.get() / responseCount) + "/");
+ System.out.println(TimeUnit.NANOSECONDS.toMillis(maxLatency.get()) + " ms");
+ }
+
+ private class GatewayLoadTestExchange extends ContentExchange
+ {
+ private final CountDownLatch latch;
+ private volatile long start;
+
+ private GatewayLoadTestExchange(CountDownLatch latch)
+ {
+ super(true);
+ this.latch = latch;
+ }
+
+ @Override
+ protected void onResponseComplete() throws IOException
+ {
+ if (getResponseStatus() == HttpServletResponse.SC_OK)
+ {
+ long end = System.nanoTime();
+ responses.incrementAndGet();
+ updateLatencies(start, end);
+ }
+ else
+ {
+ failures.incrementAndGet();
+ }
+ latch.countDown();
+ }
+
+ @Override
+ protected void onException(Throwable throwable)
+ {
+ failures.incrementAndGet();
+ latch.countDown();
+ }
+
+ @Override
+ protected void onExpire()
+ {
+ failures.incrementAndGet();
+ latch.countDown();
+ }
+
+ public void setStartNanos(long value)
+ {
+ start = value;
+ }
+ }
+}
diff --git a/jetty-rhttp/jetty-rhttp-gateway/src/test/java/org/eclipse/jetty/rhttp/gateway/GatewayTimeoutTest.java b/jetty-rhttp/jetty-rhttp-gateway/src/test/java/org/eclipse/jetty/rhttp/gateway/GatewayTimeoutTest.java
new file mode 100644
index 00000000000..1c6b1f8fd39
--- /dev/null
+++ b/jetty-rhttp/jetty-rhttp-gateway/src/test/java/org/eclipse/jetty/rhttp/gateway/GatewayTimeoutTest.java
@@ -0,0 +1,130 @@
+//
+// ========================================================================
+// Copyright (c) 1995-2012 Mort Bay Consulting Pty. Ltd.
+// ------------------------------------------------------------------------
+// All rights reserved. This program and the accompanying materials
+// are made available under the terms of the Eclipse Public License v1.0
+// and Apache License v2.0 which accompanies this distribution.
+//
+// The Eclipse Public License is available at
+// http://www.eclipse.org/legal/epl-v10.html
+//
+// The Apache License v2.0 is available at
+// http://www.opensource.org/licenses/apache2.0.php
+//
+// You may elect to redistribute this code under either of these licenses.
+// ========================================================================
+//
+
+package org.eclipse.jetty.rhttp.gateway;
+
+import java.net.URLEncoder;
+import java.util.HashMap;
+import java.util.concurrent.atomic.AtomicReference;
+
+import javax.servlet.http.HttpServletResponse;
+
+import junit.framework.TestCase;
+
+import org.eclipse.jetty.client.Address;
+import org.eclipse.jetty.client.ContentExchange;
+import org.eclipse.jetty.client.HttpClient;
+import org.eclipse.jetty.client.HttpExchange;
+import org.eclipse.jetty.http.HttpMethods;
+import org.eclipse.jetty.io.ByteArrayBuffer;
+import org.eclipse.jetty.rhttp.client.JettyClient;
+import org.eclipse.jetty.rhttp.client.RHTTPClient;
+import org.eclipse.jetty.rhttp.client.RHTTPListener;
+import org.eclipse.jetty.rhttp.client.RHTTPRequest;
+import org.eclipse.jetty.rhttp.client.RHTTPResponse;
+import org.eclipse.jetty.rhttp.gateway.GatewayServer;
+import org.eclipse.jetty.rhttp.gateway.StandardGateway;
+import org.eclipse.jetty.server.Connector;
+import org.eclipse.jetty.server.nio.SelectChannelConnector;
+
+
+/**
+ * @version $Revision$ $Date$
+ */
+public class GatewayTimeoutTest extends TestCase
+{
+ /**
+ * Tests a forwarded request that lasts longer than the gateway timeout.
+ * The gateway client will perform 2 long polls before the forwarded request's response is returned.
+ *
+ * @throws Exception in case of test exceptions
+ */
+ public void testGatewayTimeout() throws Exception
+ {
+ GatewayServer server = new GatewayServer();
+ Connector connector = new SelectChannelConnector();
+ server.addConnector(connector);
+ final long gatewayTimeout = 5000L;
+ ((StandardGateway)server.getGateway()).setGatewayTimeout(gatewayTimeout);
+ final long externalTimeout = gatewayTimeout * 2;
+ ((StandardGateway)server.getGateway()).setExternalTimeout(externalTimeout);
+ server.start();
+ try
+ {
+ Address address = new Address("localhost", connector.getLocalPort());
+
+ HttpClient httpClient = new HttpClient();
+ httpClient.setConnectorType(HttpClient.CONNECTOR_SELECT_CHANNEL);
+ httpClient.start();
+ try
+ {
+ String targetId = "1";
+ final RHTTPClient client = new JettyClient(httpClient, address, server.getContext().getContextPath()+GatewayServer.DFT_CONNECT_PATH, targetId);
+ final AtomicReference exceptionRef = new AtomicReference();
+ client.addListener(new RHTTPListener()
+ {
+ public void onRequest(RHTTPRequest request)
+ {
+ try
+ {
+ // Wait until gateway timeout expires
+ Thread.sleep(gatewayTimeout + 1000L);
+ RHTTPResponse response = new RHTTPResponse(request.getId(), 200, "OK", new HashMap(), request.getBody());
+ client.deliver(response);
+ }
+ catch (Exception x)
+ {
+ exceptionRef.set(x);
+ }
+ }
+ });
+ client.connect();
+ try
+ {
+ // Make a request to the gateway and check response
+ ContentExchange exchange = new ContentExchange(true);
+ exchange.setMethod(HttpMethods.POST);
+ exchange.setAddress(address);
+ exchange.setURI(server.getContext().getContextPath()+GatewayServer.DFT_EXT_PATH + "/" + URLEncoder.encode(targetId, "UTF-8"));
+ String requestContent = "body";
+ exchange.setRequestContent(new ByteArrayBuffer(requestContent.getBytes("UTF-8")));
+ httpClient.send(exchange);
+
+ int status = exchange.waitForDone();
+ assertEquals(HttpExchange.STATUS_COMPLETED, status);
+ assertEquals(HttpServletResponse.SC_OK, exchange.getResponseStatus());
+ assertNull(exceptionRef.get());
+ String responseContent = exchange.getResponseContent();
+ assertEquals(responseContent, requestContent);
+ }
+ finally
+ {
+ client.disconnect();
+ }
+ }
+ finally
+ {
+ httpClient.stop();
+ }
+ }
+ finally
+ {
+ server.stop();
+ }
+ }
+}
diff --git a/jetty-rhttp/jetty-rhttp-gateway/src/test/java/org/eclipse/jetty/rhttp/gateway/HandshakeClientTest.java b/jetty-rhttp/jetty-rhttp-gateway/src/test/java/org/eclipse/jetty/rhttp/gateway/HandshakeClientTest.java
new file mode 100644
index 00000000000..bee181319b2
--- /dev/null
+++ b/jetty-rhttp/jetty-rhttp-gateway/src/test/java/org/eclipse/jetty/rhttp/gateway/HandshakeClientTest.java
@@ -0,0 +1,75 @@
+//
+// ========================================================================
+// Copyright (c) 1995-2012 Mort Bay Consulting Pty. Ltd.
+// ------------------------------------------------------------------------
+// All rights reserved. This program and the accompanying materials
+// are made available under the terms of the Eclipse Public License v1.0
+// and Apache License v2.0 which accompanies this distribution.
+//
+// The Eclipse Public License is available at
+// http://www.eclipse.org/legal/epl-v10.html
+//
+// The Apache License v2.0 is available at
+// http://www.opensource.org/licenses/apache2.0.php
+//
+// You may elect to redistribute this code under either of these licenses.
+// ========================================================================
+//
+
+package org.eclipse.jetty.rhttp.gateway;
+
+import junit.framework.TestCase;
+
+import org.eclipse.jetty.client.Address;
+import org.eclipse.jetty.client.HttpClient;
+import org.eclipse.jetty.rhttp.client.JettyClient;
+import org.eclipse.jetty.rhttp.client.RHTTPClient;
+import org.eclipse.jetty.rhttp.gateway.GatewayServer;
+import org.eclipse.jetty.rhttp.gateway.StandardGateway;
+import org.eclipse.jetty.server.nio.SelectChannelConnector;
+
+
+/**
+ * @version $Revision$ $Date$
+ */
+public class HandshakeClientTest extends TestCase
+{
+ public void testConnectReturnsImmediately() throws Exception
+ {
+ GatewayServer server = new GatewayServer();
+ SelectChannelConnector connector = new SelectChannelConnector();
+ server.addConnector(connector);
+ long gwt=5000L;
+ ((StandardGateway)server.getGateway()).setGatewayTimeout(gwt);
+ server.start();
+ try
+ {
+ HttpClient httpClient = new HttpClient();
+ httpClient.setConnectorType(HttpClient.CONNECTOR_SELECT_CHANNEL);
+ httpClient.start();
+ try
+ {
+ RHTTPClient client = new JettyClient(httpClient, new Address("localhost", connector.getLocalPort()), server.getContext().getContextPath()+GatewayServer.DFT_CONNECT_PATH, "test1");
+ long start = System.currentTimeMillis();
+ client.connect();
+ try
+ {
+ long end = System.currentTimeMillis();
+ assertTrue(end - start < gwt / 2);
+ }
+ finally
+ {
+ client.disconnect();
+ }
+ }
+ finally
+ {
+ httpClient.stop();
+ }
+ }
+ finally
+ {
+ server.stop();
+ }
+ }
+}
diff --git a/jetty-rhttp/jetty-rhttp-gateway/src/test/java/org/eclipse/jetty/rhttp/gateway/HostTargetIdRetrieverTest.java b/jetty-rhttp/jetty-rhttp-gateway/src/test/java/org/eclipse/jetty/rhttp/gateway/HostTargetIdRetrieverTest.java
new file mode 100644
index 00000000000..911d1cb2fdb
--- /dev/null
+++ b/jetty-rhttp/jetty-rhttp-gateway/src/test/java/org/eclipse/jetty/rhttp/gateway/HostTargetIdRetrieverTest.java
@@ -0,0 +1,107 @@
+//
+// ========================================================================
+// Copyright (c) 1995-2012 Mort Bay Consulting Pty. Ltd.
+// ------------------------------------------------------------------------
+// All rights reserved. This program and the accompanying materials
+// are made available under the terms of the Eclipse Public License v1.0
+// and Apache License v2.0 which accompanies this distribution.
+//
+// The Eclipse Public License is available at
+// http://www.eclipse.org/legal/epl-v10.html
+//
+// The Apache License v2.0 is available at
+// http://www.opensource.org/licenses/apache2.0.php
+//
+// You may elect to redistribute this code under either of these licenses.
+// ========================================================================
+//
+
+package org.eclipse.jetty.rhttp.gateway;
+
+import java.lang.reflect.InvocationHandler;
+import java.lang.reflect.Method;
+import java.lang.reflect.Proxy;
+
+import javax.servlet.http.HttpServletRequest;
+
+import org.eclipse.jetty.rhttp.gateway.HostTargetIdRetriever;
+
+import junit.framework.TestCase;
+
+/**
+ * @version $Revision$ $Date$
+ */
+public class HostTargetIdRetrieverTest extends TestCase
+{
+ public void testHostTargetIdRetrieverNoSuffix()
+ {
+ String host = "test";
+ Class klass = HttpServletRequest.class;
+ HttpServletRequest request = (HttpServletRequest)Proxy.newProxyInstance(klass.getClassLoader(), new Class>[]{klass}, new Request(host));
+
+ HostTargetIdRetriever retriever = new HostTargetIdRetriever(null);
+ String result = retriever.retrieveTargetId(request);
+
+ assertEquals(host, result);
+ }
+
+ public void testHostTargetIdRetrieverWithSuffix()
+ {
+ String suffix = ".rhttp.example.com";
+ String host = "test";
+ Class klass = HttpServletRequest.class;
+ HttpServletRequest request = (HttpServletRequest)Proxy.newProxyInstance(klass.getClassLoader(), new Class>[]{klass}, new Request(host + suffix));
+
+ HostTargetIdRetriever retriever = new HostTargetIdRetriever(suffix);
+ String result = retriever.retrieveTargetId(request);
+
+ assertEquals(host, result);
+ }
+
+ public void testHostTargetIdRetrieverWithSuffixAndPort()
+ {
+ String suffix = ".rhttp.example.com";
+ String host = "test";
+ Class klass = HttpServletRequest.class;
+ HttpServletRequest request = (HttpServletRequest)Proxy.newProxyInstance(klass.getClassLoader(), new Class>[]{klass}, new Request(host + suffix + ":8080"));
+
+ HostTargetIdRetriever retriever = new HostTargetIdRetriever(suffix);
+ String result = retriever.retrieveTargetId(request);
+
+ assertEquals(host, result);
+ }
+
+ public void testHostTargetIdRetrieverNullHost()
+ {
+ Class klass = HttpServletRequest.class;
+ HttpServletRequest request = (HttpServletRequest)Proxy.newProxyInstance(klass.getClassLoader(), new Class>[]{klass}, new Request(null));
+
+ HostTargetIdRetriever retriever = new HostTargetIdRetriever(".rhttp.example.com");
+ String result = retriever.retrieveTargetId(request);
+
+ assertNull(result);
+ }
+
+ private static class Request implements InvocationHandler
+ {
+ private final String host;
+
+ private Request(String host)
+ {
+ this.host = host;
+ }
+
+ public Object invoke(Object proxy, Method method, Object[] args) throws Throwable
+ {
+ if ("getHeader".equals(method.getName()))
+ {
+ if (args.length == 1 && "Host".equals(args[0]))
+ {
+ return host;
+ }
+ }
+ return null;
+ }
+ }
+
+}
diff --git a/jetty-rhttp/jetty-rhttp-gateway/src/test/resources/log4j.properties b/jetty-rhttp/jetty-rhttp-gateway/src/test/resources/log4j.properties
new file mode 100644
index 00000000000..8e6eddb704b
--- /dev/null
+++ b/jetty-rhttp/jetty-rhttp-gateway/src/test/resources/log4j.properties
@@ -0,0 +1,13 @@
+# LOG4J levels: OFF, FATAL, ERROR, WARN, INFO, DEBUG, ALL
+#
+log4j.rootLogger=ALL,CONSOLE
+
+log4j.appender.CONSOLE=org.apache.log4j.ConsoleAppender
+#log4j.appender.CONSOLE.threshold=INFO
+log4j.appender.CONSOLE.layout=org.apache.log4j.PatternLayout
+#log4j.appender.CONSOLE.layout.ConversionPattern=%d %t [%5p][%c{1}] %m%n
+log4j.appender.CONSOLE.layout.ConversionPattern=%d [%5p][%c] %m%n
+
+# Level tuning
+log4j.logger.org.eclipse.jetty=INFO
+log4j.logger.org.mortbay.jetty.rhttp=INFO
diff --git a/jetty-rhttp/jetty-rhttp-loadtest/pom.xml b/jetty-rhttp/jetty-rhttp-loadtest/pom.xml
new file mode 100644
index 00000000000..bd36d4ebdec
--- /dev/null
+++ b/jetty-rhttp/jetty-rhttp-loadtest/pom.xml
@@ -0,0 +1,58 @@
+
+
+
+ org.eclipse.jetty.rhttp
+ jetty-rhttp-project
+ 9.0.0-SNAPSHOT
+
+
+ 4.0.0
+ reverse-http-loadtest
+ jar
+ Jetty :: Reverse HTTP :: Load Test
+
+
+
+ loader
+
+ true
+
+
+
+
+ org.codehaus.mojo
+ exec-maven-plugin
+
+ org.eclipse.jetty.rhttp.loadtest.Loader
+ runtime
+
+
+
+
+
+
+ server
+
+
+
+ org.codehaus.mojo
+ exec-maven-plugin
+
+ org.eclipse.jetty.rhttp.loadtest.Server
+ runtime
+
+
+
+
+
+
+
+
+
+ org.eclipse.jetty
+ reverse-http-gateway
+ ${project.version}
+
+
+
+
diff --git a/jetty-rhttp/jetty-rhttp-loadtest/src/main/java/org/eclipse/jetty/rhttp/loadtest/Loader.java b/jetty-rhttp/jetty-rhttp-loadtest/src/main/java/org/eclipse/jetty/rhttp/loadtest/Loader.java
new file mode 100644
index 00000000000..383ddd7ecd1
--- /dev/null
+++ b/jetty-rhttp/jetty-rhttp-loadtest/src/main/java/org/eclipse/jetty/rhttp/loadtest/Loader.java
@@ -0,0 +1,429 @@
+//
+// ========================================================================
+// Copyright (c) 1995-2012 Mort Bay Consulting Pty. Ltd.
+// ------------------------------------------------------------------------
+// All rights reserved. This program and the accompanying materials
+// are made available under the terms of the Eclipse Public License v1.0
+// and Apache License v2.0 which accompanies this distribution.
+//
+// The Eclipse Public License is available at
+// http://www.eclipse.org/legal/epl-v10.html
+//
+// The Apache License v2.0 is available at
+// http://www.opensource.org/licenses/apache2.0.php
+//
+// You may elect to redistribute this code under either of these licenses.
+// ========================================================================
+//
+
+package org.eclipse.jetty.rhttp.loadtest;
+
+import java.io.BufferedReader;
+import java.io.IOException;
+import java.io.InputStreamReader;
+import java.util.ArrayList;
+import java.util.HashMap;
+import java.util.Iterator;
+import java.util.List;
+import java.util.Map;
+import java.util.Random;
+import java.util.concurrent.ConcurrentHashMap;
+import java.util.concurrent.ConcurrentMap;
+import java.util.concurrent.TimeUnit;
+import java.util.concurrent.atomic.AtomicLong;
+
+import org.eclipse.jetty.client.Address;
+import org.eclipse.jetty.client.ContentExchange;
+import org.eclipse.jetty.client.HttpClient;
+import org.eclipse.jetty.io.ByteArrayBuffer;
+import org.eclipse.jetty.util.thread.QueuedThreadPool;
+import org.mortbay.jetty.rhttp.client.RHTTPClient;
+import org.mortbay.jetty.rhttp.client.JettyClient;
+import org.mortbay.jetty.rhttp.client.RHTTPListener;
+import org.mortbay.jetty.rhttp.client.RHTTPRequest;
+import org.mortbay.jetty.rhttp.client.RHTTPResponse;
+
+/**
+ * @version $Revision$ $Date$
+ */
+public class Loader
+{
+ private final List clients = new ArrayList();
+ private final AtomicLong start = new AtomicLong();
+ private final AtomicLong end = new AtomicLong();
+ private final AtomicLong responses = new AtomicLong();
+ private final AtomicLong failures = new AtomicLong();
+ private final AtomicLong minLatency = new AtomicLong();
+ private final AtomicLong maxLatency = new AtomicLong();
+ private final AtomicLong totLatency = new AtomicLong();
+ private final ConcurrentMap latencies = new ConcurrentHashMap();
+ private final String nodeName;
+
+ public static void main(String[] args) throws Exception
+ {
+ String nodeName = "";
+ if (args.length > 0)
+ nodeName = args[0];
+
+ Loader loader = new Loader(nodeName);
+ loader.run();
+ }
+
+ public Loader(String nodeName)
+ {
+ this.nodeName = nodeName;
+ }
+
+ private void run() throws Exception
+ {
+ HttpClient httpClient = new HttpClient();
+ httpClient.setMaxConnectionsPerAddress(40000);
+ QueuedThreadPool threadPool = new QueuedThreadPool();
+ threadPool.setMaxThreads(500);
+ threadPool.setDaemon(true);
+ httpClient.setThreadPool(threadPool);
+ httpClient.setIdleTimeout(5000);
+ httpClient.start();
+
+ Random random = new Random();
+
+ BufferedReader console = new BufferedReader(new InputStreamReader(System.in));
+
+ System.err.print("server [localhost]: ");
+ String value = console.readLine().trim();
+ if (value.length() == 0)
+ value = "localhost";
+ String host = value;
+
+ System.err.print("port [8080]: ");
+ value = console.readLine().trim();
+ if (value.length() == 0)
+ value = "8080";
+ int port = Integer.parseInt(value);
+
+ System.err.print("context []: ");
+ value = console.readLine().trim();
+ if (value.length() == 0)
+ value = "";
+ String context = value;
+
+ System.err.print("external path [/]: ");
+ value = console.readLine().trim();
+ if (value.length() == 0)
+ value = "/";
+ String externalPath = value;
+
+ System.err.print("gateway path [/__gateway]: ");
+ value = console.readLine().trim();
+ if (value.length() == 0)
+ value = "/__gateway";
+ String gatewayPath = value;
+
+ int clients = 100;
+ int batchCount = 1000;
+ int batchSize = 5;
+ long batchPause = 5;
+ int requestSize = 50;
+
+ while (true)
+ {
+ System.err.println("-----");
+
+ System.err.print("clients [" + clients + "]: ");
+ value = console.readLine();
+ if (value == null)
+ break;
+ value = value.trim();
+ if (value.length() == 0)
+ value = "" + clients;
+ clients = Integer.parseInt(value);
+
+ System.err.println("Waiting for clients to be ready...");
+
+ Address gatewayAddress = new Address(host, port);
+ String gatewayURI = context + gatewayPath;
+
+ // Create or remove the necessary clients
+ int currentClients = this.clients.size();
+ if (currentClients < clients)
+ {
+ for (int i = 0; i < clients - currentClients; ++i)
+ {
+ final RHTTPClient client = new JettyClient(httpClient, gatewayAddress, gatewayURI, nodeName + (currentClients + i));
+ client.addListener(new EchoListener(client));
+ client.connect();
+ this.clients.add(client);
+
+ // Give some time to the server to accept connections and
+ // reply to handshakes and connects
+ if (i % 10 == 0)
+ {
+ Thread.sleep(100);
+ }
+ }
+ }
+ else if (currentClients > clients)
+ {
+ for (int i = 0; i < currentClients - clients; ++i)
+ {
+ RHTTPClient client = this.clients.remove(currentClients - i - 1);
+ client.disconnect();
+ }
+ }
+
+ System.err.println("Clients ready");
+
+ currentClients = this.clients.size();
+ if (currentClients > 0)
+ {
+ System.err.print("batch count [" + batchCount + "]: ");
+ value = console.readLine().trim();
+ if (value.length() == 0)
+ value = "" + batchCount;
+ batchCount = Integer.parseInt(value);
+
+ System.err.print("batch size [" + batchSize + "]: ");
+ value = console.readLine().trim();
+ if (value.length() == 0)
+ value = "" + batchSize;
+ batchSize = Integer.parseInt(value);
+
+ System.err.print("batch pause [" + batchPause + "]: ");
+ value = console.readLine().trim();
+ if (value.length() == 0)
+ value = "" + batchPause;
+ batchPause = Long.parseLong(value);
+
+ System.err.print("request size [" + requestSize + "]: ");
+ value = console.readLine().trim();
+ if (value.length() == 0)
+ value = "" + requestSize;
+ requestSize = Integer.parseInt(value);
+ String requestBody = "";
+ for (int i = 0; i < requestSize; i++)
+ requestBody += "x";
+
+ String externalURL = "http://" + host + ":" + port + context + externalPath;
+ if (!externalURL.endsWith("/"))
+ externalURL += "/";
+
+ reset();
+
+ long start = System.nanoTime();
+ long expected = 0;
+ for (int i = 0; i < batchCount; ++i)
+ {
+ for (int j = 0; j < batchSize; ++j)
+ {
+ int clientIndex = random.nextInt(this.clients.size());
+ RHTTPClient client = this.clients.get(clientIndex);
+ String targetId = client.getTargetId();
+ String url = externalURL + targetId;
+
+ ExternalExchange exchange = new ExternalExchange();
+ exchange.setMethod("GET");
+ exchange.setURL(url);
+ exchange.setRequestContent(new ByteArrayBuffer(requestBody, "UTF-8"));
+ exchange.send(httpClient);
+ ++expected;
+ }
+
+ if (batchPause > 0)
+ Thread.sleep(batchPause);
+ }
+ long end = System.nanoTime();
+ long elapsedNanos = end - start;
+ if (elapsedNanos > 0)
+ {
+ System.err.print("Messages - Elapsed | Rate = ");
+ System.err.print(TimeUnit.NANOSECONDS.toMillis(elapsedNanos));
+ System.err.print(" ms | ");
+ System.err.print(expected * 1000 * 1000 * 1000 / elapsedNanos);
+ System.err.println(" requests/s ");
+ }
+
+ waitForResponses(expected);
+ printReport(expected);
+ }
+ }
+ }
+
+ private void reset()
+ {
+ start.set(0L);
+ end.set(0L);
+ responses.set(0L);
+ failures.set(0L);
+ minLatency.set(Long.MAX_VALUE);
+ maxLatency.set(0L);
+ totLatency.set(0L);
+ }
+
+ private void updateLatencies(long start, long end)
+ {
+ long latency = end - start;
+
+ // Update the latencies using a non-blocking algorithm
+ long oldMinLatency = minLatency.get();
+ while (latency < oldMinLatency)
+ {
+ if (minLatency.compareAndSet(oldMinLatency, latency)) break;
+ oldMinLatency = minLatency.get();
+ }
+ long oldMaxLatency = maxLatency.get();
+ while (latency > oldMaxLatency)
+ {
+ if (maxLatency.compareAndSet(oldMaxLatency, latency)) break;
+ oldMaxLatency = maxLatency.get();
+ }
+ totLatency.addAndGet(latency);
+
+ latencies.putIfAbsent(latency, new AtomicLong(0L));
+ latencies.get(latency).incrementAndGet();
+ }
+
+ private boolean waitForResponses(long expected) throws InterruptedException
+ {
+ long arrived = responses.get() + failures.get();
+ long lastArrived = 0;
+ int maxRetries = 20;
+ int retries = maxRetries;
+ while (arrived < expected)
+ {
+ System.err.println("Waiting for responses to arrive " + arrived + "/" + expected);
+ Thread.sleep(500);
+ if (lastArrived == arrived)
+ {
+ --retries;
+ if (retries == 0) break;
+ }
+ else
+ {
+ lastArrived = arrived;
+ retries = maxRetries;
+ }
+ arrived = responses.get() + failures.get();
+ }
+ if (arrived < expected)
+ {
+ System.err.println("Interrupting wait for responses " + arrived + "/" + expected);
+ return false;
+ }
+ else
+ {
+ System.err.println("All responses arrived " + arrived + "/" + expected);
+ return true;
+ }
+ }
+
+ public void printReport(long expectedCount)
+ {
+ long responseCount = responses.get() + failures.get();
+ System.err.print("Messages - Success/Failures/Expected = ");
+ System.err.print(responses.get());
+ System.err.print("/");
+ System.err.print(failures.get());
+ System.err.print("/");
+ System.err.println(expectedCount);
+
+ long elapsedNanos = end.get() - start.get();
+ if (elapsedNanos > 0)
+ {
+ System.err.print("Messages - Elapsed | Rate = ");
+ System.err.print(TimeUnit.NANOSECONDS.toMillis(elapsedNanos));
+ System.err.print(" ms | ");
+ System.err.print(responseCount * 1000 * 1000 * 1000 / elapsedNanos);
+ System.err.println(" responses/s ");
+ }
+
+ if (latencies.size() > 1)
+ {
+ long maxLatencyBucketFrequency = 0L;
+ long[] latencyBucketFrequencies = new long[20];
+ long latencyRange = maxLatency.get() - minLatency.get();
+ for (Iterator> entries = latencies.entrySet().iterator(); entries.hasNext();)
+ {
+ Map.Entry entry = entries.next();
+ long latency = entry.getKey();
+ Long bucketIndex = (latency - minLatency.get()) * latencyBucketFrequencies.length / latencyRange;
+ int index = bucketIndex.intValue() == latencyBucketFrequencies.length ? latencyBucketFrequencies.length - 1 : bucketIndex.intValue();
+ long value = entry.getValue().get();
+ latencyBucketFrequencies[index] += value;
+ if (latencyBucketFrequencies[index] > maxLatencyBucketFrequency) maxLatencyBucketFrequency = latencyBucketFrequencies[index];
+ entries.remove();
+ }
+
+ System.err.println("Messages - Latency Distribution Curve (X axis: Frequency, Y axis: Latency):");
+ for (int i = 0; i < latencyBucketFrequencies.length; i++)
+ {
+ long latencyBucketFrequency = latencyBucketFrequencies[i];
+ int value = Math.round(latencyBucketFrequency * (float) latencyBucketFrequencies.length / maxLatencyBucketFrequency);
+ if (value == latencyBucketFrequencies.length) value = value - 1;
+ for (int j = 0; j < value; ++j) System.err.print(" ");
+ System.err.print("@");
+ for (int j = value + 1; j < latencyBucketFrequencies.length; ++j) System.err.print(" ");
+ System.err.print(" _ ");
+ System.err.print(TimeUnit.NANOSECONDS.toMillis((latencyRange * (i + 1) / latencyBucketFrequencies.length) + minLatency.get()));
+ System.err.println(" ms (" + latencyBucketFrequency + ")");
+ }
+ }
+
+ System.err.print("Messages - Latency Min/Ave/Max = ");
+ System.err.print(TimeUnit.NANOSECONDS.toMillis(minLatency.get()) + "/");
+ System.err.print(responseCount == 0 ? "-/" : TimeUnit.NANOSECONDS.toMillis(totLatency.get() / responseCount) + "/");
+ System.err.println(TimeUnit.NANOSECONDS.toMillis(maxLatency.get()) + " ms");
+ }
+
+ private class ExternalExchange extends ContentExchange
+ {
+ private volatile long sendTime;
+
+ private ExternalExchange()
+ {
+ super(true);
+ }
+
+ private void send(HttpClient httpClient) throws IOException
+ {
+ this.sendTime = System.nanoTime();
+ httpClient.send(this);
+ }
+
+ @Override
+ protected void onResponseComplete() throws IOException
+ {
+ if (getResponseStatus() == 200)
+ responses.incrementAndGet();
+ else
+ failures.incrementAndGet();
+
+ long arrivalTime = System.nanoTime();
+ if (start.get() == 0L)
+ start.set(arrivalTime);
+ end.set(arrivalTime);
+ updateLatencies(sendTime, arrivalTime);
+ }
+
+ @Override
+ protected void onException(Throwable x)
+ {
+ failures.incrementAndGet();
+ }
+ }
+
+ private static class EchoListener implements RHTTPListener
+ {
+ private final RHTTPClient client;
+
+ public EchoListener(RHTTPClient client)
+ {
+ this.client = client;
+ }
+
+ public void onRequest(RHTTPRequest request) throws Exception
+ {
+ RHTTPResponse response = new RHTTPResponse(request.getId(), 200, "OK", new HashMap(), request.getBody());
+ client.deliver(response);
+ }
+ }
+}
diff --git a/jetty-rhttp/jetty-rhttp-loadtest/src/main/java/org/eclipse/jetty/rhttp/loadtest/Server.java b/jetty-rhttp/jetty-rhttp-loadtest/src/main/java/org/eclipse/jetty/rhttp/loadtest/Server.java
new file mode 100644
index 00000000000..20fcf1fa191
--- /dev/null
+++ b/jetty-rhttp/jetty-rhttp-loadtest/src/main/java/org/eclipse/jetty/rhttp/loadtest/Server.java
@@ -0,0 +1,69 @@
+//
+// ========================================================================
+// Copyright (c) 1995-2012 Mort Bay Consulting Pty. Ltd.
+// ------------------------------------------------------------------------
+// All rights reserved. This program and the accompanying materials
+// are made available under the terms of the Eclipse Public License v1.0
+// and Apache License v2.0 which accompanies this distribution.
+//
+// The Eclipse Public License is available at
+// http://www.eclipse.org/legal/epl-v10.html
+//
+// The Apache License v2.0 is available at
+// http://www.opensource.org/licenses/apache2.0.php
+//
+// You may elect to redistribute this code under either of these licenses.
+// ========================================================================
+//
+
+package org.eclipse.jetty.rhttp.loadtest;
+
+import org.eclipse.jetty.rhttp.gateway.GatewayServer;
+import org.eclipse.jetty.rhttp.gateway.StandardTargetIdRetriever;
+import org.eclipse.jetty.server.Connector;
+import org.eclipse.jetty.server.nio.SelectChannelConnector;
+
+/**
+ * @version $Revision$ $Date$
+ */
+public class Server
+{
+ public static void main(String[] args) throws Exception
+ {
+ int port = 8080;
+ if (args.length > 0)
+ port = Integer.parseInt(args[0]);
+
+ GatewayServer server = new GatewayServer();
+ Connector connector = new SelectChannelConnector();
+ connector.setLowResourceMaxIdleTime(connector.getMaxIdleTime());
+ connector.setPort(port);
+ server.addConnector(connector);
+ server.setTargetIdRetriever(new StandardTargetIdRetriever());
+ server.start();
+ server.getServer().dumpStdErr();
+ Runtime.getRuntime().addShutdownHook(new Shutdown(server));
+ }
+
+ private static class Shutdown extends Thread
+ {
+ private final GatewayServer server;
+
+ public Shutdown(GatewayServer server)
+ {
+ this.server = server;
+ }
+
+ @Override
+ public void run()
+ {
+ try
+ {
+ server.stop();
+ }
+ catch (Exception ignored)
+ {
+ }
+ }
+ }
+}
diff --git a/jetty-rhttp/jetty-rhttp-loadtest/src/main/resources/log4j.properties b/jetty-rhttp/jetty-rhttp-loadtest/src/main/resources/log4j.properties
new file mode 100644
index 00000000000..8e6eddb704b
--- /dev/null
+++ b/jetty-rhttp/jetty-rhttp-loadtest/src/main/resources/log4j.properties
@@ -0,0 +1,13 @@
+# LOG4J levels: OFF, FATAL, ERROR, WARN, INFO, DEBUG, ALL
+#
+log4j.rootLogger=ALL,CONSOLE
+
+log4j.appender.CONSOLE=org.apache.log4j.ConsoleAppender
+#log4j.appender.CONSOLE.threshold=INFO
+log4j.appender.CONSOLE.layout=org.apache.log4j.PatternLayout
+#log4j.appender.CONSOLE.layout.ConversionPattern=%d %t [%5p][%c{1}] %m%n
+log4j.appender.CONSOLE.layout.ConversionPattern=%d [%5p][%c] %m%n
+
+# Level tuning
+log4j.logger.org.eclipse.jetty=INFO
+log4j.logger.org.mortbay.jetty.rhttp=INFO
diff --git a/jetty-rhttp/pom.xml b/jetty-rhttp/pom.xml
new file mode 100644
index 00000000000..307e73846e6
--- /dev/null
+++ b/jetty-rhttp/pom.xml
@@ -0,0 +1,108 @@
+
+
+ org.eclipse.jetty
+ jetty-project
+ 9.0.0-SNAPSHOT
+
+
+ 4.0.0
+ org.eclipse.jetty.rhttp
+ jetty-rhttp-project
+ pom
+ Jetty :: Reverse HTTP
+
+
+
+
+
+ reverse-http-client
+ reverse-http-connector
+ reverse-http-gateway
+ reverse-http-loadtest
+
+
+
+
+
+ org.sonatype.maven.plugin
+ emma-maven-plugin
+
+
+ process-classes
+
+ instrument
+
+
+
+
+
+ maven-surefire-plugin
+
+ ${project.build.directory}/generated-classes/emma/classes
+
+
+ emma.coverage.out.file
+ ${project.build.directory}/coverage.ec
+
+
+
+
+
+ org.sonatype.maven.plugin
+ emma4it-maven-plugin
+
+
+ report
+ post-integration-test
+
+ report
+
+
+
+
+ ${project.build.sourceDirectory}
+
+
+
+
+
+
+
+
+
+
+
+
+ javax.servlet
+ servlet-api
+
+
+ org.eclipse.jetty
+ jetty-server
+ ${project.version}
+
+
+ org.eclipse.jetty
+ jetty-client
+ ${project.version}
+
+
+ org.eclipse.jetty
+ jetty-io
+ ${project.version}
+
+
+ org.eclipse.jetty
+ jetty-util
+ ${project.version}
+
+
+ junit
+ junit
+ test
+
+
+
+
+ org.eclipse.jetty.rhttp
+