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; + } } }