diff --git a/android/src/main/java/io/ably/lib/rest/AblyRest.java b/android/src/main/java/io/ably/lib/rest/AblyRest.java
index 7f04feb2a..282d4eff3 100644
--- a/android/src/main/java/io/ably/lib/rest/AblyRest.java
+++ b/android/src/main/java/io/ably/lib/rest/AblyRest.java
@@ -36,6 +36,13 @@ public AblyRest(ClientOptions options) throws AblyException {
super(options, new AndroidPlatformAgentProvider());
}
+ /**
+ * Constructor implementation to be able to have proxy based on this class
+ */
+ protected AblyRest(AblyRest underlyingClient, DerivedClientOptions derivedOptions) {
+ super(underlyingClient, derivedOptions);
+ }
+
/**
* Retrieves a {@link LocalDevice} object that represents the current state of the device as a target for push notifications.
*
@@ -56,6 +63,15 @@ public void setAndroidContext(Context context) throws AblyException {
this.push.tryRequestRegistrationToken();
}
+ /**
+ * [Internal Method]
+ *
+ * We use this method to implement proxy Realtime / Rest clients that add additional data to the underlying client.
+ */
+ public AblyRest createDerivedClient(DerivedClientOptions derivedOptions) {
+ return new AblyRest(this, derivedOptions);
+ }
+
/**
* clientId set by late initialisation
*/
diff --git a/java/src/main/java/io/ably/lib/rest/AblyRest.java b/java/src/main/java/io/ably/lib/rest/AblyRest.java
index 7ab6a3390..74bdf388c 100644
--- a/java/src/main/java/io/ably/lib/rest/AblyRest.java
+++ b/java/src/main/java/io/ably/lib/rest/AblyRest.java
@@ -32,4 +32,20 @@ public AblyRest(String key) throws AblyException {
public AblyRest(ClientOptions options) throws AblyException {
super(options, new JavaPlatformAgentProvider());
}
+
+ /**
+ * Constructor implementation to be able to have proxy based on this class
+ */
+ protected AblyRest(AblyRest underlyingClient, DerivedClientOptions derivedOptions) {
+ super(underlyingClient, derivedOptions);
+ }
+
+ /**
+ * [Internal Method]
+ *
+ * We use this method to implement proxy Realtime / Rest clients that add additional data to the underlying client.
+ */
+ public AblyRest createDerivedClient(DerivedClientOptions derivedOptions) {
+ return new AblyRest(this, derivedOptions);
+ }
}
diff --git a/lib/src/main/java/io/ably/lib/http/AsyncHttpScheduler.java b/lib/src/main/java/io/ably/lib/http/AsyncHttpScheduler.java
index 598b9337f..285cd856c 100644
--- a/lib/src/main/java/io/ably/lib/http/AsyncHttpScheduler.java
+++ b/lib/src/main/java/io/ably/lib/http/AsyncHttpScheduler.java
@@ -16,10 +16,23 @@ public AsyncHttpScheduler(HttpCore httpCore, ClientOptions options) {
super(httpCore, new CloseableThreadPoolExecutor(options));
}
+ private AsyncHttpScheduler(HttpCore httpCore, CloseableExecutor executor) {
+ super(httpCore, executor);
+ }
+
private static final long KEEP_ALIVE_TIME = 2000L;
protected static final String TAG = AsyncHttpScheduler.class.getName();
+ /**
+ * [Internal Method]
+ *
+ * We use this method to implement proxy Realtime / Rest clients that add additional data to the underlying client.
+ */
+ public AsyncHttpScheduler exchangeHttpCore(HttpCore httpCore) {
+ return new AsyncHttpScheduler(httpCore, this.executor);
+ }
+
private static class CloseableThreadPoolExecutor implements CloseableExecutor {
private final ThreadPoolExecutor executor;
diff --git a/lib/src/main/java/io/ably/lib/http/Http.java b/lib/src/main/java/io/ably/lib/http/Http.java
index cf40b7c0b..708ccf13b 100644
--- a/lib/src/main/java/io/ably/lib/http/Http.java
+++ b/lib/src/main/java/io/ably/lib/http/Http.java
@@ -21,6 +21,15 @@ public void close() throws Exception {
asyncHttp.close();
}
+ /**
+ * [Internal Method]
+ *
+ * We use this method to implement proxy Realtime / Rest clients that add additional data to the underlying client.
+ */
+ public Http exchangeHttpCore(HttpCore httpCore) {
+ return new Http(asyncHttp.exchangeHttpCore(httpCore), new SyncHttpScheduler(httpCore));
+ }
+
public class Request {
private final Execute execute;
diff --git a/lib/src/main/java/io/ably/lib/http/HttpCore.java b/lib/src/main/java/io/ably/lib/http/HttpCore.java
index 470562b26..dce868d2b 100644
--- a/lib/src/main/java/io/ably/lib/http/HttpCore.java
+++ b/lib/src/main/java/io/ably/lib/http/HttpCore.java
@@ -10,6 +10,7 @@
import io.ably.lib.network.HttpRequest;
import io.ably.lib.network.HttpResponse;
import io.ably.lib.rest.Auth;
+import io.ably.lib.rest.DerivedClientOptions;
import io.ably.lib.transport.Defaults;
import io.ably.lib.transport.Hosts;
import io.ably.lib.types.AblyException;
@@ -68,6 +69,8 @@ public class HttpCore {
private final HttpEngine engine;
private HttpAuth proxyAuth;
+ private DerivedClientOptions derivedOptions;
+
/*************************
* Public API
*************************/
@@ -103,6 +106,18 @@ public HttpCore(ClientOptions options, Auth auth, PlatformAgentProvider platform
this.engine = engineFactory.create(new HttpEngineConfig(ClientOptionsUtils.convertToProxyConfig(options)));
}
+ private HttpCore(HttpCore underlyingHttpCore, DerivedClientOptions derivedOptions) {
+ this.options = underlyingHttpCore.options;
+ this.auth = underlyingHttpCore.auth;
+ this.platformAgentProvider = underlyingHttpCore.platformAgentProvider;
+ this.scheme = underlyingHttpCore.scheme;
+ this.port = underlyingHttpCore.port;
+ this.hosts = underlyingHttpCore.hosts;
+ this.proxyAuth = underlyingHttpCore.proxyAuth;
+ this.engine = underlyingHttpCore.engine;
+ this.derivedOptions = derivedOptions;
+ }
+
/**
* Make a synchronous HTTP request specified by URL and proxy, retrying if necessary on WWW-Authenticate
*
@@ -307,7 +322,7 @@ private Map collectRequestHeaders(URL url, String method, Param[
/* pass required headers */
requestHeaders.put(Defaults.ABLY_PROTOCOL_VERSION_HEADER, Defaults.ABLY_PROTOCOL_VERSION); // RSC7a
- requestHeaders.put(Defaults.ABLY_AGENT_HEADER, AgentHeaderCreator.create(options.agents, platformAgentProvider));
+ requestHeaders.put(Defaults.ABLY_AGENT_HEADER, AgentHeaderCreator.create(options.agents, platformAgentProvider, derivedOptions));
if (options.clientId != null)
requestHeaders.put(Defaults.ABLY_CLIENT_ID_HEADER, Base64Coder.encodeString(options.clientId));
@@ -455,6 +470,15 @@ private Response executeRequest(HttpRequest request) {
return response;
}
+ /**
+ * [Internal Method]
+ *
+ * We use this method to implement proxy Realtime / Rest clients that add additional data to the underlying client.
+ */
+ public HttpCore applyDerivedOptions(DerivedClientOptions derivedOptions) {
+ return new HttpCore(this, derivedOptions);
+ }
+
/**
* Interface for an entity that supplies an httpCore request body
*/
diff --git a/lib/src/main/java/io/ably/lib/http/HttpScheduler.java b/lib/src/main/java/io/ably/lib/http/HttpScheduler.java
index 343a7f728..1da80c526 100644
--- a/lib/src/main/java/io/ably/lib/http/HttpScheduler.java
+++ b/lib/src/main/java/io/ably/lib/http/HttpScheduler.java
@@ -440,7 +440,7 @@ public Future ablyHttpExecuteWithRetry(
return request;
}
- private final CloseableExecutor executor;
+ protected final CloseableExecutor executor;
private final HttpCore httpCore;
protected static final String TAG = HttpScheduler.class.getName();
diff --git a/lib/src/main/java/io/ably/lib/realtime/AblyRealtime.java b/lib/src/main/java/io/ably/lib/realtime/AblyRealtime.java
index 8e7c99a63..388d94323 100644
--- a/lib/src/main/java/io/ably/lib/realtime/AblyRealtime.java
+++ b/lib/src/main/java/io/ably/lib/realtime/AblyRealtime.java
@@ -7,6 +7,7 @@
import io.ably.lib.rest.AblyRest;
import io.ably.lib.rest.Auth;
+import io.ably.lib.rest.DerivedClientOptions;
import io.ably.lib.transport.ConnectionManager;
import io.ably.lib.types.AblyException;
import io.ably.lib.types.ChannelOptions;
@@ -83,6 +84,15 @@ public void onConnectionStateChanged(ConnectionStateListener.ConnectionStateChan
if(options.autoConnect) connection.connect();
}
+ /**
+ * Package-private constructor implementation to be able to have proxy based on this class
+ */
+ AblyRealtime(AblyRealtime underlyingClient, DerivedClientOptions derivedOptions) {
+ super(underlyingClient, derivedOptions);
+ this.channels = underlyingClient.channels;
+ this.connection = underlyingClient.connection;
+ }
+
/**
* Calls {@link Connection#connect} and causes the connection to open,
* entering the connecting state. Explicitly calling connect() is unnecessary
@@ -118,6 +128,15 @@ public void close() {
connection.close();
}
+ /**
+ * [Internal Method]
+ *
+ * We use this method to implement proxy Realtime / Rest clients that add additional data to the underlying client.
+ */
+ public AblyRealtime createDerivedClient(DerivedClientOptions derivedOptions) {
+ return new AblyRealtime(this, derivedOptions);
+ }
+
/**
* Authentication token has changed.
*/
diff --git a/lib/src/main/java/io/ably/lib/rest/AblyBase.java b/lib/src/main/java/io/ably/lib/rest/AblyBase.java
index c0c745a52..53dec5a4b 100644
--- a/lib/src/main/java/io/ably/lib/rest/AblyBase.java
+++ b/lib/src/main/java/io/ably/lib/rest/AblyBase.java
@@ -114,6 +114,20 @@ public AblyBase(ClientOptions options, PlatformAgentProvider platformAgentProvid
push = new Push(this);
}
+ /**
+ * We use empty constructor to be able to create proxy implementation of Realtime and Rest client
+ */
+ protected AblyBase(AblyBase underlyingClient, DerivedClientOptions derivedOptions) {
+ this.options = underlyingClient.options;
+ this.auth = underlyingClient.auth;
+ this.httpCore = underlyingClient.httpCore.applyDerivedOptions(derivedOptions);
+ this.http = underlyingClient.http.exchangeHttpCore(this.httpCore);
+ this.platform = underlyingClient.platform;
+ this.push = underlyingClient.push;
+ this.channels = underlyingClient.channels;
+ this.platformAgentProvider = underlyingClient.platformAgentProvider;
+ }
+
/**
* Causes the connection to close, entering the [{@link io.ably.lib.realtime.ConnectionState#closing} state.
* Once closed, the library does not attempt to re-establish the connection without an explicit call to
diff --git a/lib/src/main/java/io/ably/lib/rest/DerivedClientOptions.java b/lib/src/main/java/io/ably/lib/rest/DerivedClientOptions.java
new file mode 100644
index 000000000..b4955ec40
--- /dev/null
+++ b/lib/src/main/java/io/ably/lib/rest/DerivedClientOptions.java
@@ -0,0 +1,33 @@
+package io.ably.lib.rest;
+
+import java.util.HashMap;
+import java.util.Map;
+
+public final class DerivedClientOptions {
+ private final Map agents;
+
+ DerivedClientOptions(Map agents) {
+ this.agents = agents;
+ }
+
+ public static DerivedClientOptionsBuilder builder() {
+ return new DerivedClientOptionsBuilder();
+ }
+
+ public Map getAgents() {
+ return this.agents;
+ }
+
+ public static class DerivedClientOptionsBuilder {
+ private final Map agents = new HashMap<>();
+
+ public DerivedClientOptionsBuilder addAgent(String agent, String version) {
+ this.agents.put(agent, version);
+ return this;
+ }
+
+ public DerivedClientOptions build() {
+ return new DerivedClientOptions(this.agents);
+ }
+ }
+}
diff --git a/lib/src/main/java/io/ably/lib/util/AgentHeaderCreator.java b/lib/src/main/java/io/ably/lib/util/AgentHeaderCreator.java
index be13caef9..5ba1231aa 100644
--- a/lib/src/main/java/io/ably/lib/util/AgentHeaderCreator.java
+++ b/lib/src/main/java/io/ably/lib/util/AgentHeaderCreator.java
@@ -1,5 +1,6 @@
package io.ably.lib.util;
+import io.ably.lib.rest.DerivedClientOptions;
import io.ably.lib.transport.Defaults;
import java.util.Map;
@@ -15,7 +16,7 @@ public class AgentHeaderCreator {
*/
public static final String AGENT_DIVIDER = "/";
- public static String create(Map additionalAgents, PlatformAgentProvider platformAgentProvider) {
+ public static String create(Map additionalAgents, PlatformAgentProvider platformAgentProvider, DerivedClientOptions derivedOptions) {
StringBuilder agentStringBuilder = new StringBuilder();
agentStringBuilder.append(Defaults.ABLY_AGENT_VERSION);
if (additionalAgents != null && !additionalAgents.isEmpty()) {
@@ -27,9 +28,23 @@ public static String create(Map additionalAgents, PlatformAgentP
agentStringBuilder.append(AGENT_ENTRY_SEPARATOR);
agentStringBuilder.append(platformAgent);
}
+
+ if (derivedOptions != null) {
+ derivedOptions.getAgents().entrySet().forEach(entry -> {
+ agentStringBuilder.append(AGENT_ENTRY_SEPARATOR);
+ agentStringBuilder.append(entry.getKey());
+ agentStringBuilder.append(AGENT_DIVIDER);
+ agentStringBuilder.append(entry.getValue());
+ });
+ }
+
return agentStringBuilder.toString();
}
+ public static String create(Map additionalAgents, PlatformAgentProvider platformAgentProvider) {
+ return create(additionalAgents, platformAgentProvider, null);
+ }
+
private static String getAdditionalAgentEntries(Map additionalAgents) {
StringBuilder additionalAgentsBuilder = new StringBuilder();
for (String additionalAgentName : additionalAgents.keySet()) {
diff --git a/lib/src/test/java/io/ably/lib/test/realtime/RealtimeHttpHeaderTest.java b/lib/src/test/java/io/ably/lib/test/realtime/RealtimeHttpHeaderTest.java
index 5c46ad266..c61b795cc 100644
--- a/lib/src/test/java/io/ably/lib/test/realtime/RealtimeHttpHeaderTest.java
+++ b/lib/src/test/java/io/ably/lib/test/realtime/RealtimeHttpHeaderTest.java
@@ -2,6 +2,7 @@
import fi.iki.elonen.NanoHTTPD;
import io.ably.lib.realtime.AblyRealtime;
+import io.ably.lib.rest.DerivedClientOptions;
import io.ably.lib.test.common.ParameterizedTest;
import io.ably.lib.types.AblyException;
import io.ably.lib.types.ClientOptions;
@@ -17,6 +18,7 @@
import static org.junit.Assert.assertEquals;
import static org.junit.Assert.assertNotNull;
+import static org.junit.Assert.assertSame;
/**
* Test for correct version headers passed to websocket
@@ -155,8 +157,66 @@ public void realtime_websocket_param_test() {
}
}
+ /**
+ * Verify that correct version is used for realtime HTTP request
+ */
+ @Test
+ public void realtime_derived_client_headers_test() throws InterruptedException, AblyException {
+ AblyRealtime realtime;
+ /* Init values for local server */
+ String key = testVars.keys[0].keyStr;
+ ClientOptions opts = new ClientOptions(key);
+ opts.port = port;
+ opts.realtimeHost = "localhost";
+ opts.restHost = "localhost";
+ opts.tls = false;
+ opts.useBinaryProtocol = testParams.useBinaryProtocol;
+ opts.autoConnect = false;
+
+ server.resetRequestParameters();
+
+ realtime = new AblyRealtime(opts);
+
+ server.resetRequestHeaders();
+ DerivedClientOptions derivedOptions = DerivedClientOptions.builder().addAgent("chat-android", "0.1.2").build();
+ AblyRealtime derivedRealtime = realtime.createDerivedClient(derivedOptions);
+ try { derivedRealtime.time(); } catch (Exception e) {}
+ Map requestHeaders = tryGetServerRequestHeaders();
+
+ assertEquals("Verify correct lib version",
+ "ably-java/1.2.48 jre/" + System.getProperty("java.version") + " chat-android/0.1.2",
+ requestHeaders.get("ably-agent")
+ );
+
+ try { realtime.time(); } catch (Exception e) {}
+
+ requestHeaders = tryGetServerRequestHeaders();
+
+ assertEquals("Verify correct lib version",
+ "ably-java/1.2.48 jre/" + System.getProperty("java.version"),
+ requestHeaders.get("ably-agent")
+ );
+
+ assertSame(realtime.connection, derivedRealtime.connection);
+ assertSame(realtime.channels, derivedRealtime.channels);
+ }
+
+ private Map tryGetServerRequestHeaders() throws InterruptedException {
+ Map requestHeaders = null;
+
+ for (int i = 0; requestHeaders == null && i < 10; i++) {
+ Thread.sleep(100);
+ requestHeaders = server.getRequestHeaders();
+ }
+
+ assertNotNull("Verify connection attempt", requestHeaders);
+
+ return requestHeaders;
+ }
+
private static class SessionHandlerNanoHTTPD extends NanoHTTPD {
Map> requestParameters;
+ Map requestHeaders;
SessionHandlerNanoHTTPD(int port) {
super(port);
@@ -166,6 +226,8 @@ private static class SessionHandlerNanoHTTPD extends NanoHTTPD {
public Response serve(IHTTPSession session) {
if (requestParameters == null)
requestParameters = decodeParameters(session.getQueryParameterString());
+ if (requestHeaders == null)
+ requestHeaders = session.getHeaders();
return newFixedLengthResponse("Ignored response");
}
@@ -174,9 +236,17 @@ void resetRequestParameters() {
requestParameters = null;
}
+ void resetRequestHeaders() {
+ requestHeaders = null;
+ }
+
Map> getRequestParameters() {
return requestParameters;
}
+
+ Map getRequestHeaders() {
+ return requestHeaders;
+ }
}
}