From 3745b92a796f2f2e99076dd92c56c7f4e7ca8f18 Mon Sep 17 00:00:00 2001 From: Quintin Willison Date: Thu, 1 Oct 2020 15:29:27 +0100 Subject: [PATCH 1/3] Convert tabs to spaces at the beginning of all lines in java files. On macOS, I used: find . -name '*.java' ! -type d -exec bash -c 'gexpand -i -t 4 "$0" > /tmp/e && mv /tmp/e "$0"' {} \; --- .../lib/test/android/AndroidPushTest.java | 3628 ++++----- .../ably/lib/test/android/AndroidSuite.java | 130 +- .../ably/lib/test/loader/ArgumentLoader.java | 8 +- .../ably/lib/test/loader/ResourceLoader.java | 36 +- .../platform/AndroidNetworkConnectivity.java | 100 +- .../java/io/ably/lib/platform/Platform.java | 72 +- .../push/AblyFirebaseInstanceIdService.java | 24 +- .../io/ably/lib/push/ActivationContext.java | 278 +- .../ably/lib/push/ActivationStateMachine.java | 1360 ++-- .../java/io/ably/lib/push/LocalDevice.java | 336 +- .../src/main/java/io/ably/lib/push/Push.java | 172 +- .../java/io/ably/lib/push/PushChannel.java | 328 +- .../java/io/ably/lib/realtime/Channel.java | 24 +- .../main/java/io/ably/lib/rest/AblyRest.java | 96 +- .../main/java/io/ably/lib/rest/Channel.java | 16 +- .../io/ably/lib/types/RegistrationToken.java | 54 +- .../java/io/ably/lib/util/IntentUtils.java | 36 +- .../java/io/ably/lib/platform/Platform.java | 12 +- java/src/main/java/io/ably/lib/push/Push.java | 6 +- .../java/io/ably/lib/realtime/Channel.java | 8 +- .../main/java/io/ably/lib/rest/AblyRest.java | 38 +- .../main/java/io/ably/lib/rest/Channel.java | 6 +- .../ably/lib/test/loader/ArgumentLoader.java | 6 +- .../ably/lib/test/loader/ResourceLoader.java | 34 +- .../java/io/ably/lib/debug/DebugOptions.java | 32 +- .../lib/http/AsyncHttpPaginatedQuery.java | 290 +- .../io/ably/lib/http/AsyncHttpScheduler.java | 32 +- .../io/ably/lib/http/AsyncPaginatedQuery.java | 66 +- .../io/ably/lib/http/BasePaginatedQuery.java | 636 +- lib/src/main/java/io/ably/lib/http/Http.java | 120 +- .../main/java/io/ably/lib/http/HttpAuth.java | 434 +- .../java/io/ably/lib/http/HttpConstants.java | 42 +- .../main/java/io/ably/lib/http/HttpCore.java | 1104 +-- .../java/io/ably/lib/http/HttpHelpers.java | 196 +- .../io/ably/lib/http/HttpPaginatedQuery.java | 284 +- .../java/io/ably/lib/http/HttpScheduler.java | 768 +- .../main/java/io/ably/lib/http/HttpUtils.java | 476 +- .../java/io/ably/lib/http/PaginatedQuery.java | 68 +- .../io/ably/lib/http/SyncHttpScheduler.java | 6 +- .../main/java/io/ably/lib/push/PushBase.java | 696 +- .../io/ably/lib/realtime/AblyRealtime.java | 366 +- .../io/ably/lib/realtime/ChannelBase.java | 2308 +++--- .../io/ably/lib/realtime/ChannelEvent.java | 16 +- .../io/ably/lib/realtime/ChannelState.java | 28 +- .../lib/realtime/ChannelStateListener.java | 120 +- .../ably/lib/realtime/CompletionListener.java | 116 +- .../java/io/ably/lib/realtime/Connection.java | 214 +- .../io/ably/lib/realtime/ConnectionEvent.java | 18 +- .../io/ably/lib/realtime/ConnectionState.java | 30 +- .../lib/realtime/ConnectionStateListener.java | 102 +- .../java/io/ably/lib/realtime/Presence.java | 2032 ++--- .../main/java/io/ably/lib/rest/AblyBase.java | 536 +- lib/src/main/java/io/ably/lib/rest/Auth.java | 2150 +++--- .../java/io/ably/lib/rest/ChannelBase.java | 380 +- .../java/io/ably/lib/rest/DeviceDetails.java | 250 +- .../ably/lib/transport/ConnectionManager.java | 3314 ++++---- .../java/io/ably/lib/transport/Defaults.java | 94 +- .../java/io/ably/lib/transport/Hosts.java | 310 +- .../io/ably/lib/transport/ITransport.java | 216 +- .../lib/transport/NetworkConnectivity.java | 118 +- .../lib/transport/WebSocketTransport.java | 620 +- .../java/io/ably/lib/types/AblyException.java | 86 +- .../lib/types/AsyncHttpPaginatedResponse.java | 66 +- .../ably/lib/types/AsyncPaginatedResult.java | 26 +- .../java/io/ably/lib/types/BaseMessage.java | 630 +- .../ably/lib/types/BasePaginatedResult.java | 28 +- .../main/java/io/ably/lib/types/Callback.java | 60 +- .../java/io/ably/lib/types/Capability.java | 320 +- .../java/io/ably/lib/types/ChannelMode.java | 42 +- .../io/ably/lib/types/ChannelOptions.java | 174 +- .../io/ably/lib/types/ChannelProperties.java | 14 +- .../java/io/ably/lib/types/ClientOptions.java | 362 +- .../io/ably/lib/types/ConnectionDetails.java | 116 +- .../io/ably/lib/types/DecodingContext.java | 48 +- .../java/io/ably/lib/types/DeltaExtras.java | 142 +- .../java/io/ably/lib/types/ErrorInfo.java | 340 +- .../java/io/ably/lib/types/ErrorResponse.java | 22 +- .../ably/lib/types/HttpPaginatedResponse.java | 38 +- .../main/java/io/ably/lib/types/Message.java | 540 +- .../lib/types/MessageDecodeException.java | 24 +- .../java/io/ably/lib/types/MessageExtras.java | 256 +- .../io/ably/lib/types/MessageSerializer.java | 328 +- .../io/ably/lib/types/PaginatedResult.java | 28 +- .../main/java/io/ably/lib/types/Param.java | 152 +- .../io/ably/lib/types/PresenceMessage.java | 414 +- .../io/ably/lib/types/PresenceSerializer.java | 210 +- .../io/ably/lib/types/ProtocolMessage.java | 530 +- .../io/ably/lib/types/ProtocolSerializer.java | 86 +- .../java/io/ably/lib/types/ProxyOptions.java | 12 +- .../io/ably/lib/types/PublishResponse.java | 272 +- .../main/java/io/ably/lib/types/Stats.java | 220 +- .../java/io/ably/lib/types/StatsReader.java | 36 +- .../io/ably/lib/util/CollectionUtils.java | 30 +- .../main/java/io/ably/lib/util/Crypto.java | 614 +- .../ably/lib/util/CurrentThreadExecutor.java | 10 +- .../java/io/ably/lib/util/EventEmitter.java | 186 +- .../main/java/io/ably/lib/util/JsonUtils.java | 60 +- lib/src/main/java/io/ably/lib/util/Log.java | 268 +- .../java/io/ably/lib/util/Multicaster.java | 18 +- .../java/io/ably/lib/util/Serialisation.java | 452 +- .../java/io/ably/lib/util/StringUtils.java | 16 +- .../java/io/ably/lib/test/common/Helpers.java | 1902 ++--- .../lib/test/common/ParameterizedTest.java | 134 +- .../java/io/ably/lib/test/common/Setup.java | 504 +- .../test/realtime/ConnectionManagerTest.java | 1354 ++-- .../lib/test/realtime/EventEmitterTest.java | 670 +- .../io/ably/lib/test/realtime/HostsTest.java | 320 +- .../lib/test/realtime/RealtimeAuthTest.java | 1670 ++-- .../realtime/RealtimeChannelHistoryTest.java | 2718 +++---- .../test/realtime/RealtimeChannelTest.java | 3762 ++++----- .../realtime/RealtimeConnectFailTest.java | 1016 +-- .../test/realtime/RealtimeConnectTest.java | 442 +- .../lib/test/realtime/RealtimeCryptoTest.java | 2158 +++--- .../realtime/RealtimeDeltaDecoderTest.java | 330 +- .../test/realtime/RealtimeHttpHeaderTest.java | 312 +- .../lib/test/realtime/RealtimeInitTest.java | 342 +- .../lib/test/realtime/RealtimeJWTTest.java | 746 +- .../test/realtime/RealtimeMessageTest.java | 1836 ++--- .../realtime/RealtimePresenceHistoryTest.java | 2514 +++---- .../test/realtime/RealtimePresenceTest.java | 6704 ++++++++--------- .../lib/test/realtime/RealtimeReauthTest.java | 928 +-- .../test/realtime/RealtimeRecoverTest.java | 778 +- .../lib/test/realtime/RealtimeResumeTest.java | 1426 ++-- .../ably/lib/test/realtime/RealtimeSuite.java | 66 +- .../io/ably/lib/test/rest/HttpHeaderTest.java | 180 +- .../java/io/ably/lib/test/rest/HttpTest.java | 2682 +++---- .../ably/lib/test/rest/RestAppStatsTest.java | 750 +- .../lib/test/rest/RestAuthAttributeTest.java | 478 +- .../io/ably/lib/test/rest/RestAuthTest.java | 3756 ++++----- .../lib/test/rest/RestCapabilityTest.java | 538 +- .../test/rest/RestChannelBulkPublishTest.java | 398 +- .../lib/test/rest/RestChannelHistoryTest.java | 1178 +-- .../lib/test/rest/RestChannelPublishTest.java | 798 +- .../ably/lib/test/rest/RestChannelTest.java | 52 +- .../io/ably/lib/test/rest/RestCryptoTest.java | 480 +- .../io/ably/lib/test/rest/RestErrorTest.java | 282 +- .../io/ably/lib/test/rest/RestInitTest.java | 572 +- .../io/ably/lib/test/rest/RestJWTTest.java | 276 +- .../ably/lib/test/rest/RestPresenceTest.java | 612 +- .../io/ably/lib/test/rest/RestProxyTest.java | 350 +- .../io/ably/lib/test/rest/RestPushTest.java | 1562 ++-- .../ably/lib/test/rest/RestRequestTest.java | 1356 ++-- .../java/io/ably/lib/test/rest/RestSuite.java | 68 +- .../io/ably/lib/test/rest/RestTimeTest.java | 138 +- .../io/ably/lib/test/rest/RestTokenTest.java | 430 +- .../lib/test/util/MockWebsocketFactory.java | 292 +- .../io/ably/lib/test/util/StatsWriter.java | 6 +- .../io/ably/lib/test/util/StatusHandler.java | 72 +- .../java/io/ably/lib/test/util/TestCases.java | 106 +- .../io/ably/lib/test/util/TimeHandler.java | 48 +- .../io/ably/lib/test/util/TokenServer.java | 240 +- .../io/ably/lib/transport/DefaultsTest.java | 8 +- .../io/ably/lib/types/MessageExtrasTest.java | 76 +- .../io/ably/lib/util/CryptoMessageTest.java | 252 +- .../java/io/ably/lib/util/CryptoTest.java | 338 +- 155 files changed, 41839 insertions(+), 41839 deletions(-) diff --git a/android/src/androidTest/java/io/ably/lib/test/android/AndroidPushTest.java b/android/src/androidTest/java/io/ably/lib/test/android/AndroidPushTest.java index 65cc7de01..b1a813beb 100644 --- a/android/src/androidTest/java/io/ably/lib/test/android/AndroidPushTest.java +++ b/android/src/androidTest/java/io/ably/lib/test/android/AndroidPushTest.java @@ -62,1818 +62,1818 @@ public class AndroidPushTest extends AndroidTestCase { - private class TestActivation { - private Helpers.RawHttpTracker httpTracker; - private AblyRest rest; - private TestActivationContext activationContext; - private TestActivationStateMachine machine; - private AblyRest adminRest; - - TestActivation() { - this(null); - } - - public class Options { - public DebugOptions clientOptions; - public boolean clearPersisted = true; - public TestActivationContext activationContext; - } - - TestActivation(Helpers.AblyFunction configure) { - try { - httpTracker = new Helpers.RawHttpTracker(); - DebugOptions options = createOptions(testVars.keys[0].keyStr); - options.httpListener = httpTracker; - options.useTokenAuth = true; - Context context = getContext(); - - Options activationOptions = new Options(); - activationOptions.clientOptions = options; - activationOptions.activationContext = new TestActivationContext(context.getApplicationContext()); - if (configure != null) { - configure.apply(activationOptions); - } - activationContext = activationOptions.activationContext; - options = activationOptions.clientOptions; - - ActivationContext.setActivationContext(context.getApplicationContext(), activationContext); - if (activationOptions.clearPersisted) { - activationContext.reset(); - } - machine = new TestActivationStateMachine(activationContext); - activationContext.setActivationStateMachine(machine); - - rest = new AblyRest(options); - rest.auth.authorize(null, null); - activationContext.setAbly(rest); - rest.setAndroidContext(context); - - adminRest = new AblyRest(options); - adminRest.auth.authorize(new Auth.TokenParams() {{ - clientId = Auth.WILDCARD_CLIENTID; - }}, null); - } catch(AblyException e) {} - } - - private void registerAndWait() throws AblyException { - AsyncWaiter requestWaiter = httpTracker.getRequestWaiter(); - AsyncWaiter activateWaiter = broadcastWaiter("PUSH_ACTIVATE"); - - rest.push.getActivationContext().onNewRegistrationToken(RegistrationToken.Type.FCM, "testToken"); - rest.push.activate(false); - - activateWaiter.waitFor(); - assertNull(activateWaiter.error); - requestWaiter.waitFor(); - Helpers.RawHttpRequest request = requestWaiter.result; - Log.d("AndroidPushTest"," registration method: " + request.method); - assertTrue(request.method.equals("PATCH") || request.method.equals("POST")); - assertTrue(request.url.getPath().startsWith("/push/deviceRegistrations")); - } - - private void moveToAfterRegistrationUpdateFailed() throws AblyException { - // Move to AfterRegistrationSyncFailed by forcing an update failure. - - rest.push.activate(true); // Just to set useCustomRegistrar to true. - AsyncWaiter customRegisterer = broadcastWaiter("PUSH_REGISTER_DEVICE"); - rest.push.getActivationContext().onNewRegistrationToken(RegistrationToken.Type.FCM, "testTokenFailed"); - customRegisterer.waitFor(); - - CompletionWaiter failedWaiter = machine.getTransitionedToWaiter(AfterRegistrationSyncFailed.class); - - Intent intent = new Intent(); - IntentUtils.addErrorInfo(intent, new ErrorInfo("intentional", 123)); - sendBroadcast("PUSH_DEVICE_REGISTERED", intent); - - failedWaiter.waitFor(); - } - } - - public static Test suite() { - TestSuite suite = new TestSuite(); - suite.addTest(new TestSetup(new TestSuite(AndroidPushTest.class)) { - protected void setUp() throws Exception { - setUpBeforeClass(); - } - protected void tearDown() throws Exception { - tearDownAfterClass(); - } - }); - return suite; - } - - // RSH2a - public void test_push_activate() throws InterruptedException, AblyException { - TestActivation activation = new TestActivation(); - BlockingQueue events = activation.machine.getEventReceiver(2); // CalledActivate + GotPushDeviceDetails - assertInstanceOf(ActivationStateMachine.NotActivated.class, activation.machine.current); - activation.rest.push.activate(); - Event event = events.poll(10, TimeUnit.SECONDS); - assertInstanceOf(CalledActivate.class, event); - } - - // RSH2b - public void test_push_deactivate() throws InterruptedException, AblyException { - TestActivation activation = new TestActivation(); - BlockingQueue events = activation.machine.getEventReceiver(1); - assertInstanceOf(NotActivated.class, activation.machine.current); - activation.rest.push.deactivate(); - Event event = events.poll(10, TimeUnit.SECONDS); - assertInstanceOf(CalledDeactivate.class, event); - } - - // RSH2c / RSH8g - public void test_push_onNewRegistrationToken() throws InterruptedException, AblyException { - TestActivation activation = new TestActivation(); - BlockingQueue events = activation.machine.getEventReceiver(1); - final BlockingQueue> tokenCallbacks = new ArrayBlockingQueue<>(1) ; - - activation.activationContext.onGetRegistrationToken = new Helpers.AblyFunction, Void>() { - @Override - public Void apply(Callback callback) throws AblyException { - try { - tokenCallbacks.put(callback); - } catch (InterruptedException e) { - throw AblyException.fromThrowable(e); - } - return null; - } - }; - - activation.rest.push.activate(true); // This registers the listener for registration tokens. - assertInstanceOf(CalledActivate.class, events.poll(10, TimeUnit.SECONDS)); - - Callback tokenCallback = tokenCallbacks.poll(10, TimeUnit.SECONDS); - - tokenCallback.onSuccess("foo"); - assertInstanceOf(GotPushDeviceDetails.class, events.poll(10, TimeUnit.SECONDS)); - - tokenCallback.onSuccess("bar"); - assertInstanceOf(GotPushDeviceDetails.class, events.poll(10, TimeUnit.SECONDS)); - } - - // RSH2d / RSH8h - public void test_push_onNewRegistrationTokenFailed() throws InterruptedException, AblyException { - TestActivation activation = new TestActivation(); - BlockingQueue events = activation.machine.getEventReceiver(1); - final BlockingQueue> tokenCallbacks = new ArrayBlockingQueue<>(1) ; - - activation.activationContext.onGetRegistrationToken = new Helpers.AblyFunction, Void>() { - @Override - public Void apply(Callback callback) throws AblyException { - try { - tokenCallbacks.put(callback); - } catch (InterruptedException e) { - throw AblyException.fromThrowable(e); - } - return null; - } - }; - - activation.rest.push.activate(true); // This registers the listener for registration tokens. - assertInstanceOf(CalledActivate.class, events.poll(10, TimeUnit.SECONDS)); - - Callback tokenCallback = tokenCallbacks.poll(10, TimeUnit.SECONDS); - - tokenCallback.onError(new ErrorInfo("foo", 123, 123)); - Event event = events.poll(10, TimeUnit.SECONDS); - assertInstanceOf(ActivationStateMachine.GettingPushDeviceDetailsFailed.class, event); - assertEquals(123,((ActivationStateMachine.GettingPushDeviceDetailsFailed) event).reason.code); - } - - // RSH2e / RSH8i - public void test_push_syncOnStartup() throws InterruptedException, AblyException { - final BlockingQueue> tokenCallbacks = new ArrayBlockingQueue<>(1) ; - - Helpers.AblyFunction configureActivation = new Helpers.AblyFunction() { - @Override - public Void apply(TestActivation.Options options) throws AblyException { - options.activationContext.onGetRegistrationToken = new Helpers.AblyFunction, Void>() { - @Override - public Void apply(Callback callback) throws AblyException { - try { - tokenCallbacks.put(callback); - } catch (InterruptedException e) { - throw AblyException.fromThrowable(e); - } - return null; - } - }; - return null; - } - }; - - TestActivation activation = new TestActivation(configureActivation); - - // Fake-register the device. - AsyncWaiter customRegisterer = broadcastWaiter("PUSH_REGISTER_DEVICE"); - AsyncWaiter activated = broadcastWaiter("PUSH_ACTIVATE"); - activation.rest.push.activate(true); - Callback tokenCallback = tokenCallbacks.take(); - tokenCallback.onSuccess("foo"); - customRegisterer.waitFor(); - Intent intent = new Intent(); - intent.putExtra("deviceIdentityToken", "fakeToken"); - sendBroadcast("PUSH_DEVICE_REGISTERED", intent); - activated.waitFor(); - - // Now just creating a new library instance should request the current token. - - BlockingQueue events = activation.machine.getEventReceiver(1); - - configureActivation = new Helpers.AblyFunction() { - @Override - public Void apply(TestActivation.Options options) throws AblyException { - options.clearPersisted = false; - options.activationContext.onGetRegistrationToken = new Helpers.AblyFunction, Void>() { - @Override - public Void apply(Callback callback) throws AblyException { - try { - tokenCallbacks.put(callback); - } catch (InterruptedException e) { - throw AblyException.fromThrowable(e); - } - return null; - } - }; - return null; - } - }; - - activation = new TestActivation(configureActivation); - tokenCallback = tokenCallbacks.take(); - - // With the same token, nothing happens. - events = activation.machine.getEventReceiver(1); - tokenCallback.onSuccess("foo"); - assertNull(events.poll(100, TimeUnit.MILLISECONDS)); - - // Do the same with a different token, expect a GotPushDeviceDetails. - activation = new TestActivation(configureActivation); - events = activation.machine.getEventReceiver(1); - tokenCallback = tokenCallbacks.take(); - tokenCallback.onSuccess("qux"); - Event event = events.poll(100, TimeUnit.MILLISECONDS); - assertInstanceOf(GotPushDeviceDetails.class, event); - } - - // RSH8a, RSH8c - public void test_push_device_persistence() throws InterruptedException, AblyException { - TestActivation activation = new TestActivation(new Helpers.AblyFunction() { - @Override - public Void apply(TestActivation.Options options) throws AblyException { - options.clientOptions.clientId = "testClient"; - return null; - } - }); - - // Fake-register the device. - AsyncWaiter customRegisterer = broadcastWaiter("PUSH_REGISTER_DEVICE"); - AsyncWaiter activated = broadcastWaiter("PUSH_ACTIVATE"); - activation.rest.push.activate(true); - - customRegisterer.waitFor(); - - LocalDevice device = activation.rest.device(); - assertEquals("testClient", device.clientId); - assertNotNull(device.id); - assertNotNull(device.deviceSecret); - - Intent intent = new Intent(); - intent.putExtra("deviceIdentityToken", "fakeToken"); - sendBroadcast("PUSH_DEVICE_REGISTERED", intent); - activated.waitFor(); - - assertEquals("fakeToken", activation.rest.device().deviceIdentityToken); - - // Load from persisted state. - activation = new TestActivation(new Helpers.AblyFunction() { - @Override - public Void apply(TestActivation.Options options) throws AblyException { - options.clearPersisted = false; - return null; - } - }); - LocalDevice newDevice = activation.rest.device(); - assertEquals("fakeToken", newDevice.deviceIdentityToken); - assertEquals(device.id, newDevice.id); - assertEquals(device.deviceSecret, newDevice.deviceSecret); - assertEquals(device.clientId, newDevice.clientId); - } - - // RSH8d - public void test_push_late_clientId_persisted() throws InterruptedException, AblyException { - TestActivation activation = new TestActivation(); - - assertNull(activation.rest.auth.clientId); - assertNull(activation.rest.device().clientId); - - Auth.TokenParams params = new Auth.TokenParams(); - params.clientId = "testClient"; - activation.rest.auth.authorize(params, null); - - assertEquals("testClient", activation.rest.auth.clientId); - assertEquals("testClient", activation.rest.device().clientId); - - activation = new TestActivation(new Helpers.AblyFunction() { - @Override - public Void apply(TestActivation.Options options) throws AblyException { - options.clearPersisted = false; - return null; - } - }); - assertEquals("testClient", activation.rest.device().clientId); - } - - // RSH8e - public void test_push_late_clientId_emits_GotPushDeviceDetails() throws InterruptedException, AblyException { - TestActivation activation = new TestActivation(); - - // Fake-register the device. - AsyncWaiter customRegisterer = broadcastWaiter("PUSH_REGISTER_DEVICE"); - AsyncWaiter activated = broadcastWaiter("PUSH_ACTIVATE"); - activation.rest.push.activate(true); - customRegisterer.waitFor(); - Intent intent = new Intent(); - intent.putExtra("deviceIdentityToken", "fakeToken"); - sendBroadcast("PUSH_DEVICE_REGISTERED", intent); - activated.waitFor(); - - BlockingQueue events = activation.machine.getEventReceiver(1); - - Auth.TokenParams params = new Auth.TokenParams(); - params.clientId = "testClient"; - activation.rest.auth.authorize(params, null); - - Event event = events.poll(100, TimeUnit.MILLISECONDS); - assertInstanceOf(GotPushDeviceDetails.class, event); - } - - // RSH8f - public void test_push_clientId_from_server() throws InterruptedException, AblyException { - TestActivation activation = new TestActivation(); - - JsonObject body = new JsonObject(); - body.addProperty("clientId", "testClient"); - JsonObject fakeToken = new JsonObject(); - fakeToken.addProperty("token", "fakeToken"); - body.add("deviceIdentityToken", fakeToken); - HttpCore.Response response = new HttpCore.Response(); - response.statusCode = 200; - response.statusLine = "OK"; - response.contentType = "application/json"; - response.body = gson.toJson(body).getBytes(); - response.contentLength = response.body.length; - activation.httpTracker.mockResponse = response; - - try { - AsyncWaiter activated = broadcastWaiter("PUSH_ACTIVATE"); - activation.rest.push.activate(false); - activated.waitFor(); - } finally { - activation.adminRest.push.admin.deviceRegistrations.remove(activation.rest.device().id); - } - - assertEquals("testClient", activation.rest.device().clientId); - } - - // RSH3a1 - public void test_NotActivated_on_CalledDeactivate() { - TestActivation activation = new TestActivation(); - - ActivationStateMachine.State state = new NotActivated(activation.machine); - - final Helpers.AsyncWaiter waiter = broadcastWaiter("PUSH_DEACTIVATE"); - - State to = state.transition(new CalledDeactivate()); - - // RSH3a1a - waiter.waitFor(); - assertNull(waiter.error); - - // RSH3a1b - assertInstanceOf(NotActivated.class, to); - } - - // RSH3a2a - public void test_NotActivated_on_CalledActivate_with_DeviceToken() throws Exception { - class TestCase extends TestCases.Base { - private final String persistedClientId; - private final String instanceClientId; - private final ErrorInfo syncError; - private final boolean useCustomRegistrar; - private final Class expectedEvent; - private final Class expectedState; - private final Integer expectedErrorCode; - - public TestCase( - String name, - String persistedClientId, - String instanceClientId, - boolean useCustomRegistrar, - ErrorInfo syncError, - Class expectedEvent, - Class expectedState, - Integer expectedErrorCode - ) { - super(name, null); - this.persistedClientId = persistedClientId; - this.instanceClientId = instanceClientId; - this.useCustomRegistrar = useCustomRegistrar; - this.syncError = syncError; - this.expectedEvent = expectedEvent; - this.expectedState = expectedState; - this.expectedErrorCode = expectedErrorCode; - } - - @Override - public void run() throws Exception { - // Register local device before doing anything, in order to trigger RSH3a2a. - TestActivation activation = new TestActivation(new Helpers.AblyFunction() { - @Override - public Void apply(TestActivation.Options options) throws AblyException { - options.clientOptions.clientId = persistedClientId; - return null; - } - }); - - try { - Helpers.AsyncWaiter activateCallback = broadcastWaiter("PUSH_ACTIVATE"); - activation.rest.push.activate(false); - activateCallback.waitFor(); - - LocalDevice device = activation.rest.push.getLocalDevice(); - assertNotNull(device.id); - assertNotNull(device.deviceIdentityToken); - assertEquals(persistedClientId, device.clientId); - - - // Now use a new instance, to force persistence consistency checking. - activation = new TestActivation(new Helpers.AblyFunction() { - @Override - public Void apply(TestActivation.Options options) throws AblyException { - options.clientOptions.clientId = instanceClientId; - options.clearPersisted = false; - return null; - } - }); - - Helpers.AsyncWaiter registerCallback = useCustomRegistrar ? broadcastWaiter("PUSH_REGISTER_DEVICE") : null; - activateCallback = broadcastWaiter("PUSH_ACTIVATE"); - - CompletionWaiter calledActivateHandled = activation.machine.getEventHandledWaiter(CalledActivate.class); - Helpers.AsyncWaiter requestWaiter = null; - - if (!useCustomRegistrar) { - if (syncError != null) { - activation.httpTracker.mockResponse = Helpers.httpResponseFromErrorInfo(syncError); - } - - requestWaiter = activation.httpTracker.getRequestWaiter(); - // Block until we've checked the intermediate WaitingForRegistrationSync state, - // before the request's response causes another state transition. - // Otherwise, our test would be racing against the request. - activation.httpTracker.lockRequests(); - } - - activation.rest.push.activate(useCustomRegistrar); - calledActivateHandled.waitFor(); - - // RSH3a2a1: SyncRegistrationFailed may be enqueued (synchronously). In that - // case, register callback or PUT request won't be invoked, and we'll go - // synchronously to AfterRegistrationSyncFailed. - if (activation.machine.current instanceof WaitingForRegistrationSync) { - if (useCustomRegistrar) { - // RSH3a2a2 - registerCallback.waitFor(); - assertNull(registerCallback.error); - } else { - // RSH3a2a3 - requestWaiter.waitFor(); - Helpers.RawHttpRequest request = requestWaiter.result; - assertEquals("PUT", request.method); - assertEquals("/push/deviceRegistrations/" + device.id, request.url.getPath()); - } - - // RSH3a2a4 - assertSize(0, activation.machine.pendingEvents); - assertInstanceOf(WaitingForRegistrationSync.class, activation.machine.current); - - // Now wait for next event, when we may have an error. - - CompletionWaiter handled = activation.machine.getEventHandledWaiter(); - BlockingQueue events = activation.machine.getEventReceiver(1); - - if (useCustomRegistrar) { - Intent intent = new Intent(); - if (syncError != null) { - IntentUtils.addErrorInfo(intent, syncError); - } - sendBroadcast("PUSH_DEVICE_REGISTERED", intent); - } else { - activation.httpTracker.unlockRequests(); - } - - assertInstanceOf(expectedEvent, events.poll(10, TimeUnit.SECONDS)); - assertNull(handled.waitFor()); - } // else: RSH3a2a1 validation failed - - // RSH3e2 or RSH3e3 - activateCallback.waitFor(); - if (expectedErrorCode != null) { - assertNotNull(activateCallback.error); - assertEquals(expectedErrorCode.intValue(), activateCallback.error.code); - } else { - assertNull(activateCallback.error); - } - assertInstanceOf(expectedState, activation.machine.current); - } finally { - activation.httpTracker.unlockRequests(); - /* delete the registration without sending (invalid) local device credentials */ - LocalDevice localDevice = activation.rest.push.getLocalDevice(); - String deviceId = localDevice.id; - localDevice.reset(); - activation.rest.push.admin.deviceRegistrations.remove(deviceId); - } - } - } - - TestCases testCases = new TestCases(); - - // RSH3a2a1, RSH3a2a4, RSH3e3 - testCases.add(new TestCase( - "clientId mismatch", - "testClientId", - "otherClientId", - false, - null, - SyncRegistrationFailed.class, - AfterRegistrationSyncFailed.class, - 61002 - )); - - // RSH3a2a1, RSH3a2a2, RSH3a2a4, RSH3e2 - testCases.add(new TestCase( - "ok with custom registerer", - "testClientId", - "testClientId", - true, - null, - RegistrationSynced.class, - WaitingForNewPushDeviceDetails.class, - null - )); - - // RSH3a2a1, RSH3a2a2, RSH3a2a4, RSH3e3 - testCases.add(new TestCase( - "failing with custom registerer", - "testClientId", - "testClientId", - true, - new ErrorInfo("test error", 123, 123), - SyncRegistrationFailed.class, - AfterRegistrationSyncFailed.class, - 123 - )); - - // RSH3a2a1, RSH3a2a3, RSH3a2a4, RSH3e2 - testCases.add(new TestCase( - "ok without custom registerer", - "testClientId", - "testClientId", - false, - null, - RegistrationSynced.class, - WaitingForNewPushDeviceDetails.class, - null - )); - - // RSH3a2a1, RSH3a2a3, RSH3a2a4, RSH3e3 - testCases.add(new TestCase( - "failing without custom registerer", - "testClientId", - "testClientId", - false, - new ErrorInfo("test error", 123, 123), - SyncRegistrationFailed.class, - AfterRegistrationSyncFailed.class, - 123 - )); - - testCases.run(); - } - - // RSH3a3a - public void test_NotActivated_on_GotPushDeviceDetails() throws InterruptedException { - TestActivation activation = new TestActivation(); - State state = new NotActivated(activation.machine); - - State to = state.transition(new GotPushDeviceDetails()); - - assertSize(0, activation.machine.pendingEvents); - assertInstanceOf(NotActivated.class, to); - } - - // RSH3a2b - public void test_NotActivated_on_CalledActivate_with_registrationToken() throws InterruptedException, AblyException { - TestActivation activation = new TestActivation(); - activation.rest.push.getActivationContext().onNewRegistrationToken(RegistrationToken.Type.FCM, "testToken"); - - State state = new NotActivated(activation.machine); - State to = state.transition(new CalledActivate()); - - assertSize(1, activation.machine.pendingEvents); - assertInstanceOf(GotPushDeviceDetails.class, activation.machine.pendingEvents.getLast()); - - assertInstanceOf(WaitingForPushDeviceDetails.class, to); - - // RSH8b - LocalDevice device = activation.rest.device(); - assertNotNull(device.id); - assertNotNull(device.deviceSecret); - } - - // RSH3a2c - public void test_NotActivated_on_CalledActivate_without_registrationToken() throws InterruptedException { - TestActivation activation = new TestActivation(); - State state = new NotActivated(activation.machine); - State to = state.transition(new CalledActivate()); - - assertSize(0, activation.machine.pendingEvents); - - assertInstanceOf(WaitingForPushDeviceDetails.class, to); - } - - // RSH3b1 - public void test_WaitingForPushDeviceDetails_on_CalledActivate() { - TestActivation activation = new TestActivation(); - State state = new WaitingForPushDeviceDetails(activation.machine); - State to = state.transition(new CalledActivate()); - - assertSize(0, activation.machine.pendingEvents); - - // RSH3b1a - assertInstanceOf(WaitingForPushDeviceDetails.class, to); - } - - // RSH3b2 - public void test_WaitingForPushDeviceDetails_on_CalledDeactivate() { - TestActivation activation = new TestActivation(); - State state = new WaitingForPushDeviceDetails(activation.machine); - - final Helpers.AsyncWaiter waiter = broadcastWaiter("PUSH_DEACTIVATE"); - - State to = state.transition(new CalledDeactivate()); - - // RSH3b2a - waiter.waitFor(); - assertNull(waiter.error); - - assertSize(0, activation.machine.pendingEvents); - - // RSH3b2b - assertInstanceOf(NotActivated.class, to); - } - - // RSH3b3 - public void test_WaitingForPushDeviceDetails_on_GotPushDeviceDetails() throws Exception { - class TestCase extends TestCases.Base { - private final ErrorInfo registerError; - private final boolean useCustomRegistrar; - private final String deviceIdentityToken; - private final Class expectedEvent; - private final Class expectedState; - protected TestActivation activation; - - public TestCase(String name, boolean useCustomRegistrar, ErrorInfo error, String deviceIdentityToken, Class expectedEvent, Class expectedState) { - super(name, null); - this.useCustomRegistrar = useCustomRegistrar; - this.registerError = error; - this.deviceIdentityToken = deviceIdentityToken; - this.expectedEvent = expectedEvent; - this.expectedState = expectedState; - } - - @Override - public void run() throws Exception { - try { - - activation = new TestActivation(); - activation.activationContext.onGetRegistrationToken = new Helpers.AblyFunction, Void>() { - @Override - public Void apply(Callback callback) throws AblyException { - // Ignore request; will send event manually below. - return null; - } - }; - final Helpers.AsyncWaiter registerCallback = useCustomRegistrar ? broadcastWaiter("PUSH_REGISTER_DEVICE") : null; - final Helpers.AsyncWaiter activateCallback = broadcastWaiter("PUSH_ACTIVATE"); - - // Will move to WaitingForPushDeviceDetails. - activation.rest.push.activate(useCustomRegistrar); - - CompletionWaiter handled = activation.machine.getEventHandledWaiter(GotPushDeviceDetails.class); - Helpers.AsyncWaiter requestWaiter = null; - - if (!useCustomRegistrar) { - if (registerError != null) { - activation.httpTracker.mockResponse = Helpers.httpResponseFromErrorInfo(registerError); - } - - requestWaiter = activation.httpTracker.getRequestWaiter(); - // Block until we've checked the intermediate WaitingForDeviceRegistration state, - // before the request's response causes another state transition. - // Otherwise, our test would be racing against the request. - activation.httpTracker.lockRequests(); - } - - // Will send GotPushDeviceDetails event. - activation.rest.push.getActivationContext().onNewRegistrationToken(RegistrationToken.Type.FCM, "testToken"); - - handled.waitFor(); - - if (useCustomRegistrar) { - // RSH3b3a - registerCallback.waitFor(); - assertNull(registerCallback.error); - } else { - // RSH3b3b - requestWaiter.waitFor(); - Helpers.RawHttpRequest request = requestWaiter.result; - assertEquals("POST", request.method); - assertEquals("/push/deviceRegistrations", request.url.getPath()); - } - - // RSH3b3d - assertSize(0, activation.machine.pendingEvents); - assertInstanceOf(WaitingForDeviceRegistration.class, activation.machine.current); - - // Now wait for next event, when we've got an deviceIdentityToken or an error. - handled = activation.machine.getEventHandledWaiter(); - BlockingQueue events = activation.machine.getEventReceiver(1); - - if (useCustomRegistrar) { - Intent intent = new Intent(); - if (registerError != null) { - IntentUtils.addErrorInfo(intent, registerError); - } else { - intent.putExtra("deviceIdentityToken", deviceIdentityToken); - } - sendBroadcast("PUSH_DEVICE_REGISTERED", intent); - } else { - activation.httpTracker.unlockRequests(); - } - - assertInstanceOf(expectedEvent, events.poll(10, TimeUnit.SECONDS)); - assertNull(handled.waitFor()); - - // RSH3c2a - if (useCustomRegistrar) { - assertEquals(deviceIdentityToken, activation.rest.push.getLocalDevice().deviceIdentityToken); - } else if (registerError == null) { - // No error expected, so deviceIdentityToken should've been set by the server. - assertNotNull(activation.rest.push.getLocalDevice().deviceIdentityToken); - - } - - // RSH3c2b, RSH3c3a - activateCallback.waitFor(); - assertEquals(registerError, activateCallback.error); - assertInstanceOf(expectedState, activation.machine.current); - } finally { - activation.httpTracker.unlockRequests(); - /* delete the registration without sending (invalid) local device credentials */ - LocalDevice localDevice = activation.rest.push.getLocalDevice(); - String deviceId = localDevice.id; - localDevice.reset(); - activation.rest.push.admin.deviceRegistrations.remove(deviceId); - } - } - } - - TestCases testCases = new TestCases(); - - // RSH3c2 - testCases.add(new TestCase( - "ok with custom registerer", - true, - null, "testDeviceToken", - GotDeviceRegistration.class, // RSH3b3c - WaitingForNewPushDeviceDetails.class /* RSH3c2c */)); - - testCases.add(new TestCase( - "ok with default registerer", - false, - null, "testDeviceToken", - ActivationStateMachine.GotDeviceRegistration.class, // RSH3b3c - WaitingForNewPushDeviceDetails.class /* RSH3c2c */)); - - // RSH3c3 - testCases.add(new TestCase( - "failing with custom registerer", - true, - new ErrorInfo("testError", 123), null, - GettingDeviceRegistrationFailed.class, // RSH3b3c - NotActivated.class /* RSH3c3b */)); - - testCases.add(new TestCase( - "failing with default registerer", - false, - new ErrorInfo("testError", 123), null, - GettingDeviceRegistrationFailed.class, // RSH3b3c - NotActivated.class /* RSH3c3b */)); - - testCases.run(); - } - - // RSH3c1 - public void test_WaitingForDeviceRegistration_on_CalledActivate() { - TestActivation activation = new TestActivation(); - State state = new WaitingForDeviceRegistration(activation.machine); - State to = state.transition(new CalledActivate()); - - assertSize(0, activation.machine.pendingEvents); - - // RSH3c1a - assertInstanceOf(WaitingForDeviceRegistration.class, to); - } - - // RSH3d1 - public void test_WaitingForNewPushDeviceDetails_on_CalledActivate() { - TestActivation activation = new TestActivation(); - State state = new WaitingForNewPushDeviceDetails(activation.machine); - - final Helpers.AsyncWaiter waiter = broadcastWaiter("PUSH_ACTIVATE"); - - State to = state.transition(new CalledActivate()); - - // RSH3d1a - waiter.waitFor(); - assertNull(waiter.error); - - assertSize(0, activation.machine.pendingEvents); - - // RSH3d1b - assertInstanceOf(WaitingForNewPushDeviceDetails.class, to); - } - - // RSH3d2 - public void test_WaitingForNewPushDeviceDetails_on_CalledDeactivate() throws Exception { - new DeactivateTest(WaitingForNewPushDeviceDetails.class) { - @Override - protected void setUpMachineState(TestCase testCase) throws AblyException { - testCase.testActivation.registerAndWait(); - } - }.run(); - } - - // RSH3d3 - public void test_WaitingForNewPushDeviceDetails_on_GotPushDeviceDetails() throws Exception { - new UpdateRegistrationTest() { - @Override - protected void setUpMachineState(TestCase testCase) throws AblyException { - testCase.testActivation.registerAndWait(); - testCase.testActivation.rest.push.activate(testCase.useCustomRegistrar); - } - }.run(); - } - - // RSH3e1 - public void test_WaitingForRegistrationUpdate_on_CalledActivate() { - TestActivation activation = new TestActivation(); - State state = new WaitingForRegistrationSync(activation.machine, null); - - final AsyncWaiter waiter = broadcastWaiter("PUSH_ACTIVATE"); - - State to = state.transition(new CalledActivate()); - - // RSH3e1a - waiter.waitFor(); - assertNull(waiter.error); - - assertSize(0, activation.machine.pendingEvents); - - // RSH3e1b - assertInstanceOf(WaitingForRegistrationSync.class, to); - } - - // RSH3e2 - public void test_WaitingForRegistrationUpdate_on_RegistrationUpdated() { - TestActivation activation = new TestActivation(); - State state = new WaitingForRegistrationSync(activation.machine, null); - - State to = state.transition(new RegistrationSynced()); - - // RSH3e2a - assertSize(0, activation.machine.pendingEvents); - assertInstanceOf(WaitingForNewPushDeviceDetails.class, to); - } - - // RSH3e3 - public void test_WaitingForRegistrationUpdate_on_UpdatingRegistrationFailed() { - TestActivation activation = new TestActivation(); - State state = new WaitingForRegistrationSync(activation.machine, null); - ErrorInfo reason = new ErrorInfo("test", 123); - - final AsyncWaiter waiter = broadcastWaiter("PUSH_UPDATE_FAILED"); - - State to = state.transition(new SyncRegistrationFailed(reason)); - - // RSH3e3a - waiter.waitFor(); - assertNull(waiter.result); - assertEquals(reason, waiter.error); - - assertSize(0, activation.machine.pendingEvents); - - // RSH3e3b - assertInstanceOf(AfterRegistrationSyncFailed.class, to); - } - - // RSH3f1 - public void test_AfterRegistrationUpdateFailed_on_GotPushDeviceDetails() throws Exception { - new UpdateRegistrationTest() { - @Override - protected void setUpMachineState(TestCase testCase) throws AblyException { - testCase.testActivation.registerAndWait(); - testCase.testActivation.rest.push.activate(testCase.useCustomRegistrar); - testCase.testActivation.moveToAfterRegistrationUpdateFailed(); - } - }.run(); - } - - // RSH3f1 - public void test_AfterRegistrationUpdateFailed_on_CalledActivate() throws Exception { - new UpdateRegistrationTest("PUSH_ACTIVATE") { - @Override - protected void setUpMachineState(TestCase testCase) throws AblyException { - testCase.testActivation.registerAndWait(); - testCase.testActivation.moveToAfterRegistrationUpdateFailed(); - } - - @Override - protected String sendInitialEvent(UpdateRegistrationTest.TestCase testCase) throws AblyException { - testCase.testActivation.rest.push.activate(testCase.useCustomRegistrar); - return "testTokenFailed"; - } - }.run(); - } - - // RSH3f1 - public void test_AfterRegistrationUpdateFailed_on_CalledDeactivate() throws Exception { - new DeactivateTest(AfterRegistrationSyncFailed.class) { - @Override - protected void setUpMachineState(TestCase testCase) throws AblyException { - testCase.testActivation.registerAndWait(); - testCase.testActivation.moveToAfterRegistrationUpdateFailed(); - } - }.run(); - } - - // RSH3g1 - public void test_WaitingForDeregistration_on_CalledDeactivate() throws Exception { - TestActivation activation = new TestActivation(); - State state = new WaitingForDeregistration(activation.machine, null); - - State to = state.transition(new CalledDeactivate()); - - assertSize(0, activation.machine.pendingEvents); - assertInstanceOf(WaitingForDeregistration.class, to); - } - - // RSH3g2 - public void test_WaitingForDeregistration_on_Deregistered() throws Exception { - TestActivation activation = new TestActivation(); - State state = new WaitingForDeregistration(activation.machine, null); - - activation.rest.push.getLocalDevice().setDeviceIdentityToken("test"); - final Helpers.AsyncWaiter waiter = broadcastWaiter("PUSH_DEACTIVATE"); - - State to = state.transition(new Deregistered()); - - // RSH3g2b - waiter.waitFor(); - assertNull(waiter.error); - - // RSH3g2a - assertNull(activation.rest.push.getLocalDevice().deviceIdentityToken); - - // RSH3g2c - assertSize(0, activation.machine.pendingEvents); - assertInstanceOf(NotActivated.class, to); - } - - // RSH3g3 - public void test_WaitingForDeregistration_on_DeregistrationFailed() throws Exception { - class TestCase extends TestCases.Base { - private TestActivation testActivation; - private State previousState; - - public TestCase(String name, TestActivation testActivation, State previousState) { - super(name, null); - this.testActivation = testActivation; - this.previousState = previousState; - } - - @Override - public void run() throws Exception { - State state = new WaitingForDeregistration(testActivation.machine, previousState); - - Helpers.AsyncWaiter waiter = broadcastWaiter("PUSH_DEACTIVATE"); - ErrorInfo reason = new ErrorInfo("test", 123); - - State to = state.transition(new DeregistrationFailed(reason)); - - // RSH3g3a - waiter.waitFor(); - assertEquals(reason, waiter.error); - - // RSH3g3b - assertSize(0, testActivation.machine.pendingEvents); - assertInstanceOf(previousState.getClass(), to); - } - } - - TestCases testCases = new TestCases(); - - TestActivation activation0 = new TestActivation(); - testCases.add(new TestCase( - "from WaitingForNewPushDeviceDetails", - activation0, - new WaitingForNewPushDeviceDetails(activation0.machine))); - - TestActivation activation1 = new TestActivation(); - testCases.add(new TestCase( - "from AfterRegistrationSyncFailed", - activation1, - new AfterRegistrationSyncFailed(activation1.machine))); - - testCases.run(); - } - - // RSH4a1 - public void test_PushChannel_subscribeDevice_not_registered() throws AblyException { - TestActivation activation = new TestActivation(); - Channel channel = activation.rest.channels.get("pushenabled:foo"); - - try { - channel.push.subscribeDevice(); - fail("expected failure due to device not being registered"); - } catch (AblyException e) { - } finally { - String deviceId = activation.rest.push.getLocalDevice().id; - if(deviceId != null) { - PushBase.ChannelSubscription sub = PushBase.ChannelSubscription.forDevice(channel.name, deviceId); - activation.rest.push.admin.channelSubscriptions.remove(sub); - } - } - } - - // RSH4a2 - public void test_PushChannel_subscribeDevice_ok() throws AblyException { - TestActivation activation = new TestActivation(); - Channel channel = activation.rest.channels.get("pushenabled:foo"); - PushBase.ChannelSubscription sub = null; - - try { - activation.registerAndWait(); - sub = PushBase.ChannelSubscription.forDevice(channel.name, activation.rest.push.getLocalDevice().id); - channel.push.subscribeDevice(); - - PushBase.ChannelSubscription[] items = activation.rest.push.admin.channelSubscriptions.list(new Param[]{ - new Param("channel", channel.name), - new Param("deviceId", sub.deviceId), - new Param("fullWait", "true"), - }).items(); - assertSize(1, items); - assertEquals(items[0], sub); - } finally { - if(sub != null) { - activation.rest.push.admin.channelSubscriptions.remove(sub); - } - } - } - - // RSH4b1 - public void test_PushChannel_subscribeClient_not_registered() throws AblyException { - TestActivation activation = new TestActivation(); - Channel channel = activation.rest.channels.get("pushenabled:foo"); - - try { - channel.push.subscribeClient(); - fail("expected failure due to device not having a client ID"); - } catch (AblyException e) { - } - } - - // RSH4b2 - public void test_PushChannel_subscribeClient_ok() throws AblyException { - TestActivation activation = new TestActivation(); - final String testClientId = "testClient"; - activation.rest.auth.setClientId(testClientId); - activation.rest.auth.authorize(new Auth.TokenParams() {{ clientId = testClientId; }}, null); - - PushBase.ChannelSubscription sub = null; - try { - activation.registerAndWait(); - - Channel channel = activation.rest.channels.get("pushenabled:foo"); - sub = PushBase.ChannelSubscription.forClientId(channel.name, activation.rest.push.getLocalDevice().clientId); - channel.push.subscribeClient(); - - PushBase.ChannelSubscription[] items = activation.rest.push.admin.channelSubscriptions.list(new Param[]{ - new Param("channel", channel.name), - new Param("clientId", sub.clientId), - new Param("fullWait", "true"), - }).items(); - assertSize(1, items); - assertEquals(items[0], sub); - } finally { - if(sub != null) { - activation.rest.push.admin.channelSubscriptions.remove(sub); - } - } - } - - // RSH4c1 - public void test_PushChannel_unsubscribeDevice_not_registered() throws AblyException { - TestActivation activation = new TestActivation(); - Channel channel = activation.rest.channels.get("pushenabled:foo"); - PushBase.ChannelSubscription sub = PushBase.ChannelSubscription.forDevice(channel.name, activation.rest.push.getLocalDevice().id); - - try { - channel.push.unsubscribeDevice(); - fail("expected failure due to device not being registered"); - } catch (AblyException e) { - } - } - - // RSH4c2 - public void test_PushChannel_unsubscribeDevice_ok() throws AblyException { - TestActivation activation = new TestActivation(); - Channel channel = activation.rest.channels.get("pushenabled:foo"); - PushBase.ChannelSubscription sub = null; - - try { - activation.registerAndWait(); - sub = PushBase.ChannelSubscription.forDevice(channel.name, activation.rest.push.getLocalDevice().id); - - activation.rest.push.admin.channelSubscriptions.save(sub); - - channel.push.unsubscribeDevice(); - - PushBase.ChannelSubscription[] items = activation.rest.push.admin.channelSubscriptions.list(new Param[]{ - new Param("channel", channel.name), - new Param("deviceId", sub.deviceId), - new Param("fullWait", "true"), - }).items(); - assertSize(0, items); - } finally { - if(sub != null) { - activation.rest.push.admin.channelSubscriptions.remove(sub); - } - } - } - - // RSH4d1 - public void test_PushChannel_unsubscribeClient_not_registered() throws AblyException { - TestActivation activation = new TestActivation(); - Channel channel = activation.rest.channels.get("pushenabled:foo"); - PushBase.ChannelSubscription sub = PushBase.ChannelSubscription.forClientId(channel.name, activation.rest.push.getLocalDevice().clientId); - - try { - channel.push.unsubscribeClient(); - fail("expected failure due to device not having a client ID"); - } catch (AblyException e) { - } - } - - // RSH4d2 - public void test_PushChannel_unsubscribeClient_ok() throws AblyException { - TestActivation activation = new TestActivation(); - final String testClientId = "testClient"; - activation.rest.auth.setClientId(testClientId); - activation.rest.auth.authorize(new Auth.TokenParams() {{ clientId = testClientId; }}, null); - - Channel channel = activation.rest.channels.get("pushenabled:foo"); - PushBase.ChannelSubscription sub = null; - - try { - activation.registerAndWait(); - sub = PushBase.ChannelSubscription.forClientId(channel.name, activation.rest.push.getLocalDevice().clientId); - - activation.rest.push.admin.channelSubscriptions.save(sub); - - channel.push.unsubscribeClient(); - - PushBase.ChannelSubscription[] items = activation.rest.push.admin.channelSubscriptions.list(new Param[]{ - new Param("channel", channel.name), - new Param("clientId", sub.clientId), - new Param("fullWait", "true"), - }).items(); - assertSize(0, items); - } finally { - if(sub != null) { - activation.rest.push.admin.channelSubscriptions.remove(sub); - } - } - } - - // RSH4e - public void test_PushChannel_listSubscriptions() throws Exception { - class TestCase extends TestCases.Base { - private boolean useClientId; - private TestActivation testActivation; - - public TestCase(String name, boolean useClientId) { - super(name, null); - this.useClientId = useClientId; - } - - @Override - public void run() throws Exception { - testActivation = new TestActivation(); - if (useClientId) { - final String testClientId = "testClient"; - testActivation.rest.auth.setClientId(testClientId); - testActivation.rest.auth.authorize(new Auth.TokenParams() {{ clientId = testClientId; }}, null); - } else { - testActivation.rest.auth.authorize(null, null); - } - - testActivation.registerAndWait(); - DeviceDetails otherDevice = DeviceDetails.fromJsonObject(JsonUtils.object() - .add("id", "other") - .add("platform", "android") - .add("formFactor", "tablet") - .add("metadata", JsonUtils.object()) - .add("push", JsonUtils.object() - .add("recipient", JsonUtils.object() - .add("transportType", "fcm") - .add("registrationToken", "qux"))) - .toJson()); - - String deviceId = testActivation.rest.push.getLocalDevice().id; - - Push.ChannelSubscription[] fixtures = new Push.ChannelSubscription[] { - PushBase.ChannelSubscription.forDevice("pushenabled:foo", deviceId), - PushBase.ChannelSubscription.forDevice("pushenabled:foo", "other"), - PushBase.ChannelSubscription.forDevice("pushenabled:bar", deviceId), - PushBase.ChannelSubscription.forClientId("pushenabled:foo", "testClient"), - PushBase.ChannelSubscription.forClientId("pushenabled:foo", "otherClient"), - PushBase.ChannelSubscription.forClientId("pushenabled:bar", "testClient"), - }; - - try { - testActivation.adminRest.push.admin.deviceRegistrations.save(otherDevice); - - for (PushBase.ChannelSubscription sub : fixtures) { - testActivation.adminRest.push.admin.channelSubscriptions.save(sub); - } - - Push.ChannelSubscription[] got = testActivation.rest.channels.get("pushenabled:foo").push.listSubscriptions().items(); - - ArrayList expected = new ArrayList<>(2); - expected.add(PushBase.ChannelSubscription.forDevice("pushenabled:foo", deviceId)); - if (useClientId) { - expected.add(PushBase.ChannelSubscription.forClientId("pushenabled:foo", "testClient")); - } - - assertArrayUnorderedEquals(expected.toArray(), got); - } finally { - testActivation.adminRest.push.admin.deviceRegistrations.remove(otherDevice); - for (PushBase.ChannelSubscription sub : fixtures) { - testActivation.adminRest.push.admin.channelSubscriptions.remove(sub); - } - } - } - } - - TestCases testCases = new TestCases(); - - testCases.add(new TestCase("without client ID", false)); - testCases.add(new TestCase("with client ID", true)); - - testCases.run(); - } - - public void test_Realtime_push_interface() throws Exception { - AblyRealtime realtime = new AblyRealtime(new ClientOptions() {{ - autoConnect = false; - key = "madeup"; - }}); - realtime.setAndroidContext(getContext()); - assertInstanceOf(LocalDevice.class, realtime.push.getLocalDevice()); - assertInstanceOf(Push.class, realtime.push); - assertInstanceOf(PushChannel.class, realtime.channels.get("test").push); - } - - public void test_push_AfterRegistrationUpdateFailed_migrate_to_AfterRegistrationSyncFailed() { - new TestActivation(); // Just for the side effect of clearing persisted state. - - SharedPreferences.Editor editor = PreferenceManager.getDefaultSharedPreferences(getContext().getApplicationContext()).edit(); - editor.putString(ActivationStateMachine.PersistKeys.CURRENT_STATE, "io.ably.lib.push.ActivationStateMachine$AfterRegistrationUpdateFailed"); - assertTrue(editor.commit()); - - TestActivation activation = new TestActivation(new Helpers.AblyFunction() { - @Override - public Void apply(TestActivation.Options options) throws AblyException { - options.clearPersisted = false; - return null; - } - }); - assertInstanceOf(AfterRegistrationSyncFailed.class, activation.machine.current); - } - - // https://github.com/ably/ably-java/issues/598 - public void test_restore_non_nullary_event() { - TestActivation activation = new TestActivation(); - assertInstanceOf(NotActivated.class, activation.machine.current); - - SyncRegistrationFailed event = new SyncRegistrationFailed(new ErrorInfo()); - - activation.machine.handleEvent(event); - - // NotActivated can't handle SyncRegistrationFailed, so it should be pending. - assertTrue(activation.machine.pendingEvents.contains(event)); - - // Now recover the persisted state and events. - - activation = new TestActivation(new Helpers.AblyFunction() { - @Override - public Void apply(TestActivation.Options options) throws AblyException { - options.clearPersisted = false; - return null; - } - }); - - // Since the event doesn't have a nullary constructor, it should be dropped. - assertInstanceOf(NotActivated.class, activation.machine.current); - assertSize(0, activation.machine.pendingEvents); - } - - // This is all copied and pasted from ParameterizedTest, since I can't inherit from it. - // I need to inherit from AndroidPushTest, and Java doesn't have multiple inheritance - // or mixins or something like that. - - protected static Setup.TestVars testVars; - - public static void setUpBeforeClass() throws Exception { - testVars = Setup.getTestVars(); - } - - public static void tearDownAfterClass() throws Exception { - Setup.clearTestVars(); - } - - private Setup.TestParameters testParams = Setup.TestParameters.getDefault(); - - protected DebugOptions createOptions() throws AblyException { - return testVars.createOptions(testParams); - } - - protected DebugOptions createOptions(String key) throws AblyException { - return testVars.createOptions(key, testParams); - } - - protected void fillInOptions(ClientOptions opts) { - testVars.fillInOptions(opts, testParams); - } - - private class TestActivationContext extends ActivationContext { - public Helpers.AblyFunction, Void> onGetRegistrationToken; - - TestActivationContext(Context context) { - super(context); - this.onGetRegistrationToken = new Helpers.AblyFunction, Void>() { - @Override - public Void apply(Callback callback) throws AblyException { - callback.onSuccess(ULID.random()); - return null; - } - }; - } - - @Override - public synchronized ActivationStateMachine getActivationStateMachine() { - if(activationStateMachine == null) { - activationStateMachine = new TestActivationStateMachine(this); - } - return activationStateMachine; - } - - @Override - protected void getRegistrationToken(final Callback callback) { - try { - this.onGetRegistrationToken.apply(callback); - } catch (AblyException e) { - callback.onError(ErrorInfo.fromThrowable(e)); - } - } - } - - private class TestActivationStateMachine extends ActivationStateMachine { - class EventOrStateWaiter extends CompletionWaiter { - Class event; - Class state; - - public boolean shouldFire(State state, Event event) { - if (this.state != null) { - if (this.state.isInstance(state)) { - return true; - } - } else if (this.event != null) { - if (this.event.isInstance(event)) { - return true; - } - } else { - return true; - } - return false; - } - } - - private BlockingQueue events = null; - private EventOrStateWaiter waiter; - private Class waitingForState; - - public TestActivationStateMachine(ActivationContext activationContext) { - super(activationContext); - } - - @Override - public synchronized boolean handleEvent(Event event) { - if (events != null) { - try { - events.put(event); - } catch (InterruptedException e) {} - } - - boolean ok = super.handleEvent(event); - - if (waiter != null && waiter.shouldFire(current, event)) { - CompletionWaiter w = waiter; - waiter = null; - w.onSuccess(); - } - return ok; - } - - @Override - public boolean reset() { - waiter = null; - events = null; - return super.reset(); - } - - public BlockingQueue getEventReceiver(int capacity) { - events = new ArrayBlockingQueue(capacity); - return events; - } - - public CompletionWaiter getEventHandledWaiter() { - return getEventHandledWaiter(null); - } - - public CompletionWaiter getEventHandledWaiter(final Class e) { - waiter = new EventOrStateWaiter() {{ - event = e; - }}; - return waiter; - } - - public CompletionWaiter getTransitionedToWaiter(final Class s) { - waiter = new EventOrStateWaiter() {{ - state = s; - }}; - return waiter; - } - } - - private AsyncWaiter broadcastWaiter(String event) { - final AsyncWaiter waiter = new AsyncWaiter(); - BroadcastReceiver onceReceiver = new BroadcastReceiver() { - @Override - public void onReceive(Context context, Intent intent) { - LocalBroadcastManager.getInstance(context.getApplicationContext()).unregisterReceiver(this); - ErrorInfo error = IntentUtils.getErrorInfo(intent); - if (error == null) { - waiter.onSuccess(intent); - } else { - waiter.onError(error); - } - } - }; - IntentFilter filter = new IntentFilter("io.ably.broadcast." + event); - LocalBroadcastManager.getInstance(getContext().getApplicationContext()).registerReceiver(onceReceiver, filter); - return waiter; - } - - private void sendBroadcast(String name, Intent intent) { - intent.setAction("io.ably.broadcast." + name); - LocalBroadcastManager.getInstance(getContext().getApplicationContext()).sendBroadcast(intent); - } - - private abstract class DeactivateTest { - private Class previousState; - - DeactivateTest(Class previousState) { - this.previousState = previousState; - } - - protected abstract void setUpMachineState(TestCase testCase) throws AblyException; - - class TestCase extends TestCases.Base { - private final ErrorInfo deregisterError; - private final boolean useCustomDeregisterer; - private final Class expectedEvent; - private final Class expectedState; - protected TestActivation testActivation; - - public TestCase(String name, boolean useCustomDeregisterer, ErrorInfo error, Class expectedEvent, Class expectedState) { - super(name, null); - this.useCustomDeregisterer = useCustomDeregisterer; - this.deregisterError = error; - this.expectedEvent = expectedEvent; - this.expectedState = expectedState; - } - - @Override - public void run() throws Exception { - try { - testActivation = new TestActivation(); - - setUpMachineState(this); - - final AsyncWaiter deregisterCallback = useCustomDeregisterer ? broadcastWaiter("PUSH_DEREGISTER_DEVICE") : null; - final AsyncWaiter deactivateCallback = broadcastWaiter("PUSH_DEACTIVATE"); - AsyncWaiter requestWaiter = null; - - if (!useCustomDeregisterer) { - if (deregisterError != null) { - testActivation.httpTracker.mockResponse = Helpers.httpResponseFromErrorInfo(deregisterError); - } - - requestWaiter = testActivation.httpTracker.getRequestWaiter(); - // Block until we've checked the intermediate WaitingForDeregistration state, - // before the request's response causes another state transition. - // Otherwise, our test would be racing against the request. - testActivation.httpTracker.lockRequests(); - } - - CompletionWaiter deactivatingWaiter = testActivation.machine.getTransitionedToWaiter(WaitingForDeregistration.class); - // Will send a CalledDeactivate event. - testActivation.rest.push.deactivate(useCustomDeregisterer); - deactivatingWaiter.waitFor(); - - if (useCustomDeregisterer) { - // RSH3d2a - deregisterCallback.waitFor(); - assertNull(deregisterCallback.error); - } else { - // RSH3d2b - requestWaiter.waitFor(); - Helpers.RawHttpRequest request = requestWaiter.result; - assertEquals("DELETE", request.method); - assertEquals("/push/deviceRegistrations/" + testActivation.rest.push.getLocalDevice().id, request.url.getPath()); - } - - // RSH3d2d - assertInstanceOf(WaitingForDeregistration.class, testActivation.machine.current); - - // Now wait for next event, after deregistration. - CompletionWaiter handled = testActivation.machine.getEventHandledWaiter(); - BlockingQueue events = testActivation.machine.getEventReceiver(1); - - if (useCustomDeregisterer) { - Intent intent = new Intent(); - if (deregisterError != null) { - IntentUtils.addErrorInfo(intent, deregisterError); - } - sendBroadcast("PUSH_DEVICE_DEREGISTERED", intent); - } else { - testActivation.httpTracker.unlockRequests(); - } - - assertInstanceOf(expectedEvent, events.poll(10, TimeUnit.SECONDS)); - assertNull(handled.waitFor()); - - if (deregisterError == null) { - // RSH3g2a - assertNull(testActivation.rest.push.getLocalDevice().deviceIdentityToken); - } else { - // RSH3g3a - assertNotNull(testActivation.rest.push.getLocalDevice().deviceIdentityToken); - } - - // RSH3g2b, RSH3g3a - deactivateCallback.waitFor(); - assertEquals(deregisterError, deactivateCallback.error); - assertInstanceOf(expectedState, testActivation.machine.current); - } finally { - testActivation.httpTracker.unlockRequests(); - testActivation.rest.push.admin.deviceRegistrations.remove(testActivation.rest.push.getLocalDevice()); - } - } - } - - public void run() throws Exception { - TestCases testCases = new TestCases(); - - // RSH3g2 - testCases.add(new TestCase( - "ok with custom deregisterer", - true, - null, - Deregistered.class, - NotActivated.class /* RSH3g2c */)); - - testCases.add(new TestCase( - "ok with default deregisterer", - false, - null, - Deregistered.class, - NotActivated.class /* RSH3g2c */)); - - // RSH3g3 - testCases.add(new TestCase( - "failing with custom deregisterer", - true, - new ErrorInfo("testError", 123), - DeregistrationFailed.class, - previousState /* RSH3g3b */)); - - testCases.add(new TestCase( - "failing with default deregisterer", - false, - new ErrorInfo("testError", 123), - DeregistrationFailed.class, - previousState /* RSH3g3b */)); - - testCases.run(); - } - } - - private abstract class UpdateRegistrationTest { - private final String onFailedEvent; - - protected abstract void setUpMachineState(TestCase testCase) throws AblyException; - - UpdateRegistrationTest() { - this("PUSH_UPDATE_FAILED"); - } - - UpdateRegistrationTest(String onFailedEvent) { - this.onFailedEvent = onFailedEvent; - } - - class TestCase extends TestCases.Base { - private final ErrorInfo updateError; - private final boolean useCustomRegistrar; - private final Class expectedEvent; - private final Class expectedState; - protected TestActivation testActivation; - - public TestCase(String name, boolean useCustomRegistrar, ErrorInfo error, Class expectedEvent, Class expectedState) { - super(name, null); - this.useCustomRegistrar = useCustomRegistrar; - this.updateError = error; - this.expectedEvent = expectedEvent; - this.expectedState = expectedState; - } - - @Override - public void run() throws Exception { - try { - testActivation = new TestActivation(); - - setUpMachineState(this); - final boolean isExpectingRegistrationValidation = testActivation.machine.current instanceof AfterRegistrationSyncFailed; - - final AsyncWaiter registerCallback = useCustomRegistrar ? broadcastWaiter("PUSH_REGISTER_DEVICE") : null; - final AsyncWaiter updateFailedCallback = updateError != null ? broadcastWaiter(onFailedEvent) : null; - AsyncWaiter requestWaiter = null; - - if (!useCustomRegistrar) { - if (updateError != null) { - testActivation.httpTracker.mockResponse = Helpers.httpResponseFromErrorInfo(updateError); - } - - requestWaiter = testActivation.httpTracker.getRequestWaiter(); - // Block until we've checked the intermediate WaitingForDeregistration state, - // before the request's response causes another state transition. - // Otherwise, our test would be racing against the request. - testActivation.httpTracker.lockRequests(); - } - - CompletionWaiter updatingWaiter = testActivation.machine.getTransitionedToWaiter(WaitingForRegistrationSync.class); - String updatedRegistrationToken = sendInitialEvent(this); - updatingWaiter.waitFor(); - - if (useCustomRegistrar) { - // RSH3d3a - registerCallback.waitFor(); - assertNull(registerCallback.error); - } else { - requestWaiter.waitFor(); - Helpers.RawHttpRequest request = requestWaiter.result; - assertEquals("/push/deviceRegistrations/"+testActivation.rest.push.getLocalDevice().id, request.url.getPath()); - String authToken = Base64Coder.decodeString(request.requestHeaders.get("X-Ably-DeviceToken").get(0)); - assertEquals(testActivation.rest.push.getLocalDevice().deviceIdentityToken, authToken); - - JsonObject requestBody = (JsonObject)Serialisation.msgpackToGson(request.requestBody.getEncoded()); - JsonObject requestRecipient = requestBody.getAsJsonObject("push").getAsJsonObject("recipient"); - assertEquals("fcm", requestRecipient.getAsJsonPrimitive("transportType").getAsString()); - assertEquals(updatedRegistrationToken, requestRecipient.getAsJsonPrimitive("registrationToken").getAsString()); - - if(isExpectingRegistrationValidation) { - // RSH3f1a: PUT the entire DeviceDetails - assertEquals("PUT", request.method); - assertTrue(requestBody.has("deviceSecret")); - assertTrue(requestBody.has("clientId")); - } else { - // RSH3d3b: PATCH the updated members - assertEquals("PATCH", request.method); - assertFalse(requestBody.has("deviceSecret")); - assertFalse(requestBody.has("clientId")); - } - } - - // RSH3d3d - assertInstanceOf(WaitingForRegistrationSync.class, testActivation.machine.current); - - // Now wait for next event, after updated. - CompletionWaiter handled = testActivation.machine.getEventHandledWaiter(); - BlockingQueue events = testActivation.machine.getEventReceiver(1); - - if (useCustomRegistrar) { - Intent intent = new Intent(); - if (updateError != null) { - IntentUtils.addErrorInfo(intent, updateError); - } - sendBroadcast("PUSH_DEVICE_REGISTERED", intent); - } else { - testActivation.httpTracker.unlockRequests(); - } - - assertInstanceOf(expectedEvent, events.poll(10, TimeUnit.SECONDS)); - assertNull(handled.waitFor()); - - if (updateError != null) { - // RSH3e3a - updateFailedCallback.waitFor(); - assertEquals(updateError, updateFailedCallback.error); - } - // RSH3e2a, RSH3e3b - assertInstanceOf(expectedState, testActivation.machine.current); - } finally { - testActivation.httpTracker.unlockRequests(); - testActivation.rest.push.admin.deviceRegistrations.remove(testActivation.rest.push.getLocalDevice()); - } - } - } - - public void run() throws Exception { - TestCases testCases = new TestCases(); - - // RSH3e2 - testCases.add(new TestCase( - "ok with custom registerer", - true, - null, - RegistrationSynced.class, - WaitingForNewPushDeviceDetails.class)); - - testCases.add(new TestCase( - "ok with default registerer", - false, - null, - RegistrationSynced.class, - WaitingForNewPushDeviceDetails.class)); - - // RSH3e3 - testCases.add(new TestCase( - "failing with custom registerer", - true, - new ErrorInfo("testError", 123), - SyncRegistrationFailed.class, - AfterRegistrationSyncFailed.class)); - - testCases.add(new TestCase( - "failing with default registerer", - false, - new ErrorInfo("testError", 123), - SyncRegistrationFailed.class, - AfterRegistrationSyncFailed.class)); - - testCases.run(); - } - - protected String sendInitialEvent(TestCase testCase) throws AblyException { - // Will send GotPushDeviceDetails event. - CalledActivate.useCustomRegistrar(testCase.useCustomRegistrar, PreferenceManager.getDefaultSharedPreferences(getContext())); - testCase.testActivation.rest.push.getActivationContext().onNewRegistrationToken(RegistrationToken.Type.FCM, "testTokenUpdated"); - return "testTokenUpdated"; - } - } + private class TestActivation { + private Helpers.RawHttpTracker httpTracker; + private AblyRest rest; + private TestActivationContext activationContext; + private TestActivationStateMachine machine; + private AblyRest adminRest; + + TestActivation() { + this(null); + } + + public class Options { + public DebugOptions clientOptions; + public boolean clearPersisted = true; + public TestActivationContext activationContext; + } + + TestActivation(Helpers.AblyFunction configure) { + try { + httpTracker = new Helpers.RawHttpTracker(); + DebugOptions options = createOptions(testVars.keys[0].keyStr); + options.httpListener = httpTracker; + options.useTokenAuth = true; + Context context = getContext(); + + Options activationOptions = new Options(); + activationOptions.clientOptions = options; + activationOptions.activationContext = new TestActivationContext(context.getApplicationContext()); + if (configure != null) { + configure.apply(activationOptions); + } + activationContext = activationOptions.activationContext; + options = activationOptions.clientOptions; + + ActivationContext.setActivationContext(context.getApplicationContext(), activationContext); + if (activationOptions.clearPersisted) { + activationContext.reset(); + } + machine = new TestActivationStateMachine(activationContext); + activationContext.setActivationStateMachine(machine); + + rest = new AblyRest(options); + rest.auth.authorize(null, null); + activationContext.setAbly(rest); + rest.setAndroidContext(context); + + adminRest = new AblyRest(options); + adminRest.auth.authorize(new Auth.TokenParams() {{ + clientId = Auth.WILDCARD_CLIENTID; + }}, null); + } catch(AblyException e) {} + } + + private void registerAndWait() throws AblyException { + AsyncWaiter requestWaiter = httpTracker.getRequestWaiter(); + AsyncWaiter activateWaiter = broadcastWaiter("PUSH_ACTIVATE"); + + rest.push.getActivationContext().onNewRegistrationToken(RegistrationToken.Type.FCM, "testToken"); + rest.push.activate(false); + + activateWaiter.waitFor(); + assertNull(activateWaiter.error); + requestWaiter.waitFor(); + Helpers.RawHttpRequest request = requestWaiter.result; + Log.d("AndroidPushTest"," registration method: " + request.method); + assertTrue(request.method.equals("PATCH") || request.method.equals("POST")); + assertTrue(request.url.getPath().startsWith("/push/deviceRegistrations")); + } + + private void moveToAfterRegistrationUpdateFailed() throws AblyException { + // Move to AfterRegistrationSyncFailed by forcing an update failure. + + rest.push.activate(true); // Just to set useCustomRegistrar to true. + AsyncWaiter customRegisterer = broadcastWaiter("PUSH_REGISTER_DEVICE"); + rest.push.getActivationContext().onNewRegistrationToken(RegistrationToken.Type.FCM, "testTokenFailed"); + customRegisterer.waitFor(); + + CompletionWaiter failedWaiter = machine.getTransitionedToWaiter(AfterRegistrationSyncFailed.class); + + Intent intent = new Intent(); + IntentUtils.addErrorInfo(intent, new ErrorInfo("intentional", 123)); + sendBroadcast("PUSH_DEVICE_REGISTERED", intent); + + failedWaiter.waitFor(); + } + } + + public static Test suite() { + TestSuite suite = new TestSuite(); + suite.addTest(new TestSetup(new TestSuite(AndroidPushTest.class)) { + protected void setUp() throws Exception { + setUpBeforeClass(); + } + protected void tearDown() throws Exception { + tearDownAfterClass(); + } + }); + return suite; + } + + // RSH2a + public void test_push_activate() throws InterruptedException, AblyException { + TestActivation activation = new TestActivation(); + BlockingQueue events = activation.machine.getEventReceiver(2); // CalledActivate + GotPushDeviceDetails + assertInstanceOf(ActivationStateMachine.NotActivated.class, activation.machine.current); + activation.rest.push.activate(); + Event event = events.poll(10, TimeUnit.SECONDS); + assertInstanceOf(CalledActivate.class, event); + } + + // RSH2b + public void test_push_deactivate() throws InterruptedException, AblyException { + TestActivation activation = new TestActivation(); + BlockingQueue events = activation.machine.getEventReceiver(1); + assertInstanceOf(NotActivated.class, activation.machine.current); + activation.rest.push.deactivate(); + Event event = events.poll(10, TimeUnit.SECONDS); + assertInstanceOf(CalledDeactivate.class, event); + } + + // RSH2c / RSH8g + public void test_push_onNewRegistrationToken() throws InterruptedException, AblyException { + TestActivation activation = new TestActivation(); + BlockingQueue events = activation.machine.getEventReceiver(1); + final BlockingQueue> tokenCallbacks = new ArrayBlockingQueue<>(1) ; + + activation.activationContext.onGetRegistrationToken = new Helpers.AblyFunction, Void>() { + @Override + public Void apply(Callback callback) throws AblyException { + try { + tokenCallbacks.put(callback); + } catch (InterruptedException e) { + throw AblyException.fromThrowable(e); + } + return null; + } + }; + + activation.rest.push.activate(true); // This registers the listener for registration tokens. + assertInstanceOf(CalledActivate.class, events.poll(10, TimeUnit.SECONDS)); + + Callback tokenCallback = tokenCallbacks.poll(10, TimeUnit.SECONDS); + + tokenCallback.onSuccess("foo"); + assertInstanceOf(GotPushDeviceDetails.class, events.poll(10, TimeUnit.SECONDS)); + + tokenCallback.onSuccess("bar"); + assertInstanceOf(GotPushDeviceDetails.class, events.poll(10, TimeUnit.SECONDS)); + } + + // RSH2d / RSH8h + public void test_push_onNewRegistrationTokenFailed() throws InterruptedException, AblyException { + TestActivation activation = new TestActivation(); + BlockingQueue events = activation.machine.getEventReceiver(1); + final BlockingQueue> tokenCallbacks = new ArrayBlockingQueue<>(1) ; + + activation.activationContext.onGetRegistrationToken = new Helpers.AblyFunction, Void>() { + @Override + public Void apply(Callback callback) throws AblyException { + try { + tokenCallbacks.put(callback); + } catch (InterruptedException e) { + throw AblyException.fromThrowable(e); + } + return null; + } + }; + + activation.rest.push.activate(true); // This registers the listener for registration tokens. + assertInstanceOf(CalledActivate.class, events.poll(10, TimeUnit.SECONDS)); + + Callback tokenCallback = tokenCallbacks.poll(10, TimeUnit.SECONDS); + + tokenCallback.onError(new ErrorInfo("foo", 123, 123)); + Event event = events.poll(10, TimeUnit.SECONDS); + assertInstanceOf(ActivationStateMachine.GettingPushDeviceDetailsFailed.class, event); + assertEquals(123,((ActivationStateMachine.GettingPushDeviceDetailsFailed) event).reason.code); + } + + // RSH2e / RSH8i + public void test_push_syncOnStartup() throws InterruptedException, AblyException { + final BlockingQueue> tokenCallbacks = new ArrayBlockingQueue<>(1) ; + + Helpers.AblyFunction configureActivation = new Helpers.AblyFunction() { + @Override + public Void apply(TestActivation.Options options) throws AblyException { + options.activationContext.onGetRegistrationToken = new Helpers.AblyFunction, Void>() { + @Override + public Void apply(Callback callback) throws AblyException { + try { + tokenCallbacks.put(callback); + } catch (InterruptedException e) { + throw AblyException.fromThrowable(e); + } + return null; + } + }; + return null; + } + }; + + TestActivation activation = new TestActivation(configureActivation); + + // Fake-register the device. + AsyncWaiter customRegisterer = broadcastWaiter("PUSH_REGISTER_DEVICE"); + AsyncWaiter activated = broadcastWaiter("PUSH_ACTIVATE"); + activation.rest.push.activate(true); + Callback tokenCallback = tokenCallbacks.take(); + tokenCallback.onSuccess("foo"); + customRegisterer.waitFor(); + Intent intent = new Intent(); + intent.putExtra("deviceIdentityToken", "fakeToken"); + sendBroadcast("PUSH_DEVICE_REGISTERED", intent); + activated.waitFor(); + + // Now just creating a new library instance should request the current token. + + BlockingQueue events = activation.machine.getEventReceiver(1); + + configureActivation = new Helpers.AblyFunction() { + @Override + public Void apply(TestActivation.Options options) throws AblyException { + options.clearPersisted = false; + options.activationContext.onGetRegistrationToken = new Helpers.AblyFunction, Void>() { + @Override + public Void apply(Callback callback) throws AblyException { + try { + tokenCallbacks.put(callback); + } catch (InterruptedException e) { + throw AblyException.fromThrowable(e); + } + return null; + } + }; + return null; + } + }; + + activation = new TestActivation(configureActivation); + tokenCallback = tokenCallbacks.take(); + + // With the same token, nothing happens. + events = activation.machine.getEventReceiver(1); + tokenCallback.onSuccess("foo"); + assertNull(events.poll(100, TimeUnit.MILLISECONDS)); + + // Do the same with a different token, expect a GotPushDeviceDetails. + activation = new TestActivation(configureActivation); + events = activation.machine.getEventReceiver(1); + tokenCallback = tokenCallbacks.take(); + tokenCallback.onSuccess("qux"); + Event event = events.poll(100, TimeUnit.MILLISECONDS); + assertInstanceOf(GotPushDeviceDetails.class, event); + } + + // RSH8a, RSH8c + public void test_push_device_persistence() throws InterruptedException, AblyException { + TestActivation activation = new TestActivation(new Helpers.AblyFunction() { + @Override + public Void apply(TestActivation.Options options) throws AblyException { + options.clientOptions.clientId = "testClient"; + return null; + } + }); + + // Fake-register the device. + AsyncWaiter customRegisterer = broadcastWaiter("PUSH_REGISTER_DEVICE"); + AsyncWaiter activated = broadcastWaiter("PUSH_ACTIVATE"); + activation.rest.push.activate(true); + + customRegisterer.waitFor(); + + LocalDevice device = activation.rest.device(); + assertEquals("testClient", device.clientId); + assertNotNull(device.id); + assertNotNull(device.deviceSecret); + + Intent intent = new Intent(); + intent.putExtra("deviceIdentityToken", "fakeToken"); + sendBroadcast("PUSH_DEVICE_REGISTERED", intent); + activated.waitFor(); + + assertEquals("fakeToken", activation.rest.device().deviceIdentityToken); + + // Load from persisted state. + activation = new TestActivation(new Helpers.AblyFunction() { + @Override + public Void apply(TestActivation.Options options) throws AblyException { + options.clearPersisted = false; + return null; + } + }); + LocalDevice newDevice = activation.rest.device(); + assertEquals("fakeToken", newDevice.deviceIdentityToken); + assertEquals(device.id, newDevice.id); + assertEquals(device.deviceSecret, newDevice.deviceSecret); + assertEquals(device.clientId, newDevice.clientId); + } + + // RSH8d + public void test_push_late_clientId_persisted() throws InterruptedException, AblyException { + TestActivation activation = new TestActivation(); + + assertNull(activation.rest.auth.clientId); + assertNull(activation.rest.device().clientId); + + Auth.TokenParams params = new Auth.TokenParams(); + params.clientId = "testClient"; + activation.rest.auth.authorize(params, null); + + assertEquals("testClient", activation.rest.auth.clientId); + assertEquals("testClient", activation.rest.device().clientId); + + activation = new TestActivation(new Helpers.AblyFunction() { + @Override + public Void apply(TestActivation.Options options) throws AblyException { + options.clearPersisted = false; + return null; + } + }); + assertEquals("testClient", activation.rest.device().clientId); + } + + // RSH8e + public void test_push_late_clientId_emits_GotPushDeviceDetails() throws InterruptedException, AblyException { + TestActivation activation = new TestActivation(); + + // Fake-register the device. + AsyncWaiter customRegisterer = broadcastWaiter("PUSH_REGISTER_DEVICE"); + AsyncWaiter activated = broadcastWaiter("PUSH_ACTIVATE"); + activation.rest.push.activate(true); + customRegisterer.waitFor(); + Intent intent = new Intent(); + intent.putExtra("deviceIdentityToken", "fakeToken"); + sendBroadcast("PUSH_DEVICE_REGISTERED", intent); + activated.waitFor(); + + BlockingQueue events = activation.machine.getEventReceiver(1); + + Auth.TokenParams params = new Auth.TokenParams(); + params.clientId = "testClient"; + activation.rest.auth.authorize(params, null); + + Event event = events.poll(100, TimeUnit.MILLISECONDS); + assertInstanceOf(GotPushDeviceDetails.class, event); + } + + // RSH8f + public void test_push_clientId_from_server() throws InterruptedException, AblyException { + TestActivation activation = new TestActivation(); + + JsonObject body = new JsonObject(); + body.addProperty("clientId", "testClient"); + JsonObject fakeToken = new JsonObject(); + fakeToken.addProperty("token", "fakeToken"); + body.add("deviceIdentityToken", fakeToken); + HttpCore.Response response = new HttpCore.Response(); + response.statusCode = 200; + response.statusLine = "OK"; + response.contentType = "application/json"; + response.body = gson.toJson(body).getBytes(); + response.contentLength = response.body.length; + activation.httpTracker.mockResponse = response; + + try { + AsyncWaiter activated = broadcastWaiter("PUSH_ACTIVATE"); + activation.rest.push.activate(false); + activated.waitFor(); + } finally { + activation.adminRest.push.admin.deviceRegistrations.remove(activation.rest.device().id); + } + + assertEquals("testClient", activation.rest.device().clientId); + } + + // RSH3a1 + public void test_NotActivated_on_CalledDeactivate() { + TestActivation activation = new TestActivation(); + + ActivationStateMachine.State state = new NotActivated(activation.machine); + + final Helpers.AsyncWaiter waiter = broadcastWaiter("PUSH_DEACTIVATE"); + + State to = state.transition(new CalledDeactivate()); + + // RSH3a1a + waiter.waitFor(); + assertNull(waiter.error); + + // RSH3a1b + assertInstanceOf(NotActivated.class, to); + } + + // RSH3a2a + public void test_NotActivated_on_CalledActivate_with_DeviceToken() throws Exception { + class TestCase extends TestCases.Base { + private final String persistedClientId; + private final String instanceClientId; + private final ErrorInfo syncError; + private final boolean useCustomRegistrar; + private final Class expectedEvent; + private final Class expectedState; + private final Integer expectedErrorCode; + + public TestCase( + String name, + String persistedClientId, + String instanceClientId, + boolean useCustomRegistrar, + ErrorInfo syncError, + Class expectedEvent, + Class expectedState, + Integer expectedErrorCode + ) { + super(name, null); + this.persistedClientId = persistedClientId; + this.instanceClientId = instanceClientId; + this.useCustomRegistrar = useCustomRegistrar; + this.syncError = syncError; + this.expectedEvent = expectedEvent; + this.expectedState = expectedState; + this.expectedErrorCode = expectedErrorCode; + } + + @Override + public void run() throws Exception { + // Register local device before doing anything, in order to trigger RSH3a2a. + TestActivation activation = new TestActivation(new Helpers.AblyFunction() { + @Override + public Void apply(TestActivation.Options options) throws AblyException { + options.clientOptions.clientId = persistedClientId; + return null; + } + }); + + try { + Helpers.AsyncWaiter activateCallback = broadcastWaiter("PUSH_ACTIVATE"); + activation.rest.push.activate(false); + activateCallback.waitFor(); + + LocalDevice device = activation.rest.push.getLocalDevice(); + assertNotNull(device.id); + assertNotNull(device.deviceIdentityToken); + assertEquals(persistedClientId, device.clientId); + + + // Now use a new instance, to force persistence consistency checking. + activation = new TestActivation(new Helpers.AblyFunction() { + @Override + public Void apply(TestActivation.Options options) throws AblyException { + options.clientOptions.clientId = instanceClientId; + options.clearPersisted = false; + return null; + } + }); + + Helpers.AsyncWaiter registerCallback = useCustomRegistrar ? broadcastWaiter("PUSH_REGISTER_DEVICE") : null; + activateCallback = broadcastWaiter("PUSH_ACTIVATE"); + + CompletionWaiter calledActivateHandled = activation.machine.getEventHandledWaiter(CalledActivate.class); + Helpers.AsyncWaiter requestWaiter = null; + + if (!useCustomRegistrar) { + if (syncError != null) { + activation.httpTracker.mockResponse = Helpers.httpResponseFromErrorInfo(syncError); + } + + requestWaiter = activation.httpTracker.getRequestWaiter(); + // Block until we've checked the intermediate WaitingForRegistrationSync state, + // before the request's response causes another state transition. + // Otherwise, our test would be racing against the request. + activation.httpTracker.lockRequests(); + } + + activation.rest.push.activate(useCustomRegistrar); + calledActivateHandled.waitFor(); + + // RSH3a2a1: SyncRegistrationFailed may be enqueued (synchronously). In that + // case, register callback or PUT request won't be invoked, and we'll go + // synchronously to AfterRegistrationSyncFailed. + if (activation.machine.current instanceof WaitingForRegistrationSync) { + if (useCustomRegistrar) { + // RSH3a2a2 + registerCallback.waitFor(); + assertNull(registerCallback.error); + } else { + // RSH3a2a3 + requestWaiter.waitFor(); + Helpers.RawHttpRequest request = requestWaiter.result; + assertEquals("PUT", request.method); + assertEquals("/push/deviceRegistrations/" + device.id, request.url.getPath()); + } + + // RSH3a2a4 + assertSize(0, activation.machine.pendingEvents); + assertInstanceOf(WaitingForRegistrationSync.class, activation.machine.current); + + // Now wait for next event, when we may have an error. + + CompletionWaiter handled = activation.machine.getEventHandledWaiter(); + BlockingQueue events = activation.machine.getEventReceiver(1); + + if (useCustomRegistrar) { + Intent intent = new Intent(); + if (syncError != null) { + IntentUtils.addErrorInfo(intent, syncError); + } + sendBroadcast("PUSH_DEVICE_REGISTERED", intent); + } else { + activation.httpTracker.unlockRequests(); + } + + assertInstanceOf(expectedEvent, events.poll(10, TimeUnit.SECONDS)); + assertNull(handled.waitFor()); + } // else: RSH3a2a1 validation failed + + // RSH3e2 or RSH3e3 + activateCallback.waitFor(); + if (expectedErrorCode != null) { + assertNotNull(activateCallback.error); + assertEquals(expectedErrorCode.intValue(), activateCallback.error.code); + } else { + assertNull(activateCallback.error); + } + assertInstanceOf(expectedState, activation.machine.current); + } finally { + activation.httpTracker.unlockRequests(); + /* delete the registration without sending (invalid) local device credentials */ + LocalDevice localDevice = activation.rest.push.getLocalDevice(); + String deviceId = localDevice.id; + localDevice.reset(); + activation.rest.push.admin.deviceRegistrations.remove(deviceId); + } + } + } + + TestCases testCases = new TestCases(); + + // RSH3a2a1, RSH3a2a4, RSH3e3 + testCases.add(new TestCase( + "clientId mismatch", + "testClientId", + "otherClientId", + false, + null, + SyncRegistrationFailed.class, + AfterRegistrationSyncFailed.class, + 61002 + )); + + // RSH3a2a1, RSH3a2a2, RSH3a2a4, RSH3e2 + testCases.add(new TestCase( + "ok with custom registerer", + "testClientId", + "testClientId", + true, + null, + RegistrationSynced.class, + WaitingForNewPushDeviceDetails.class, + null + )); + + // RSH3a2a1, RSH3a2a2, RSH3a2a4, RSH3e3 + testCases.add(new TestCase( + "failing with custom registerer", + "testClientId", + "testClientId", + true, + new ErrorInfo("test error", 123, 123), + SyncRegistrationFailed.class, + AfterRegistrationSyncFailed.class, + 123 + )); + + // RSH3a2a1, RSH3a2a3, RSH3a2a4, RSH3e2 + testCases.add(new TestCase( + "ok without custom registerer", + "testClientId", + "testClientId", + false, + null, + RegistrationSynced.class, + WaitingForNewPushDeviceDetails.class, + null + )); + + // RSH3a2a1, RSH3a2a3, RSH3a2a4, RSH3e3 + testCases.add(new TestCase( + "failing without custom registerer", + "testClientId", + "testClientId", + false, + new ErrorInfo("test error", 123, 123), + SyncRegistrationFailed.class, + AfterRegistrationSyncFailed.class, + 123 + )); + + testCases.run(); + } + + // RSH3a3a + public void test_NotActivated_on_GotPushDeviceDetails() throws InterruptedException { + TestActivation activation = new TestActivation(); + State state = new NotActivated(activation.machine); + + State to = state.transition(new GotPushDeviceDetails()); + + assertSize(0, activation.machine.pendingEvents); + assertInstanceOf(NotActivated.class, to); + } + + // RSH3a2b + public void test_NotActivated_on_CalledActivate_with_registrationToken() throws InterruptedException, AblyException { + TestActivation activation = new TestActivation(); + activation.rest.push.getActivationContext().onNewRegistrationToken(RegistrationToken.Type.FCM, "testToken"); + + State state = new NotActivated(activation.machine); + State to = state.transition(new CalledActivate()); + + assertSize(1, activation.machine.pendingEvents); + assertInstanceOf(GotPushDeviceDetails.class, activation.machine.pendingEvents.getLast()); + + assertInstanceOf(WaitingForPushDeviceDetails.class, to); + + // RSH8b + LocalDevice device = activation.rest.device(); + assertNotNull(device.id); + assertNotNull(device.deviceSecret); + } + + // RSH3a2c + public void test_NotActivated_on_CalledActivate_without_registrationToken() throws InterruptedException { + TestActivation activation = new TestActivation(); + State state = new NotActivated(activation.machine); + State to = state.transition(new CalledActivate()); + + assertSize(0, activation.machine.pendingEvents); + + assertInstanceOf(WaitingForPushDeviceDetails.class, to); + } + + // RSH3b1 + public void test_WaitingForPushDeviceDetails_on_CalledActivate() { + TestActivation activation = new TestActivation(); + State state = new WaitingForPushDeviceDetails(activation.machine); + State to = state.transition(new CalledActivate()); + + assertSize(0, activation.machine.pendingEvents); + + // RSH3b1a + assertInstanceOf(WaitingForPushDeviceDetails.class, to); + } + + // RSH3b2 + public void test_WaitingForPushDeviceDetails_on_CalledDeactivate() { + TestActivation activation = new TestActivation(); + State state = new WaitingForPushDeviceDetails(activation.machine); + + final Helpers.AsyncWaiter waiter = broadcastWaiter("PUSH_DEACTIVATE"); + + State to = state.transition(new CalledDeactivate()); + + // RSH3b2a + waiter.waitFor(); + assertNull(waiter.error); + + assertSize(0, activation.machine.pendingEvents); + + // RSH3b2b + assertInstanceOf(NotActivated.class, to); + } + + // RSH3b3 + public void test_WaitingForPushDeviceDetails_on_GotPushDeviceDetails() throws Exception { + class TestCase extends TestCases.Base { + private final ErrorInfo registerError; + private final boolean useCustomRegistrar; + private final String deviceIdentityToken; + private final Class expectedEvent; + private final Class expectedState; + protected TestActivation activation; + + public TestCase(String name, boolean useCustomRegistrar, ErrorInfo error, String deviceIdentityToken, Class expectedEvent, Class expectedState) { + super(name, null); + this.useCustomRegistrar = useCustomRegistrar; + this.registerError = error; + this.deviceIdentityToken = deviceIdentityToken; + this.expectedEvent = expectedEvent; + this.expectedState = expectedState; + } + + @Override + public void run() throws Exception { + try { + + activation = new TestActivation(); + activation.activationContext.onGetRegistrationToken = new Helpers.AblyFunction, Void>() { + @Override + public Void apply(Callback callback) throws AblyException { + // Ignore request; will send event manually below. + return null; + } + }; + final Helpers.AsyncWaiter registerCallback = useCustomRegistrar ? broadcastWaiter("PUSH_REGISTER_DEVICE") : null; + final Helpers.AsyncWaiter activateCallback = broadcastWaiter("PUSH_ACTIVATE"); + + // Will move to WaitingForPushDeviceDetails. + activation.rest.push.activate(useCustomRegistrar); + + CompletionWaiter handled = activation.machine.getEventHandledWaiter(GotPushDeviceDetails.class); + Helpers.AsyncWaiter requestWaiter = null; + + if (!useCustomRegistrar) { + if (registerError != null) { + activation.httpTracker.mockResponse = Helpers.httpResponseFromErrorInfo(registerError); + } + + requestWaiter = activation.httpTracker.getRequestWaiter(); + // Block until we've checked the intermediate WaitingForDeviceRegistration state, + // before the request's response causes another state transition. + // Otherwise, our test would be racing against the request. + activation.httpTracker.lockRequests(); + } + + // Will send GotPushDeviceDetails event. + activation.rest.push.getActivationContext().onNewRegistrationToken(RegistrationToken.Type.FCM, "testToken"); + + handled.waitFor(); + + if (useCustomRegistrar) { + // RSH3b3a + registerCallback.waitFor(); + assertNull(registerCallback.error); + } else { + // RSH3b3b + requestWaiter.waitFor(); + Helpers.RawHttpRequest request = requestWaiter.result; + assertEquals("POST", request.method); + assertEquals("/push/deviceRegistrations", request.url.getPath()); + } + + // RSH3b3d + assertSize(0, activation.machine.pendingEvents); + assertInstanceOf(WaitingForDeviceRegistration.class, activation.machine.current); + + // Now wait for next event, when we've got an deviceIdentityToken or an error. + handled = activation.machine.getEventHandledWaiter(); + BlockingQueue events = activation.machine.getEventReceiver(1); + + if (useCustomRegistrar) { + Intent intent = new Intent(); + if (registerError != null) { + IntentUtils.addErrorInfo(intent, registerError); + } else { + intent.putExtra("deviceIdentityToken", deviceIdentityToken); + } + sendBroadcast("PUSH_DEVICE_REGISTERED", intent); + } else { + activation.httpTracker.unlockRequests(); + } + + assertInstanceOf(expectedEvent, events.poll(10, TimeUnit.SECONDS)); + assertNull(handled.waitFor()); + + // RSH3c2a + if (useCustomRegistrar) { + assertEquals(deviceIdentityToken, activation.rest.push.getLocalDevice().deviceIdentityToken); + } else if (registerError == null) { + // No error expected, so deviceIdentityToken should've been set by the server. + assertNotNull(activation.rest.push.getLocalDevice().deviceIdentityToken); + + } + + // RSH3c2b, RSH3c3a + activateCallback.waitFor(); + assertEquals(registerError, activateCallback.error); + assertInstanceOf(expectedState, activation.machine.current); + } finally { + activation.httpTracker.unlockRequests(); + /* delete the registration without sending (invalid) local device credentials */ + LocalDevice localDevice = activation.rest.push.getLocalDevice(); + String deviceId = localDevice.id; + localDevice.reset(); + activation.rest.push.admin.deviceRegistrations.remove(deviceId); + } + } + } + + TestCases testCases = new TestCases(); + + // RSH3c2 + testCases.add(new TestCase( + "ok with custom registerer", + true, + null, "testDeviceToken", + GotDeviceRegistration.class, // RSH3b3c + WaitingForNewPushDeviceDetails.class /* RSH3c2c */)); + + testCases.add(new TestCase( + "ok with default registerer", + false, + null, "testDeviceToken", + ActivationStateMachine.GotDeviceRegistration.class, // RSH3b3c + WaitingForNewPushDeviceDetails.class /* RSH3c2c */)); + + // RSH3c3 + testCases.add(new TestCase( + "failing with custom registerer", + true, + new ErrorInfo("testError", 123), null, + GettingDeviceRegistrationFailed.class, // RSH3b3c + NotActivated.class /* RSH3c3b */)); + + testCases.add(new TestCase( + "failing with default registerer", + false, + new ErrorInfo("testError", 123), null, + GettingDeviceRegistrationFailed.class, // RSH3b3c + NotActivated.class /* RSH3c3b */)); + + testCases.run(); + } + + // RSH3c1 + public void test_WaitingForDeviceRegistration_on_CalledActivate() { + TestActivation activation = new TestActivation(); + State state = new WaitingForDeviceRegistration(activation.machine); + State to = state.transition(new CalledActivate()); + + assertSize(0, activation.machine.pendingEvents); + + // RSH3c1a + assertInstanceOf(WaitingForDeviceRegistration.class, to); + } + + // RSH3d1 + public void test_WaitingForNewPushDeviceDetails_on_CalledActivate() { + TestActivation activation = new TestActivation(); + State state = new WaitingForNewPushDeviceDetails(activation.machine); + + final Helpers.AsyncWaiter waiter = broadcastWaiter("PUSH_ACTIVATE"); + + State to = state.transition(new CalledActivate()); + + // RSH3d1a + waiter.waitFor(); + assertNull(waiter.error); + + assertSize(0, activation.machine.pendingEvents); + + // RSH3d1b + assertInstanceOf(WaitingForNewPushDeviceDetails.class, to); + } + + // RSH3d2 + public void test_WaitingForNewPushDeviceDetails_on_CalledDeactivate() throws Exception { + new DeactivateTest(WaitingForNewPushDeviceDetails.class) { + @Override + protected void setUpMachineState(TestCase testCase) throws AblyException { + testCase.testActivation.registerAndWait(); + } + }.run(); + } + + // RSH3d3 + public void test_WaitingForNewPushDeviceDetails_on_GotPushDeviceDetails() throws Exception { + new UpdateRegistrationTest() { + @Override + protected void setUpMachineState(TestCase testCase) throws AblyException { + testCase.testActivation.registerAndWait(); + testCase.testActivation.rest.push.activate(testCase.useCustomRegistrar); + } + }.run(); + } + + // RSH3e1 + public void test_WaitingForRegistrationUpdate_on_CalledActivate() { + TestActivation activation = new TestActivation(); + State state = new WaitingForRegistrationSync(activation.machine, null); + + final AsyncWaiter waiter = broadcastWaiter("PUSH_ACTIVATE"); + + State to = state.transition(new CalledActivate()); + + // RSH3e1a + waiter.waitFor(); + assertNull(waiter.error); + + assertSize(0, activation.machine.pendingEvents); + + // RSH3e1b + assertInstanceOf(WaitingForRegistrationSync.class, to); + } + + // RSH3e2 + public void test_WaitingForRegistrationUpdate_on_RegistrationUpdated() { + TestActivation activation = new TestActivation(); + State state = new WaitingForRegistrationSync(activation.machine, null); + + State to = state.transition(new RegistrationSynced()); + + // RSH3e2a + assertSize(0, activation.machine.pendingEvents); + assertInstanceOf(WaitingForNewPushDeviceDetails.class, to); + } + + // RSH3e3 + public void test_WaitingForRegistrationUpdate_on_UpdatingRegistrationFailed() { + TestActivation activation = new TestActivation(); + State state = new WaitingForRegistrationSync(activation.machine, null); + ErrorInfo reason = new ErrorInfo("test", 123); + + final AsyncWaiter waiter = broadcastWaiter("PUSH_UPDATE_FAILED"); + + State to = state.transition(new SyncRegistrationFailed(reason)); + + // RSH3e3a + waiter.waitFor(); + assertNull(waiter.result); + assertEquals(reason, waiter.error); + + assertSize(0, activation.machine.pendingEvents); + + // RSH3e3b + assertInstanceOf(AfterRegistrationSyncFailed.class, to); + } + + // RSH3f1 + public void test_AfterRegistrationUpdateFailed_on_GotPushDeviceDetails() throws Exception { + new UpdateRegistrationTest() { + @Override + protected void setUpMachineState(TestCase testCase) throws AblyException { + testCase.testActivation.registerAndWait(); + testCase.testActivation.rest.push.activate(testCase.useCustomRegistrar); + testCase.testActivation.moveToAfterRegistrationUpdateFailed(); + } + }.run(); + } + + // RSH3f1 + public void test_AfterRegistrationUpdateFailed_on_CalledActivate() throws Exception { + new UpdateRegistrationTest("PUSH_ACTIVATE") { + @Override + protected void setUpMachineState(TestCase testCase) throws AblyException { + testCase.testActivation.registerAndWait(); + testCase.testActivation.moveToAfterRegistrationUpdateFailed(); + } + + @Override + protected String sendInitialEvent(UpdateRegistrationTest.TestCase testCase) throws AblyException { + testCase.testActivation.rest.push.activate(testCase.useCustomRegistrar); + return "testTokenFailed"; + } + }.run(); + } + + // RSH3f1 + public void test_AfterRegistrationUpdateFailed_on_CalledDeactivate() throws Exception { + new DeactivateTest(AfterRegistrationSyncFailed.class) { + @Override + protected void setUpMachineState(TestCase testCase) throws AblyException { + testCase.testActivation.registerAndWait(); + testCase.testActivation.moveToAfterRegistrationUpdateFailed(); + } + }.run(); + } + + // RSH3g1 + public void test_WaitingForDeregistration_on_CalledDeactivate() throws Exception { + TestActivation activation = new TestActivation(); + State state = new WaitingForDeregistration(activation.machine, null); + + State to = state.transition(new CalledDeactivate()); + + assertSize(0, activation.machine.pendingEvents); + assertInstanceOf(WaitingForDeregistration.class, to); + } + + // RSH3g2 + public void test_WaitingForDeregistration_on_Deregistered() throws Exception { + TestActivation activation = new TestActivation(); + State state = new WaitingForDeregistration(activation.machine, null); + + activation.rest.push.getLocalDevice().setDeviceIdentityToken("test"); + final Helpers.AsyncWaiter waiter = broadcastWaiter("PUSH_DEACTIVATE"); + + State to = state.transition(new Deregistered()); + + // RSH3g2b + waiter.waitFor(); + assertNull(waiter.error); + + // RSH3g2a + assertNull(activation.rest.push.getLocalDevice().deviceIdentityToken); + + // RSH3g2c + assertSize(0, activation.machine.pendingEvents); + assertInstanceOf(NotActivated.class, to); + } + + // RSH3g3 + public void test_WaitingForDeregistration_on_DeregistrationFailed() throws Exception { + class TestCase extends TestCases.Base { + private TestActivation testActivation; + private State previousState; + + public TestCase(String name, TestActivation testActivation, State previousState) { + super(name, null); + this.testActivation = testActivation; + this.previousState = previousState; + } + + @Override + public void run() throws Exception { + State state = new WaitingForDeregistration(testActivation.machine, previousState); + + Helpers.AsyncWaiter waiter = broadcastWaiter("PUSH_DEACTIVATE"); + ErrorInfo reason = new ErrorInfo("test", 123); + + State to = state.transition(new DeregistrationFailed(reason)); + + // RSH3g3a + waiter.waitFor(); + assertEquals(reason, waiter.error); + + // RSH3g3b + assertSize(0, testActivation.machine.pendingEvents); + assertInstanceOf(previousState.getClass(), to); + } + } + + TestCases testCases = new TestCases(); + + TestActivation activation0 = new TestActivation(); + testCases.add(new TestCase( + "from WaitingForNewPushDeviceDetails", + activation0, + new WaitingForNewPushDeviceDetails(activation0.machine))); + + TestActivation activation1 = new TestActivation(); + testCases.add(new TestCase( + "from AfterRegistrationSyncFailed", + activation1, + new AfterRegistrationSyncFailed(activation1.machine))); + + testCases.run(); + } + + // RSH4a1 + public void test_PushChannel_subscribeDevice_not_registered() throws AblyException { + TestActivation activation = new TestActivation(); + Channel channel = activation.rest.channels.get("pushenabled:foo"); + + try { + channel.push.subscribeDevice(); + fail("expected failure due to device not being registered"); + } catch (AblyException e) { + } finally { + String deviceId = activation.rest.push.getLocalDevice().id; + if(deviceId != null) { + PushBase.ChannelSubscription sub = PushBase.ChannelSubscription.forDevice(channel.name, deviceId); + activation.rest.push.admin.channelSubscriptions.remove(sub); + } + } + } + + // RSH4a2 + public void test_PushChannel_subscribeDevice_ok() throws AblyException { + TestActivation activation = new TestActivation(); + Channel channel = activation.rest.channels.get("pushenabled:foo"); + PushBase.ChannelSubscription sub = null; + + try { + activation.registerAndWait(); + sub = PushBase.ChannelSubscription.forDevice(channel.name, activation.rest.push.getLocalDevice().id); + channel.push.subscribeDevice(); + + PushBase.ChannelSubscription[] items = activation.rest.push.admin.channelSubscriptions.list(new Param[]{ + new Param("channel", channel.name), + new Param("deviceId", sub.deviceId), + new Param("fullWait", "true"), + }).items(); + assertSize(1, items); + assertEquals(items[0], sub); + } finally { + if(sub != null) { + activation.rest.push.admin.channelSubscriptions.remove(sub); + } + } + } + + // RSH4b1 + public void test_PushChannel_subscribeClient_not_registered() throws AblyException { + TestActivation activation = new TestActivation(); + Channel channel = activation.rest.channels.get("pushenabled:foo"); + + try { + channel.push.subscribeClient(); + fail("expected failure due to device not having a client ID"); + } catch (AblyException e) { + } + } + + // RSH4b2 + public void test_PushChannel_subscribeClient_ok() throws AblyException { + TestActivation activation = new TestActivation(); + final String testClientId = "testClient"; + activation.rest.auth.setClientId(testClientId); + activation.rest.auth.authorize(new Auth.TokenParams() {{ clientId = testClientId; }}, null); + + PushBase.ChannelSubscription sub = null; + try { + activation.registerAndWait(); + + Channel channel = activation.rest.channels.get("pushenabled:foo"); + sub = PushBase.ChannelSubscription.forClientId(channel.name, activation.rest.push.getLocalDevice().clientId); + channel.push.subscribeClient(); + + PushBase.ChannelSubscription[] items = activation.rest.push.admin.channelSubscriptions.list(new Param[]{ + new Param("channel", channel.name), + new Param("clientId", sub.clientId), + new Param("fullWait", "true"), + }).items(); + assertSize(1, items); + assertEquals(items[0], sub); + } finally { + if(sub != null) { + activation.rest.push.admin.channelSubscriptions.remove(sub); + } + } + } + + // RSH4c1 + public void test_PushChannel_unsubscribeDevice_not_registered() throws AblyException { + TestActivation activation = new TestActivation(); + Channel channel = activation.rest.channels.get("pushenabled:foo"); + PushBase.ChannelSubscription sub = PushBase.ChannelSubscription.forDevice(channel.name, activation.rest.push.getLocalDevice().id); + + try { + channel.push.unsubscribeDevice(); + fail("expected failure due to device not being registered"); + } catch (AblyException e) { + } + } + + // RSH4c2 + public void test_PushChannel_unsubscribeDevice_ok() throws AblyException { + TestActivation activation = new TestActivation(); + Channel channel = activation.rest.channels.get("pushenabled:foo"); + PushBase.ChannelSubscription sub = null; + + try { + activation.registerAndWait(); + sub = PushBase.ChannelSubscription.forDevice(channel.name, activation.rest.push.getLocalDevice().id); + + activation.rest.push.admin.channelSubscriptions.save(sub); + + channel.push.unsubscribeDevice(); + + PushBase.ChannelSubscription[] items = activation.rest.push.admin.channelSubscriptions.list(new Param[]{ + new Param("channel", channel.name), + new Param("deviceId", sub.deviceId), + new Param("fullWait", "true"), + }).items(); + assertSize(0, items); + } finally { + if(sub != null) { + activation.rest.push.admin.channelSubscriptions.remove(sub); + } + } + } + + // RSH4d1 + public void test_PushChannel_unsubscribeClient_not_registered() throws AblyException { + TestActivation activation = new TestActivation(); + Channel channel = activation.rest.channels.get("pushenabled:foo"); + PushBase.ChannelSubscription sub = PushBase.ChannelSubscription.forClientId(channel.name, activation.rest.push.getLocalDevice().clientId); + + try { + channel.push.unsubscribeClient(); + fail("expected failure due to device not having a client ID"); + } catch (AblyException e) { + } + } + + // RSH4d2 + public void test_PushChannel_unsubscribeClient_ok() throws AblyException { + TestActivation activation = new TestActivation(); + final String testClientId = "testClient"; + activation.rest.auth.setClientId(testClientId); + activation.rest.auth.authorize(new Auth.TokenParams() {{ clientId = testClientId; }}, null); + + Channel channel = activation.rest.channels.get("pushenabled:foo"); + PushBase.ChannelSubscription sub = null; + + try { + activation.registerAndWait(); + sub = PushBase.ChannelSubscription.forClientId(channel.name, activation.rest.push.getLocalDevice().clientId); + + activation.rest.push.admin.channelSubscriptions.save(sub); + + channel.push.unsubscribeClient(); + + PushBase.ChannelSubscription[] items = activation.rest.push.admin.channelSubscriptions.list(new Param[]{ + new Param("channel", channel.name), + new Param("clientId", sub.clientId), + new Param("fullWait", "true"), + }).items(); + assertSize(0, items); + } finally { + if(sub != null) { + activation.rest.push.admin.channelSubscriptions.remove(sub); + } + } + } + + // RSH4e + public void test_PushChannel_listSubscriptions() throws Exception { + class TestCase extends TestCases.Base { + private boolean useClientId; + private TestActivation testActivation; + + public TestCase(String name, boolean useClientId) { + super(name, null); + this.useClientId = useClientId; + } + + @Override + public void run() throws Exception { + testActivation = new TestActivation(); + if (useClientId) { + final String testClientId = "testClient"; + testActivation.rest.auth.setClientId(testClientId); + testActivation.rest.auth.authorize(new Auth.TokenParams() {{ clientId = testClientId; }}, null); + } else { + testActivation.rest.auth.authorize(null, null); + } + + testActivation.registerAndWait(); + DeviceDetails otherDevice = DeviceDetails.fromJsonObject(JsonUtils.object() + .add("id", "other") + .add("platform", "android") + .add("formFactor", "tablet") + .add("metadata", JsonUtils.object()) + .add("push", JsonUtils.object() + .add("recipient", JsonUtils.object() + .add("transportType", "fcm") + .add("registrationToken", "qux"))) + .toJson()); + + String deviceId = testActivation.rest.push.getLocalDevice().id; + + Push.ChannelSubscription[] fixtures = new Push.ChannelSubscription[] { + PushBase.ChannelSubscription.forDevice("pushenabled:foo", deviceId), + PushBase.ChannelSubscription.forDevice("pushenabled:foo", "other"), + PushBase.ChannelSubscription.forDevice("pushenabled:bar", deviceId), + PushBase.ChannelSubscription.forClientId("pushenabled:foo", "testClient"), + PushBase.ChannelSubscription.forClientId("pushenabled:foo", "otherClient"), + PushBase.ChannelSubscription.forClientId("pushenabled:bar", "testClient"), + }; + + try { + testActivation.adminRest.push.admin.deviceRegistrations.save(otherDevice); + + for (PushBase.ChannelSubscription sub : fixtures) { + testActivation.adminRest.push.admin.channelSubscriptions.save(sub); + } + + Push.ChannelSubscription[] got = testActivation.rest.channels.get("pushenabled:foo").push.listSubscriptions().items(); + + ArrayList expected = new ArrayList<>(2); + expected.add(PushBase.ChannelSubscription.forDevice("pushenabled:foo", deviceId)); + if (useClientId) { + expected.add(PushBase.ChannelSubscription.forClientId("pushenabled:foo", "testClient")); + } + + assertArrayUnorderedEquals(expected.toArray(), got); + } finally { + testActivation.adminRest.push.admin.deviceRegistrations.remove(otherDevice); + for (PushBase.ChannelSubscription sub : fixtures) { + testActivation.adminRest.push.admin.channelSubscriptions.remove(sub); + } + } + } + } + + TestCases testCases = new TestCases(); + + testCases.add(new TestCase("without client ID", false)); + testCases.add(new TestCase("with client ID", true)); + + testCases.run(); + } + + public void test_Realtime_push_interface() throws Exception { + AblyRealtime realtime = new AblyRealtime(new ClientOptions() {{ + autoConnect = false; + key = "madeup"; + }}); + realtime.setAndroidContext(getContext()); + assertInstanceOf(LocalDevice.class, realtime.push.getLocalDevice()); + assertInstanceOf(Push.class, realtime.push); + assertInstanceOf(PushChannel.class, realtime.channels.get("test").push); + } + + public void test_push_AfterRegistrationUpdateFailed_migrate_to_AfterRegistrationSyncFailed() { + new TestActivation(); // Just for the side effect of clearing persisted state. + + SharedPreferences.Editor editor = PreferenceManager.getDefaultSharedPreferences(getContext().getApplicationContext()).edit(); + editor.putString(ActivationStateMachine.PersistKeys.CURRENT_STATE, "io.ably.lib.push.ActivationStateMachine$AfterRegistrationUpdateFailed"); + assertTrue(editor.commit()); + + TestActivation activation = new TestActivation(new Helpers.AblyFunction() { + @Override + public Void apply(TestActivation.Options options) throws AblyException { + options.clearPersisted = false; + return null; + } + }); + assertInstanceOf(AfterRegistrationSyncFailed.class, activation.machine.current); + } + + // https://github.com/ably/ably-java/issues/598 + public void test_restore_non_nullary_event() { + TestActivation activation = new TestActivation(); + assertInstanceOf(NotActivated.class, activation.machine.current); + + SyncRegistrationFailed event = new SyncRegistrationFailed(new ErrorInfo()); + + activation.machine.handleEvent(event); + + // NotActivated can't handle SyncRegistrationFailed, so it should be pending. + assertTrue(activation.machine.pendingEvents.contains(event)); + + // Now recover the persisted state and events. + + activation = new TestActivation(new Helpers.AblyFunction() { + @Override + public Void apply(TestActivation.Options options) throws AblyException { + options.clearPersisted = false; + return null; + } + }); + + // Since the event doesn't have a nullary constructor, it should be dropped. + assertInstanceOf(NotActivated.class, activation.machine.current); + assertSize(0, activation.machine.pendingEvents); + } + + // This is all copied and pasted from ParameterizedTest, since I can't inherit from it. + // I need to inherit from AndroidPushTest, and Java doesn't have multiple inheritance + // or mixins or something like that. + + protected static Setup.TestVars testVars; + + public static void setUpBeforeClass() throws Exception { + testVars = Setup.getTestVars(); + } + + public static void tearDownAfterClass() throws Exception { + Setup.clearTestVars(); + } + + private Setup.TestParameters testParams = Setup.TestParameters.getDefault(); + + protected DebugOptions createOptions() throws AblyException { + return testVars.createOptions(testParams); + } + + protected DebugOptions createOptions(String key) throws AblyException { + return testVars.createOptions(key, testParams); + } + + protected void fillInOptions(ClientOptions opts) { + testVars.fillInOptions(opts, testParams); + } + + private class TestActivationContext extends ActivationContext { + public Helpers.AblyFunction, Void> onGetRegistrationToken; + + TestActivationContext(Context context) { + super(context); + this.onGetRegistrationToken = new Helpers.AblyFunction, Void>() { + @Override + public Void apply(Callback callback) throws AblyException { + callback.onSuccess(ULID.random()); + return null; + } + }; + } + + @Override + public synchronized ActivationStateMachine getActivationStateMachine() { + if(activationStateMachine == null) { + activationStateMachine = new TestActivationStateMachine(this); + } + return activationStateMachine; + } + + @Override + protected void getRegistrationToken(final Callback callback) { + try { + this.onGetRegistrationToken.apply(callback); + } catch (AblyException e) { + callback.onError(ErrorInfo.fromThrowable(e)); + } + } + } + + private class TestActivationStateMachine extends ActivationStateMachine { + class EventOrStateWaiter extends CompletionWaiter { + Class event; + Class state; + + public boolean shouldFire(State state, Event event) { + if (this.state != null) { + if (this.state.isInstance(state)) { + return true; + } + } else if (this.event != null) { + if (this.event.isInstance(event)) { + return true; + } + } else { + return true; + } + return false; + } + } + + private BlockingQueue events = null; + private EventOrStateWaiter waiter; + private Class waitingForState; + + public TestActivationStateMachine(ActivationContext activationContext) { + super(activationContext); + } + + @Override + public synchronized boolean handleEvent(Event event) { + if (events != null) { + try { + events.put(event); + } catch (InterruptedException e) {} + } + + boolean ok = super.handleEvent(event); + + if (waiter != null && waiter.shouldFire(current, event)) { + CompletionWaiter w = waiter; + waiter = null; + w.onSuccess(); + } + return ok; + } + + @Override + public boolean reset() { + waiter = null; + events = null; + return super.reset(); + } + + public BlockingQueue getEventReceiver(int capacity) { + events = new ArrayBlockingQueue(capacity); + return events; + } + + public CompletionWaiter getEventHandledWaiter() { + return getEventHandledWaiter(null); + } + + public CompletionWaiter getEventHandledWaiter(final Class e) { + waiter = new EventOrStateWaiter() {{ + event = e; + }}; + return waiter; + } + + public CompletionWaiter getTransitionedToWaiter(final Class s) { + waiter = new EventOrStateWaiter() {{ + state = s; + }}; + return waiter; + } + } + + private AsyncWaiter broadcastWaiter(String event) { + final AsyncWaiter waiter = new AsyncWaiter(); + BroadcastReceiver onceReceiver = new BroadcastReceiver() { + @Override + public void onReceive(Context context, Intent intent) { + LocalBroadcastManager.getInstance(context.getApplicationContext()).unregisterReceiver(this); + ErrorInfo error = IntentUtils.getErrorInfo(intent); + if (error == null) { + waiter.onSuccess(intent); + } else { + waiter.onError(error); + } + } + }; + IntentFilter filter = new IntentFilter("io.ably.broadcast." + event); + LocalBroadcastManager.getInstance(getContext().getApplicationContext()).registerReceiver(onceReceiver, filter); + return waiter; + } + + private void sendBroadcast(String name, Intent intent) { + intent.setAction("io.ably.broadcast." + name); + LocalBroadcastManager.getInstance(getContext().getApplicationContext()).sendBroadcast(intent); + } + + private abstract class DeactivateTest { + private Class previousState; + + DeactivateTest(Class previousState) { + this.previousState = previousState; + } + + protected abstract void setUpMachineState(TestCase testCase) throws AblyException; + + class TestCase extends TestCases.Base { + private final ErrorInfo deregisterError; + private final boolean useCustomDeregisterer; + private final Class expectedEvent; + private final Class expectedState; + protected TestActivation testActivation; + + public TestCase(String name, boolean useCustomDeregisterer, ErrorInfo error, Class expectedEvent, Class expectedState) { + super(name, null); + this.useCustomDeregisterer = useCustomDeregisterer; + this.deregisterError = error; + this.expectedEvent = expectedEvent; + this.expectedState = expectedState; + } + + @Override + public void run() throws Exception { + try { + testActivation = new TestActivation(); + + setUpMachineState(this); + + final AsyncWaiter deregisterCallback = useCustomDeregisterer ? broadcastWaiter("PUSH_DEREGISTER_DEVICE") : null; + final AsyncWaiter deactivateCallback = broadcastWaiter("PUSH_DEACTIVATE"); + AsyncWaiter requestWaiter = null; + + if (!useCustomDeregisterer) { + if (deregisterError != null) { + testActivation.httpTracker.mockResponse = Helpers.httpResponseFromErrorInfo(deregisterError); + } + + requestWaiter = testActivation.httpTracker.getRequestWaiter(); + // Block until we've checked the intermediate WaitingForDeregistration state, + // before the request's response causes another state transition. + // Otherwise, our test would be racing against the request. + testActivation.httpTracker.lockRequests(); + } + + CompletionWaiter deactivatingWaiter = testActivation.machine.getTransitionedToWaiter(WaitingForDeregistration.class); + // Will send a CalledDeactivate event. + testActivation.rest.push.deactivate(useCustomDeregisterer); + deactivatingWaiter.waitFor(); + + if (useCustomDeregisterer) { + // RSH3d2a + deregisterCallback.waitFor(); + assertNull(deregisterCallback.error); + } else { + // RSH3d2b + requestWaiter.waitFor(); + Helpers.RawHttpRequest request = requestWaiter.result; + assertEquals("DELETE", request.method); + assertEquals("/push/deviceRegistrations/" + testActivation.rest.push.getLocalDevice().id, request.url.getPath()); + } + + // RSH3d2d + assertInstanceOf(WaitingForDeregistration.class, testActivation.machine.current); + + // Now wait for next event, after deregistration. + CompletionWaiter handled = testActivation.machine.getEventHandledWaiter(); + BlockingQueue events = testActivation.machine.getEventReceiver(1); + + if (useCustomDeregisterer) { + Intent intent = new Intent(); + if (deregisterError != null) { + IntentUtils.addErrorInfo(intent, deregisterError); + } + sendBroadcast("PUSH_DEVICE_DEREGISTERED", intent); + } else { + testActivation.httpTracker.unlockRequests(); + } + + assertInstanceOf(expectedEvent, events.poll(10, TimeUnit.SECONDS)); + assertNull(handled.waitFor()); + + if (deregisterError == null) { + // RSH3g2a + assertNull(testActivation.rest.push.getLocalDevice().deviceIdentityToken); + } else { + // RSH3g3a + assertNotNull(testActivation.rest.push.getLocalDevice().deviceIdentityToken); + } + + // RSH3g2b, RSH3g3a + deactivateCallback.waitFor(); + assertEquals(deregisterError, deactivateCallback.error); + assertInstanceOf(expectedState, testActivation.machine.current); + } finally { + testActivation.httpTracker.unlockRequests(); + testActivation.rest.push.admin.deviceRegistrations.remove(testActivation.rest.push.getLocalDevice()); + } + } + } + + public void run() throws Exception { + TestCases testCases = new TestCases(); + + // RSH3g2 + testCases.add(new TestCase( + "ok with custom deregisterer", + true, + null, + Deregistered.class, + NotActivated.class /* RSH3g2c */)); + + testCases.add(new TestCase( + "ok with default deregisterer", + false, + null, + Deregistered.class, + NotActivated.class /* RSH3g2c */)); + + // RSH3g3 + testCases.add(new TestCase( + "failing with custom deregisterer", + true, + new ErrorInfo("testError", 123), + DeregistrationFailed.class, + previousState /* RSH3g3b */)); + + testCases.add(new TestCase( + "failing with default deregisterer", + false, + new ErrorInfo("testError", 123), + DeregistrationFailed.class, + previousState /* RSH3g3b */)); + + testCases.run(); + } + } + + private abstract class UpdateRegistrationTest { + private final String onFailedEvent; + + protected abstract void setUpMachineState(TestCase testCase) throws AblyException; + + UpdateRegistrationTest() { + this("PUSH_UPDATE_FAILED"); + } + + UpdateRegistrationTest(String onFailedEvent) { + this.onFailedEvent = onFailedEvent; + } + + class TestCase extends TestCases.Base { + private final ErrorInfo updateError; + private final boolean useCustomRegistrar; + private final Class expectedEvent; + private final Class expectedState; + protected TestActivation testActivation; + + public TestCase(String name, boolean useCustomRegistrar, ErrorInfo error, Class expectedEvent, Class expectedState) { + super(name, null); + this.useCustomRegistrar = useCustomRegistrar; + this.updateError = error; + this.expectedEvent = expectedEvent; + this.expectedState = expectedState; + } + + @Override + public void run() throws Exception { + try { + testActivation = new TestActivation(); + + setUpMachineState(this); + final boolean isExpectingRegistrationValidation = testActivation.machine.current instanceof AfterRegistrationSyncFailed; + + final AsyncWaiter registerCallback = useCustomRegistrar ? broadcastWaiter("PUSH_REGISTER_DEVICE") : null; + final AsyncWaiter updateFailedCallback = updateError != null ? broadcastWaiter(onFailedEvent) : null; + AsyncWaiter requestWaiter = null; + + if (!useCustomRegistrar) { + if (updateError != null) { + testActivation.httpTracker.mockResponse = Helpers.httpResponseFromErrorInfo(updateError); + } + + requestWaiter = testActivation.httpTracker.getRequestWaiter(); + // Block until we've checked the intermediate WaitingForDeregistration state, + // before the request's response causes another state transition. + // Otherwise, our test would be racing against the request. + testActivation.httpTracker.lockRequests(); + } + + CompletionWaiter updatingWaiter = testActivation.machine.getTransitionedToWaiter(WaitingForRegistrationSync.class); + String updatedRegistrationToken = sendInitialEvent(this); + updatingWaiter.waitFor(); + + if (useCustomRegistrar) { + // RSH3d3a + registerCallback.waitFor(); + assertNull(registerCallback.error); + } else { + requestWaiter.waitFor(); + Helpers.RawHttpRequest request = requestWaiter.result; + assertEquals("/push/deviceRegistrations/"+testActivation.rest.push.getLocalDevice().id, request.url.getPath()); + String authToken = Base64Coder.decodeString(request.requestHeaders.get("X-Ably-DeviceToken").get(0)); + assertEquals(testActivation.rest.push.getLocalDevice().deviceIdentityToken, authToken); + + JsonObject requestBody = (JsonObject)Serialisation.msgpackToGson(request.requestBody.getEncoded()); + JsonObject requestRecipient = requestBody.getAsJsonObject("push").getAsJsonObject("recipient"); + assertEquals("fcm", requestRecipient.getAsJsonPrimitive("transportType").getAsString()); + assertEquals(updatedRegistrationToken, requestRecipient.getAsJsonPrimitive("registrationToken").getAsString()); + + if(isExpectingRegistrationValidation) { + // RSH3f1a: PUT the entire DeviceDetails + assertEquals("PUT", request.method); + assertTrue(requestBody.has("deviceSecret")); + assertTrue(requestBody.has("clientId")); + } else { + // RSH3d3b: PATCH the updated members + assertEquals("PATCH", request.method); + assertFalse(requestBody.has("deviceSecret")); + assertFalse(requestBody.has("clientId")); + } + } + + // RSH3d3d + assertInstanceOf(WaitingForRegistrationSync.class, testActivation.machine.current); + + // Now wait for next event, after updated. + CompletionWaiter handled = testActivation.machine.getEventHandledWaiter(); + BlockingQueue events = testActivation.machine.getEventReceiver(1); + + if (useCustomRegistrar) { + Intent intent = new Intent(); + if (updateError != null) { + IntentUtils.addErrorInfo(intent, updateError); + } + sendBroadcast("PUSH_DEVICE_REGISTERED", intent); + } else { + testActivation.httpTracker.unlockRequests(); + } + + assertInstanceOf(expectedEvent, events.poll(10, TimeUnit.SECONDS)); + assertNull(handled.waitFor()); + + if (updateError != null) { + // RSH3e3a + updateFailedCallback.waitFor(); + assertEquals(updateError, updateFailedCallback.error); + } + // RSH3e2a, RSH3e3b + assertInstanceOf(expectedState, testActivation.machine.current); + } finally { + testActivation.httpTracker.unlockRequests(); + testActivation.rest.push.admin.deviceRegistrations.remove(testActivation.rest.push.getLocalDevice()); + } + } + } + + public void run() throws Exception { + TestCases testCases = new TestCases(); + + // RSH3e2 + testCases.add(new TestCase( + "ok with custom registerer", + true, + null, + RegistrationSynced.class, + WaitingForNewPushDeviceDetails.class)); + + testCases.add(new TestCase( + "ok with default registerer", + false, + null, + RegistrationSynced.class, + WaitingForNewPushDeviceDetails.class)); + + // RSH3e3 + testCases.add(new TestCase( + "failing with custom registerer", + true, + new ErrorInfo("testError", 123), + SyncRegistrationFailed.class, + AfterRegistrationSyncFailed.class)); + + testCases.add(new TestCase( + "failing with default registerer", + false, + new ErrorInfo("testError", 123), + SyncRegistrationFailed.class, + AfterRegistrationSyncFailed.class)); + + testCases.run(); + } + + protected String sendInitialEvent(TestCase testCase) throws AblyException { + // Will send GotPushDeviceDetails event. + CalledActivate.useCustomRegistrar(testCase.useCustomRegistrar, PreferenceManager.getDefaultSharedPreferences(getContext())); + testCase.testActivation.rest.push.getActivationContext().onNewRegistrationToken(RegistrationToken.Type.FCM, "testTokenUpdated"); + return "testTokenUpdated"; + } + } } diff --git a/android/src/androidTest/java/io/ably/lib/test/android/AndroidSuite.java b/android/src/androidTest/java/io/ably/lib/test/android/AndroidSuite.java index e5c6d3fa1..9359f2feb 100644 --- a/android/src/androidTest/java/io/ably/lib/test/android/AndroidSuite.java +++ b/android/src/androidTest/java/io/ably/lib/test/android/AndroidSuite.java @@ -26,70 +26,70 @@ * Tests specific for Android */ public class AndroidSuite { - private static SessionHandlerNanoHTTPD server; - - @BeforeClass - public static void setUp() throws IOException { - /* Create custom RouterNanoHTTPD class for getting session object */ - server = new SessionHandlerNanoHTTPD(27333); - server.start(NanoHTTPD.SOCKET_READ_TIMEOUT, true); - - while (!server.wasStarted()) { - try { - Thread.sleep(100); - } catch (InterruptedException e) { - e.printStackTrace(); - } - } - } - - @AfterClass - public static void tearDown() { - server.stop(); - } - - @Test - public void android_http_header_test() { - try { - /* Init values for local server */ - Setup.TestVars testVars = Setup.getTestVars(); - ClientOptions opts = new ClientOptions(testVars.keys[0].keyStr); - opts.tls = false; - opts.port = server.getListeningPort(); - opts.restHost = "localhost"; - AblyRest ably = new AblyRest(opts); - - ably.time(); - - Map headers = server.getHeaders(); - - assertNotNull("Verify ably server was reached", headers); - String header = headers.get(Defaults.ABLY_LIB_HEADER.toLowerCase()); - assertTrue("Verify correct library header was passed to the server", header != null && header.startsWith("android")); - } - catch (AblyException e) { - e.printStackTrace(); - fail(); - } - - } - - private static class SessionHandlerNanoHTTPD extends RouterNanoHTTPD { - public Map headers; - - public SessionHandlerNanoHTTPD(int port) { - super(port); - } - - @Override - public Response serve(IHTTPSession session) { - headers = new HashMap<>(session.getHeaders()); - return newFixedLengthResponse(String.format(Locale.US, "[%d]", System.currentTimeMillis())); - } - - public Map getHeaders() { - return headers; - } - } + private static SessionHandlerNanoHTTPD server; + + @BeforeClass + public static void setUp() throws IOException { + /* Create custom RouterNanoHTTPD class for getting session object */ + server = new SessionHandlerNanoHTTPD(27333); + server.start(NanoHTTPD.SOCKET_READ_TIMEOUT, true); + + while (!server.wasStarted()) { + try { + Thread.sleep(100); + } catch (InterruptedException e) { + e.printStackTrace(); + } + } + } + + @AfterClass + public static void tearDown() { + server.stop(); + } + + @Test + public void android_http_header_test() { + try { + /* Init values for local server */ + Setup.TestVars testVars = Setup.getTestVars(); + ClientOptions opts = new ClientOptions(testVars.keys[0].keyStr); + opts.tls = false; + opts.port = server.getListeningPort(); + opts.restHost = "localhost"; + AblyRest ably = new AblyRest(opts); + + ably.time(); + + Map headers = server.getHeaders(); + + assertNotNull("Verify ably server was reached", headers); + String header = headers.get(Defaults.ABLY_LIB_HEADER.toLowerCase()); + assertTrue("Verify correct library header was passed to the server", header != null && header.startsWith("android")); + } + catch (AblyException e) { + e.printStackTrace(); + fail(); + } + + } + + private static class SessionHandlerNanoHTTPD extends RouterNanoHTTPD { + public Map headers; + + public SessionHandlerNanoHTTPD(int port) { + super(port); + } + + @Override + public Response serve(IHTTPSession session) { + headers = new HashMap<>(session.getHeaders()); + return newFixedLengthResponse(String.format(Locale.US, "[%d]", System.currentTimeMillis())); + } + + public Map getHeaders() { + return headers; + } + } } diff --git a/android/src/androidTest/java/io/ably/lib/test/loader/ArgumentLoader.java b/android/src/androidTest/java/io/ably/lib/test/loader/ArgumentLoader.java index 17a7785a9..03bfbaae9 100644 --- a/android/src/androidTest/java/io/ably/lib/test/loader/ArgumentLoader.java +++ b/android/src/androidTest/java/io/ably/lib/test/loader/ArgumentLoader.java @@ -4,8 +4,8 @@ import android.support.test.InstrumentationRegistry; public class ArgumentLoader { - public String getTestArgument(String name) { - Bundle arguments = InstrumentationRegistry.getArguments(); - return arguments.getString(name); - } + public String getTestArgument(String name) { + Bundle arguments = InstrumentationRegistry.getArguments(); + return arguments.getString(name); + } } diff --git a/android/src/androidTest/java/io/ably/lib/test/loader/ResourceLoader.java b/android/src/androidTest/java/io/ably/lib/test/loader/ResourceLoader.java index 64076bbf4..29c754c82 100644 --- a/android/src/androidTest/java/io/ably/lib/test/loader/ResourceLoader.java +++ b/android/src/androidTest/java/io/ably/lib/test/loader/ResourceLoader.java @@ -8,23 +8,23 @@ import java.io.IOException; public class ResourceLoader { - public byte[] read(String resourceName) throws IOException { - InputStream is = null; - byte[] bytes = null; - try { - is = instrumentationCtx.getAssets().open(resourceName); - Log.v(TAG, "Reading " + is.available() + " bytes for resource " + resourceName); - bytes = new byte[is.available()]; - is.read(bytes); - } catch(IOException ioe) { - Log.e(TAG, "Unexpected exception reading asset resource", ioe); - } finally { - if(is != null) - is.close(); - return bytes; - } - } + public byte[] read(String resourceName) throws IOException { + InputStream is = null; + byte[] bytes = null; + try { + is = instrumentationCtx.getAssets().open(resourceName); + Log.v(TAG, "Reading " + is.available() + " bytes for resource " + resourceName); + bytes = new byte[is.available()]; + is.read(bytes); + } catch(IOException ioe) { + Log.e(TAG, "Unexpected exception reading asset resource", ioe); + } finally { + if(is != null) + is.close(); + return bytes; + } + } - private static final String TAG = ResourceLoader.class.getName(); - private Context instrumentationCtx = InstrumentationRegistry.getContext(); + private static final String TAG = ResourceLoader.class.getName(); + private Context instrumentationCtx = InstrumentationRegistry.getContext(); } diff --git a/android/src/main/java/io/ably/lib/platform/AndroidNetworkConnectivity.java b/android/src/main/java/io/ably/lib/platform/AndroidNetworkConnectivity.java index de42260d9..8510d8475 100644 --- a/android/src/main/java/io/ably/lib/platform/AndroidNetworkConnectivity.java +++ b/android/src/main/java/io/ably/lib/platform/AndroidNetworkConnectivity.java @@ -13,65 +13,65 @@ public class AndroidNetworkConnectivity extends NetworkConnectivity { - AndroidNetworkConnectivity(Context applicationContext) { - this.applicationContext = applicationContext; - } + AndroidNetworkConnectivity(Context applicationContext) { + this.applicationContext = applicationContext; + } - public static AndroidNetworkConnectivity getNetworkConnectivity(Context applicationContext) { - AndroidNetworkConnectivity networkConnectivity; - synchronized (contexts) { - networkConnectivity = contexts.get(applicationContext); - if(networkConnectivity == null) { - contexts.put(applicationContext, (networkConnectivity = new AndroidNetworkConnectivity(applicationContext))); - } - } - return networkConnectivity; - } + public static AndroidNetworkConnectivity getNetworkConnectivity(Context applicationContext) { + AndroidNetworkConnectivity networkConnectivity; + synchronized (contexts) { + networkConnectivity = contexts.get(applicationContext); + if(networkConnectivity == null) { + contexts.put(applicationContext, (networkConnectivity = new AndroidNetworkConnectivity(applicationContext))); + } + } + return networkConnectivity; + } - protected void onNonempty() { - activate(); - } + protected void onNonempty() { + activate(); + } - protected void onEmpty() { - deactivate(); - } + protected void onEmpty() { + deactivate(); + } - private void activate() { - if(networkStateReceiver == null && applicationContext != null) { - networkStateReceiver = new NetworkStateReceiver(); - applicationContext.registerReceiver(networkStateReceiver, new IntentFilter(ConnectivityManager.CONNECTIVITY_ACTION)); - } - } + private void activate() { + if(networkStateReceiver == null && applicationContext != null) { + networkStateReceiver = new NetworkStateReceiver(); + applicationContext.registerReceiver(networkStateReceiver, new IntentFilter(ConnectivityManager.CONNECTIVITY_ACTION)); + } + } - private void deactivate() { - if(networkStateReceiver != null) { - applicationContext.unregisterReceiver(networkStateReceiver); - networkStateReceiver = null; - } - } + private void deactivate() { + if(networkStateReceiver != null) { + applicationContext.unregisterReceiver(networkStateReceiver); + networkStateReceiver = null; + } + } - private class NetworkStateReceiver extends BroadcastReceiver { - public NetworkStateReceiver() { - } + private class NetworkStateReceiver extends BroadcastReceiver { + public NetworkStateReceiver() { + } - public void onReceive(Context context, Intent intent) { - if(intent == null || intent.getExtras() == null) { - return; - } + public void onReceive(Context context, Intent intent) { + if(intent == null || intent.getExtras() == null) { + return; + } - ConnectivityManager manager = (ConnectivityManager) context.getSystemService(Context.CONNECTIVITY_SERVICE); - NetworkInfo ni = manager.getActiveNetworkInfo(); + ConnectivityManager manager = (ConnectivityManager) context.getSystemService(Context.CONNECTIVITY_SERVICE); + NetworkInfo ni = manager.getActiveNetworkInfo(); - if(ni != null && ni.getState() == NetworkInfo.State.CONNECTED) { - notifyNetworkAvailable(); - } else if(intent.getBooleanExtra(ConnectivityManager.EXTRA_NO_CONNECTIVITY,Boolean.FALSE)) { - notifyNetworkUnavailable(new ErrorInfo("No network connection available", 503, 80003)); - } - } - } + if(ni != null && ni.getState() == NetworkInfo.State.CONNECTED) { + notifyNetworkAvailable(); + } else if(intent.getBooleanExtra(ConnectivityManager.EXTRA_NO_CONNECTIVITY,Boolean.FALSE)) { + notifyNetworkUnavailable(new ErrorInfo("No network connection available", 503, 80003)); + } + } + } - private final Context applicationContext; - private NetworkStateReceiver networkStateReceiver; + private final Context applicationContext; + private NetworkStateReceiver networkStateReceiver; - private static WeakHashMap contexts = new WeakHashMap(); + private static WeakHashMap contexts = new WeakHashMap(); } diff --git a/android/src/main/java/io/ably/lib/platform/Platform.java b/android/src/main/java/io/ably/lib/platform/Platform.java index b4182f47b..839a77d84 100644 --- a/android/src/main/java/io/ably/lib/platform/Platform.java +++ b/android/src/main/java/io/ably/lib/platform/Platform.java @@ -14,40 +14,40 @@ import java.util.WeakHashMap; public class Platform { - public static final String name = "android"; - - public Platform() {} - - public Context getApplicationContext() { - return applicationContext; - } - /** - * Set the Android Context for this instance - */ - public void setAndroidContext(Context context) throws AblyException { - context = context.getApplicationContext(); - if(applicationContext != null) { - if(context == applicationContext) { - return; - } - throw AblyException.fromErrorInfo(new ErrorInfo("Incompatible application context set", 40000, 400)); - } - applicationContext = context; - AndroidNetworkConnectivity.getNetworkConnectivity(context).addListener(this.networkConnectivity); - } - - public boolean hasApplicationContext() { - return applicationContext != null; - } - - /** - * Get the NetworkConnectivity tracker instance for this context - * @return - */ - public NetworkConnectivity getNetworkConnectivity() { - return networkConnectivity; - } - - private Context applicationContext; - private final DelegatedNetworkConnectivity networkConnectivity = new DelegatedNetworkConnectivity(); + public static final String name = "android"; + + public Platform() {} + + public Context getApplicationContext() { + return applicationContext; + } + /** + * Set the Android Context for this instance + */ + public void setAndroidContext(Context context) throws AblyException { + context = context.getApplicationContext(); + if(applicationContext != null) { + if(context == applicationContext) { + return; + } + throw AblyException.fromErrorInfo(new ErrorInfo("Incompatible application context set", 40000, 400)); + } + applicationContext = context; + AndroidNetworkConnectivity.getNetworkConnectivity(context).addListener(this.networkConnectivity); + } + + public boolean hasApplicationContext() { + return applicationContext != null; + } + + /** + * Get the NetworkConnectivity tracker instance for this context + * @return + */ + public NetworkConnectivity getNetworkConnectivity() { + return networkConnectivity; + } + + private Context applicationContext; + private final DelegatedNetworkConnectivity networkConnectivity = new DelegatedNetworkConnectivity(); } diff --git a/android/src/main/java/io/ably/lib/push/AblyFirebaseInstanceIdService.java b/android/src/main/java/io/ably/lib/push/AblyFirebaseInstanceIdService.java index 4a99cd022..282afff2b 100644 --- a/android/src/main/java/io/ably/lib/push/AblyFirebaseInstanceIdService.java +++ b/android/src/main/java/io/ably/lib/push/AblyFirebaseInstanceIdService.java @@ -6,17 +6,17 @@ public class AblyFirebaseInstanceIdService { - /** - * Update Ably with the Registration Token - * @param context - * @param token - */ - public static void onNewRegistrationToken(Context context, String token) { - if(token != null && token.length() > 10) { - Log.i(TAG, "Firebase token registered: " + token.substring(0,10)); - } - ActivationContext.getActivationContext(context.getApplicationContext()).onNewRegistrationToken(RegistrationToken.Type.FCM, token); - } + /** + * Update Ably with the Registration Token + * @param context + * @param token + */ + public static void onNewRegistrationToken(Context context, String token) { + if(token != null && token.length() > 10) { + Log.i(TAG, "Firebase token registered: " + token.substring(0,10)); + } + ActivationContext.getActivationContext(context.getApplicationContext()).onNewRegistrationToken(RegistrationToken.Type.FCM, token); + } - private static final String TAG = AblyFirebaseInstanceIdService.class.getName(); + private static final String TAG = AblyFirebaseInstanceIdService.class.getName(); } diff --git a/android/src/main/java/io/ably/lib/push/ActivationContext.java b/android/src/main/java/io/ably/lib/push/ActivationContext.java index 703d37dbd..9156806fc 100644 --- a/android/src/main/java/io/ably/lib/push/ActivationContext.java +++ b/android/src/main/java/io/ably/lib/push/ActivationContext.java @@ -18,143 +18,143 @@ import java.util.WeakHashMap; public class ActivationContext { - public ActivationContext(Context context) { - this.context = context; - this.prefs = PreferenceManager.getDefaultSharedPreferences(context); - } - - Context getContext() { - return context; - } - SharedPreferences getPreferences() { return prefs; } - - public synchronized LocalDevice getLocalDevice() { - if(localDevice == null) { - localDevice = new LocalDevice(this); - } - return localDevice; - } - - public synchronized void setActivationStateMachine(ActivationStateMachine activationStateMachine) { - this.activationStateMachine = activationStateMachine; - } - - public synchronized ActivationStateMachine getActivationStateMachine() { - if(activationStateMachine == null) { - activationStateMachine = new ActivationStateMachine(this); - } - return activationStateMachine; - } - - public void setAbly(AblyRest ably) { - this.ably = ably; - this.clientId = ably.auth.clientId; - } - - AblyRest getAbly() throws AblyException { - if(ably != null) { - Log.v(TAG, "getAbly(): returning existing Ably instance"); - return ably; - } - - String deviceIdentityToken = getLocalDevice().deviceIdentityToken; - if(deviceIdentityToken == null) { - Log.e(TAG, "getAbly(): unable to create Ably instance using deviceIdentityToken"); - throw AblyException.fromErrorInfo(new ErrorInfo("Unable to get Ably library instance; no device identity token", 40000, 400)); - } - Log.v(TAG, "getAbly(): returning Ably instance using deviceIdentityToken"); - return (ably = new AblyRest(deviceIdentityToken)); - } - - public boolean setClientId(String clientId, boolean propagateGotPushDeviceDetails) { - boolean updated = !clientId.equals(this.clientId); - if(updated) { - this.clientId = clientId; - if(localDevice != null) { - /* Spec: RSH8d */ - localDevice.setClientId(clientId); - if(localDevice.isRegistered() && activationStateMachine != null && propagateGotPushDeviceDetails) { - /* Spec: RSH8e */ - activationStateMachine.handleEvent(new ActivationStateMachine.GotPushDeviceDetails()); - } - } - } - return updated; - } - - public void onNewRegistrationToken(RegistrationToken.Type type, String token) { - LocalDevice localDevice = getLocalDevice(); - RegistrationToken previous = localDevice.getRegistrationToken(); - if (previous != null) { - if (previous.type != type) { - Log.e(TAG, "trying to register device with " + type + ", but it was already registered with " + previous.type); - return; - } - if (previous.token.equals(token)) { - return; - } - } - Log.v(TAG, "onNewRegistrationToken(): updating token"); - localDevice.setAndPersistRegistrationToken(new RegistrationToken(type, token)); - getActivationStateMachine().handleEvent(new ActivationStateMachine.GotPushDeviceDetails()); - } - - public void reset() { - ably = null; - - getActivationStateMachine().reset(); - activationStateMachine = null; - - getLocalDevice().reset(); - localDevice = null; - } - - public static ActivationContext getActivationContext(Context applicationContext) { - return getActivationContext(applicationContext, null); - } - - public static ActivationContext getActivationContext(Context applicationContext, AblyRest ably) { - ActivationContext activationContext; - synchronized (activationContexts) { - activationContext = activationContexts.get(applicationContext); - if(activationContext == null) { - Log.v(TAG, "getActivationContext(): creating new ActivationContext for this application"); - activationContexts.put(applicationContext, (activationContext = new ActivationContext(applicationContext))); - } - if(ably != null) { - activationContext.setAbly(ably); - } - } - return activationContext; - } - - protected void getRegistrationToken(final Callback callback) { - FirebaseInstanceId.getInstance().getInstanceId() - .addOnCompleteListener(new OnCompleteListener() { - @Override - public void onComplete(Task task) { - if(task.isSuccessful()) { - /* Get new Instance ID token */ - String token = task.getResult().getToken(); - callback.onSuccess(token); - } else { - callback.onError(ErrorInfo.fromThrowable(task.getException())); - } - } - }); - } - - public static void setActivationContext(Context applicationContext, ActivationContext activationContext) { - activationContexts.put(applicationContext, activationContext); - } - - protected AblyRest ably; - protected String clientId; - protected ActivationStateMachine activationStateMachine; - protected LocalDevice localDevice; - protected final SharedPreferences prefs; - protected final Context context; - - private static WeakHashMap activationContexts = new WeakHashMap(); - private static final String TAG = ActivationContext.class.getName(); + public ActivationContext(Context context) { + this.context = context; + this.prefs = PreferenceManager.getDefaultSharedPreferences(context); + } + + Context getContext() { + return context; + } + SharedPreferences getPreferences() { return prefs; } + + public synchronized LocalDevice getLocalDevice() { + if(localDevice == null) { + localDevice = new LocalDevice(this); + } + return localDevice; + } + + public synchronized void setActivationStateMachine(ActivationStateMachine activationStateMachine) { + this.activationStateMachine = activationStateMachine; + } + + public synchronized ActivationStateMachine getActivationStateMachine() { + if(activationStateMachine == null) { + activationStateMachine = new ActivationStateMachine(this); + } + return activationStateMachine; + } + + public void setAbly(AblyRest ably) { + this.ably = ably; + this.clientId = ably.auth.clientId; + } + + AblyRest getAbly() throws AblyException { + if(ably != null) { + Log.v(TAG, "getAbly(): returning existing Ably instance"); + return ably; + } + + String deviceIdentityToken = getLocalDevice().deviceIdentityToken; + if(deviceIdentityToken == null) { + Log.e(TAG, "getAbly(): unable to create Ably instance using deviceIdentityToken"); + throw AblyException.fromErrorInfo(new ErrorInfo("Unable to get Ably library instance; no device identity token", 40000, 400)); + } + Log.v(TAG, "getAbly(): returning Ably instance using deviceIdentityToken"); + return (ably = new AblyRest(deviceIdentityToken)); + } + + public boolean setClientId(String clientId, boolean propagateGotPushDeviceDetails) { + boolean updated = !clientId.equals(this.clientId); + if(updated) { + this.clientId = clientId; + if(localDevice != null) { + /* Spec: RSH8d */ + localDevice.setClientId(clientId); + if(localDevice.isRegistered() && activationStateMachine != null && propagateGotPushDeviceDetails) { + /* Spec: RSH8e */ + activationStateMachine.handleEvent(new ActivationStateMachine.GotPushDeviceDetails()); + } + } + } + return updated; + } + + public void onNewRegistrationToken(RegistrationToken.Type type, String token) { + LocalDevice localDevice = getLocalDevice(); + RegistrationToken previous = localDevice.getRegistrationToken(); + if (previous != null) { + if (previous.type != type) { + Log.e(TAG, "trying to register device with " + type + ", but it was already registered with " + previous.type); + return; + } + if (previous.token.equals(token)) { + return; + } + } + Log.v(TAG, "onNewRegistrationToken(): updating token"); + localDevice.setAndPersistRegistrationToken(new RegistrationToken(type, token)); + getActivationStateMachine().handleEvent(new ActivationStateMachine.GotPushDeviceDetails()); + } + + public void reset() { + ably = null; + + getActivationStateMachine().reset(); + activationStateMachine = null; + + getLocalDevice().reset(); + localDevice = null; + } + + public static ActivationContext getActivationContext(Context applicationContext) { + return getActivationContext(applicationContext, null); + } + + public static ActivationContext getActivationContext(Context applicationContext, AblyRest ably) { + ActivationContext activationContext; + synchronized (activationContexts) { + activationContext = activationContexts.get(applicationContext); + if(activationContext == null) { + Log.v(TAG, "getActivationContext(): creating new ActivationContext for this application"); + activationContexts.put(applicationContext, (activationContext = new ActivationContext(applicationContext))); + } + if(ably != null) { + activationContext.setAbly(ably); + } + } + return activationContext; + } + + protected void getRegistrationToken(final Callback callback) { + FirebaseInstanceId.getInstance().getInstanceId() + .addOnCompleteListener(new OnCompleteListener() { + @Override + public void onComplete(Task task) { + if(task.isSuccessful()) { + /* Get new Instance ID token */ + String token = task.getResult().getToken(); + callback.onSuccess(token); + } else { + callback.onError(ErrorInfo.fromThrowable(task.getException())); + } + } + }); + } + + public static void setActivationContext(Context applicationContext, ActivationContext activationContext) { + activationContexts.put(applicationContext, activationContext); + } + + protected AblyRest ably; + protected String clientId; + protected ActivationStateMachine activationStateMachine; + protected LocalDevice localDevice; + protected final SharedPreferences prefs; + protected final Context context; + + private static WeakHashMap activationContexts = new WeakHashMap(); + private static final String TAG = ActivationContext.class.getName(); } diff --git a/android/src/main/java/io/ably/lib/push/ActivationStateMachine.java b/android/src/main/java/io/ably/lib/push/ActivationStateMachine.java index b205c7b60..d478b4549 100644 --- a/android/src/main/java/io/ably/lib/push/ActivationStateMachine.java +++ b/android/src/main/java/io/ably/lib/push/ActivationStateMachine.java @@ -26,684 +26,684 @@ import io.ably.lib.util.Serialisation; public class ActivationStateMachine { - public static class CalledActivate extends ActivationStateMachine.Event { - public static ActivationStateMachine.CalledActivate useCustomRegistrar(boolean useCustomRegistrar, SharedPreferences prefs) { - prefs.edit().putBoolean(ActivationStateMachine.PersistKeys.PUSH_CUSTOM_REGISTRAR, useCustomRegistrar).apply(); - return new ActivationStateMachine.CalledActivate(); - } - } - - public static class CalledDeactivate extends ActivationStateMachine.Event { - static ActivationStateMachine.CalledDeactivate useCustomRegistrar(boolean useCustomRegistrar, SharedPreferences prefs) { - prefs.edit().putBoolean(ActivationStateMachine.PersistKeys.PUSH_CUSTOM_REGISTRAR, useCustomRegistrar).apply(); - return new ActivationStateMachine.CalledDeactivate(); - } - } - - public static class GotPushDeviceDetails extends ActivationStateMachine.Event {} - - public static class GotDeviceRegistration extends ActivationStateMachine.Event { - final String deviceIdentityToken; - GotDeviceRegistration(String token) { this.deviceIdentityToken = token; } - } - - public static class GettingDeviceRegistrationFailed extends ActivationStateMachine.ErrorEvent { - GettingDeviceRegistrationFailed(ErrorInfo reason) { super(reason); } - } - - public static class GettingPushDeviceDetailsFailed extends ActivationStateMachine.ErrorEvent { - GettingPushDeviceDetailsFailed(ErrorInfo reason) { super(reason); } - } - - public static class RegistrationSynced extends ActivationStateMachine.Event {} - - public static class SyncRegistrationFailed extends ActivationStateMachine.ErrorEvent { - public SyncRegistrationFailed(ErrorInfo reason) { super(reason); } - } - - public static class Deregistered extends ActivationStateMachine.Event {} - - public static class DeregistrationFailed extends ActivationStateMachine.ErrorEvent { - public DeregistrationFailed(ErrorInfo reason) { super(reason); } - } - - public abstract static class Event {} - - public abstract static class ErrorEvent extends ActivationStateMachine.Event { - public final ErrorInfo reason; - ErrorEvent(ErrorInfo reason) { this.reason = reason; } - } - - public static class NotActivated extends ActivationStateMachine.PersistentState { - public NotActivated(ActivationStateMachine machine) { super(machine); } - public ActivationStateMachine.State transition(ActivationStateMachine.Event event) { - if (event instanceof ActivationStateMachine.CalledDeactivate) { - machine.callDeactivatedCallback(null); - return this; - } else if (event instanceof ActivationStateMachine.CalledActivate) { - LocalDevice device = machine.getDevice(); - - if (device.isRegistered()) { - machine.validateRegistration(); - return new ActivationStateMachine.WaitingForRegistrationSync(machine, event); - } - - if (device.getRegistrationToken() != null) { - machine.pendingEvents.add(new ActivationStateMachine.GotPushDeviceDetails()); - } else { - machine.getRegistrationToken(); - } - - if(!device.isCreated()) { - device.create(); - } - - return new ActivationStateMachine.WaitingForPushDeviceDetails(machine); - } else if (event instanceof ActivationStateMachine.GotPushDeviceDetails) { - return this; - } - return null; - } - } - - public static class WaitingForPushDeviceDetails extends ActivationStateMachine.PersistentState { - public WaitingForPushDeviceDetails(ActivationStateMachine machine) { super(machine); } - public ActivationStateMachine.State transition(final ActivationStateMachine.Event event) { - if (event instanceof ActivationStateMachine.CalledActivate) { - return this; - } else if (event instanceof ActivationStateMachine.CalledDeactivate) { - machine.callDeactivatedCallback(null); - return new ActivationStateMachine.NotActivated(machine); - } else if (event instanceof ActivationStateMachine.GettingPushDeviceDetailsFailed) { - machine.callDeactivatedCallback(((ActivationStateMachine.GettingPushDeviceDetailsFailed)event).reason); - return new ActivationStateMachine.NotActivated(machine); - } else if (event instanceof ActivationStateMachine.GotPushDeviceDetails) { - final ActivationContext activationContext = machine.activationContext; - final LocalDevice device = activationContext.getLocalDevice(); - - boolean useCustomRegistrar = activationContext.getPreferences().getBoolean(ActivationStateMachine.PersistKeys.PUSH_CUSTOM_REGISTRAR, false); - if (useCustomRegistrar) { - machine.invokeCustomRegistration(device, true); - } else { - final AblyRest ably; - try { - ably = activationContext.getAbly(); - } catch(AblyException ae) { - ErrorInfo reason = ae.errorInfo; - Log.e(TAG, "exception registering " + device.id + ": " + reason.toString()); - machine.handleEvent(new ActivationStateMachine.GettingDeviceRegistrationFailed(reason)); - return new ActivationStateMachine.NotActivated(machine); - } - final HttpCore.RequestBody body = HttpUtils.requestBodyFromGson(device.toJsonObject(), ably.options.useBinaryProtocol); - ably.http.request(new Http.Execute() { - @Override - public void execute(HttpScheduler http, Callback callback) throws AblyException { - Param[] params = null; - if(ably.options.pushFullWait) { - params = Param.push(null, "fullWait", "true"); - } - /* this is authenticated using the Ably library credentials, plus the deviceSecret in the request body */ - http.post("/push/deviceRegistrations", HttpUtils.defaultAcceptHeaders(ably.options.useBinaryProtocol), params, body, new Serialisation.HttpResponseHandler(), true, callback); - } - }).async(new Callback() { - @Override - public void onSuccess(JsonObject response) { - Log.i(TAG, "registered " + device.id); - JsonObject deviceIdentityTokenJson = response.getAsJsonObject("deviceIdentityToken"); - if(deviceIdentityTokenJson == null) { - Log.e(TAG, "invalid device registration response (no deviceIdentityToken); deviceId = " + device.id); - machine.handleEvent(new ActivationStateMachine.GettingDeviceRegistrationFailed(new ErrorInfo("Invalid deviceIdentityToken in response", 40000, 400))); - return; - } - JsonPrimitive responseClientIdJson = response.getAsJsonPrimitive("clientId"); - if(responseClientIdJson != null) { - String responseClientId = responseClientIdJson.getAsString(); - if(device.clientId == null) { - /* Spec RSH8f: there is an implied clientId in our credentials that we didn't know about */ - activationContext.setClientId(responseClientId, false); - } - } - machine.handleEvent(new ActivationStateMachine.GotDeviceRegistration(deviceIdentityTokenJson.getAsJsonPrimitive("token").getAsString())); - } - @Override - public void onError(ErrorInfo reason) { - Log.e(TAG, "error registering " + device.id + ": " + reason.toString()); - machine.handleEvent(new ActivationStateMachine.GettingDeviceRegistrationFailed(reason)); - } - }); - } - - return new ActivationStateMachine.WaitingForDeviceRegistration(machine); - } - return null; - } - } - - public static class WaitingForDeviceRegistration extends ActivationStateMachine.State { - public WaitingForDeviceRegistration(ActivationStateMachine machine) { super(machine); } - public ActivationStateMachine.State transition(ActivationStateMachine.Event event) { - if (event instanceof ActivationStateMachine.CalledActivate) { - return this; - } else if (event instanceof ActivationStateMachine.GotDeviceRegistration) { - LocalDevice device = machine.getDevice(); - device.setDeviceIdentityToken(((ActivationStateMachine.GotDeviceRegistration) event).deviceIdentityToken); - machine.callActivatedCallback(null); - return new ActivationStateMachine.WaitingForNewPushDeviceDetails(machine); - } else if (event instanceof ActivationStateMachine.GettingDeviceRegistrationFailed) { - machine.callActivatedCallback(((ActivationStateMachine.GettingDeviceRegistrationFailed) event).reason); - return new ActivationStateMachine.NotActivated(machine); - } - return null; - } - } - - public static class WaitingForNewPushDeviceDetails extends ActivationStateMachine.PersistentState { - public WaitingForNewPushDeviceDetails(ActivationStateMachine machine) { super(machine); } - public ActivationStateMachine.State transition(ActivationStateMachine.Event event) { - if (event instanceof ActivationStateMachine.CalledActivate) { - machine.callActivatedCallback(null); - return this; - } else if (event instanceof ActivationStateMachine.CalledDeactivate) { - LocalDevice device = machine.getDevice(); - machine.deregister(); - return new ActivationStateMachine.WaitingForDeregistration(machine, this); - } else if (event instanceof ActivationStateMachine.GotPushDeviceDetails) { - machine.getDevice(); - machine.updateRegistration(); - - return new WaitingForRegistrationSync(machine, event); - } - return null; - } - } - - public static class WaitingForRegistrationSync extends ActivationStateMachine.State { - private final Event fromEvent; - - public WaitingForRegistrationSync(ActivationStateMachine machine, Event fromEvent) { - super(machine); - this.fromEvent = fromEvent; - } - - public ActivationStateMachine.State transition(ActivationStateMachine.Event event) { - if (event instanceof ActivationStateMachine.CalledActivate) { - if (fromEvent instanceof CalledActivate) { - // Don't handle; there's a CalledActivate ongoing already, so this one should - // be enqueued for when that one finishes. - return null; - } - machine.callActivatedCallback(null); - return this; - } else if (event instanceof RegistrationSynced) { - if (fromEvent instanceof CalledActivate) { - machine.callActivatedCallback(null); - } - return new ActivationStateMachine.WaitingForNewPushDeviceDetails(machine); - } else if (event instanceof SyncRegistrationFailed) { - // TODO: Here we could try to recover ourselves if the error is e. g. - // a networking error. Just notify the user for now. - ErrorInfo reason = ((SyncRegistrationFailed) event).reason; - if (fromEvent instanceof CalledActivate) { - machine.callActivatedCallback(reason); - } else { - machine.callSyncRegistrationFailedCallback(reason); - } - return new AfterRegistrationSyncFailed(machine); - } - return null; - } - } - - public static class AfterRegistrationSyncFailed extends ActivationStateMachine.PersistentState { - public AfterRegistrationSyncFailed(ActivationStateMachine machine) { super(machine); } - public ActivationStateMachine.State transition(ActivationStateMachine.Event event) { - if (event instanceof ActivationStateMachine.CalledActivate || event instanceof ActivationStateMachine.GotPushDeviceDetails) { - machine.validateRegistration(); - return new WaitingForRegistrationSync(machine, event); - } else if (event instanceof ActivationStateMachine.CalledDeactivate) { - machine.deregister(); - return new ActivationStateMachine.WaitingForDeregistration(machine, this); - } - return null; - } - } - - public static class WaitingForDeregistration extends ActivationStateMachine.State { - private ActivationStateMachine.State previousState; - - public WaitingForDeregistration(ActivationStateMachine machine, ActivationStateMachine.State previousState) { - super(machine); - this.previousState = previousState; - } - - public ActivationStateMachine.State transition(ActivationStateMachine.Event event) { - if (event instanceof ActivationStateMachine.CalledDeactivate) { - return this; - } else if (event instanceof ActivationStateMachine.Deregistered) { - LocalDevice device = machine.getDevice(); - device.reset(); - machine.callDeactivatedCallback(null); - return new ActivationStateMachine.NotActivated(machine); - } else if (event instanceof ActivationStateMachine.DeregistrationFailed) { - machine.callDeactivatedCallback(((ActivationStateMachine.DeregistrationFailed) event).reason); - return previousState; - } - return null; - } - } - - private LocalDevice getDevice() { - return activationContext.getLocalDevice(); - } - - public static abstract class State { - protected final ActivationStateMachine machine; - - public State(ActivationStateMachine machine) { - this.machine = machine; - } - - public abstract ActivationStateMachine.State transition(ActivationStateMachine.Event event); - } - - private static abstract class PersistentState extends ActivationStateMachine.State { - PersistentState(ActivationStateMachine machine) { super(machine); } - } - - private void callActivatedCallback(ErrorInfo reason) { - sendErrorIntent("PUSH_ACTIVATE", reason); - } - - private void callDeactivatedCallback(ErrorInfo reason) { - sendErrorIntent("PUSH_DEACTIVATE", reason); - } - - private void callSyncRegistrationFailedCallback(ErrorInfo reason) { - sendErrorIntent("PUSH_UPDATE_FAILED", reason); - } - - private void sendErrorIntent(String name, ErrorInfo error) { - Intent intent = new Intent(); - IntentUtils.addErrorInfo(intent, error); - sendIntent(name, intent); - } - - private void invokeCustomRegistration(final DeviceDetails device, final boolean isNew) { - registerOnceReceiver("PUSH_DEVICE_REGISTERED", new BroadcastReceiver() { - @Override - public void onReceive(Context context, Intent intent) { - ErrorInfo error = IntentUtils.getErrorInfo(intent); - if (error == null) { - Log.i(TAG, "custom registration for " + device.id); - if (isNew) { - handleEvent(new ActivationStateMachine.GotDeviceRegistration(intent.getStringExtra("deviceIdentityToken"))); - } else { - handleEvent(new RegistrationSynced()); - } - } else { - Log.e(TAG, "error from custom registration for " + device.id + ": " + error.toString()); - if (isNew) { - handleEvent(new ActivationStateMachine.GettingDeviceRegistrationFailed(error)); - } else { - handleEvent(new SyncRegistrationFailed(error)); - } - } - } - }); - - Intent intent = new Intent(); - intent.putExtra("isNew", isNew); - sendIntent("PUSH_REGISTER_DEVICE", intent); - } - - private void invokeCustomDeregistration(final DeviceDetails device) { - registerOnceReceiver("PUSH_DEVICE_DEREGISTERED", new BroadcastReceiver() { - @Override - public void onReceive(Context context, Intent intent) { - ErrorInfo error = IntentUtils.getErrorInfo(intent); - if (error == null) { - Log.i(TAG, "custom deregistration for " + device.id); - handleEvent(new ActivationStateMachine.Deregistered()); - } else { - Log.e(TAG, "error from custom deregisterer for " + device.id + ": " + error.toString()); - handleEvent(new ActivationStateMachine.DeregistrationFailed(error)); - } - } - }); - - Intent intent = new Intent(); - sendIntent("PUSH_DEREGISTER_DEVICE", intent); - } - - private void sendIntent(String name, Intent intent) { - intent.setAction("io.ably.broadcast." + name); - LocalBroadcastManager.getInstance(context).sendBroadcast(intent); - } - - private void registerOnceReceiver(String name, final BroadcastReceiver receiver) { - BroadcastReceiver onceReceiver = new BroadcastReceiver() { - @Override - public void onReceive(Context context, Intent intent) { - LocalBroadcastManager.getInstance(context.getApplicationContext()).unregisterReceiver(this); - receiver.onReceive(context, intent); - } - }; - IntentFilter filter = new IntentFilter("io.ably.broadcast." + name); - LocalBroadcastManager.getInstance(context).registerReceiver(onceReceiver, filter); - } - - protected void getRegistrationToken() { - activationContext.getRegistrationToken(new Callback() { - @Override - public void onSuccess(String token) { - Log.i(TAG, "getInstanceId completed with new token"); - activationContext.onNewRegistrationToken(RegistrationToken.Type.FCM, token); - } - - @Override - public void onError(ErrorInfo error) { - Log.e(TAG, "getInstanceId failed", AblyException.fromErrorInfo(error)); - handleEvent(new ActivationStateMachine.GettingPushDeviceDetailsFailed(error)); - - } - }); - } - - private void updateRegistration() { - final LocalDevice device = activationContext.getLocalDevice(); - boolean useCustomRegistrar = activationContext.getPreferences().getBoolean(ActivationStateMachine.PersistKeys.PUSH_CUSTOM_REGISTRAR, false); - if (useCustomRegistrar) { - invokeCustomRegistration(device, false); - } else { - final AblyRest ably; - try { - ably = activationContext.getAbly(); - } catch(AblyException ae) { - ErrorInfo reason = ae.errorInfo; - Log.e(TAG, "exception registering " + device.id + ": " + reason.toString()); - handleEvent(new SyncRegistrationFailed(reason)); - return; - } - final HttpCore.RequestBody body = HttpUtils.requestBodyFromGson(device.pushRecipientJsonObject(), ably.options.useBinaryProtocol); - ably.http.request(new Http.Execute() { - @Override - public void execute(HttpScheduler http, Callback callback) throws AblyException { - Param[] params = null; - if (ably.options.pushFullWait) { - params = Param.push(params, "fullWait", "true"); - } - - http.patch("/push/deviceRegistrations/" + device.id, ably.push.pushRequestHeaders(true), params, body, null, false, callback); - } - }).async(new Callback() { - @Override - public void onSuccess(Void response) { - Log.i(TAG, "updated registration " + device.id); - handleEvent(new RegistrationSynced()); - } - @Override - public void onError(ErrorInfo reason) { - Log.e(TAG, "error updating registration " + device.id + ": " + reason.toString()); - handleEvent(new SyncRegistrationFailed(reason)); - } - }); - } - } - - private void validateRegistration() { - final LocalDevice device = activationContext.getLocalDevice(); - final AblyRest ably; - try { - ably = activationContext.getAbly(); - } catch(AblyException ae) { - ErrorInfo reason = ae.errorInfo; - Log.e(TAG, "exception validating registration for " + device.id + ": " + reason.toString()); - handleEvent(new SyncRegistrationFailed(reason)); - return; - } - /* Spec: RSH3a2a1, RSH8g: verify that the existing registration is compatible with the present credentials */ - String presentClientId = ably.auth.clientId; - if(presentClientId != null && device.clientId != null && !presentClientId.equals(device.clientId)) { - ErrorInfo clientIdErr = new ErrorInfo("Activation failed: present clientId is not compatible with existing device registration", 400, 61002); - handleEvent(new SyncRegistrationFailed(clientIdErr)); - return; - } - - boolean useCustomRegistrar = activationContext.getPreferences().getBoolean(ActivationStateMachine.PersistKeys.PUSH_CUSTOM_REGISTRAR, false); - if (useCustomRegistrar) { - invokeCustomRegistration(device, false); - } else { - ably.http.request(new Http.Execute() { - @Override - public void execute(HttpScheduler http, Callback callback) throws AblyException { - Param[] params = null; - if (ably.options.pushFullWait) { - params = Param.push(params, "fullWait", "true"); - } - - final HttpCore.RequestBody body = HttpUtils.requestBodyFromGson(device.toJsonObject(), ably.options.useBinaryProtocol); - http.put("/push/deviceRegistrations/" + device.id, ably.push.pushRequestHeaders(true), params, body, new Serialisation.HttpResponseHandler(), true, callback); - } - }).async(new Callback() { - @Override - public void onSuccess(JsonObject response) { - Log.i(TAG, "updated registration " + device.id); - JsonPrimitive responseClientIdJson = response.getAsJsonPrimitive("clientId"); - if(responseClientIdJson != null) { - String responseClientId = responseClientIdJson.getAsString(); - if(device.clientId == null) { - /* Spec RSH8f: there is an implied clientId in our credentials that we didn't know about */ - activationContext.setClientId(responseClientId, false); - } - } - handleEvent(new RegistrationSynced()); - } - @Override - public void onError(ErrorInfo reason) { - Log.e(TAG, "error validating registration " + device.id + ": " + reason.toString()); - handleEvent(new SyncRegistrationFailed(reason)); - } - }); - } - } - - private void deregister() { - final LocalDevice device = activationContext.getLocalDevice(); - if (activationContext.getPreferences().getBoolean(ActivationStateMachine.PersistKeys.PUSH_CUSTOM_REGISTRAR, false)) { - invokeCustomDeregistration(device); - } else { - final AblyRest ably; - try { - ably = activationContext.getAbly(); - } catch(AblyException ae) { - ErrorInfo reason = ae.errorInfo; - Log.e(TAG, "exception registering " + device.id + ": " + reason.toString()); - handleEvent(new ActivationStateMachine.DeregistrationFailed(reason)); - return; - } - ably.http.request(new Http.Execute() { - @Override - public void execute(HttpScheduler http, Callback callback) throws AblyException { - Param[] params = new Param[0]; - if (ably.options.pushFullWait) { - params = Param.push(params, "fullWait", "true"); - } - http.del("/push/deviceRegistrations/" + device.id, ably.push.pushRequestHeaders(true), params, null, true, callback); - } - }).async(new Callback() { - @Override - public void onSuccess(Void response) { - Log.i(TAG, "deregistered " + device.id); - handleEvent(new ActivationStateMachine.Deregistered()); - } - @Override - public void onError(ErrorInfo reason) { - Log.e(TAG, "error deregistering " + device.id + ": " + reason.toString()); - handleEvent(new ActivationStateMachine.DeregistrationFailed(reason)); - } - }); - } - } - - protected final ActivationContext activationContext; - private final Context context; - public ActivationStateMachine.State current; - public ArrayDeque pendingEvents; - protected boolean handlingEvent; - - public ActivationStateMachine(ActivationContext activationContext) { - this.activationContext = activationContext; - this.context = activationContext.getContext(); - loadPersisted(); - handlingEvent = false; - } - - private void loadPersisted() { - current = getPersistedState(); - pendingEvents = getPersistedPendingEvents(); - } - - private void enqueueEvent(ActivationStateMachine.Event event) { - Log.d(TAG, "enqueuing event: " + event.getClass().getSimpleName()); - pendingEvents.add(event); - } - - public synchronized boolean handleEvent(ActivationStateMachine.Event event) { - if (handlingEvent) { - // An event's side effects may end up synchronously calling handleEvent while it's - // itself being handled. In that case, enqueue it so it's handled next (and still - // synchronously). - // - // We don't need to persist here, as the handleEvent call up the stack will eventually - // persist when done with the synchronous transitions. - enqueueEvent(event); - return true; - } - - handlingEvent = true; - try { - Log.d(TAG, String.format("handling event %s from %s", event.getClass().getSimpleName(), current.getClass().getSimpleName())); - - ActivationStateMachine.State maybeNext = current.transition(event); - if (maybeNext == null) { - enqueueEvent(event); - return persist(); - } - - Log.d(TAG, String.format("transition: %s -(%s)-> %s", current.getClass().getSimpleName(), event.getClass().getSimpleName(), maybeNext.getClass().getSimpleName())); - current = maybeNext; - - while (true) { - ActivationStateMachine.Event pending = pendingEvents.peek(); - if (pending == null) { - break; - } - - Log.d(TAG, "attempting to consume pending event: " + pending.getClass().getSimpleName()); - - maybeNext = current.transition(pending); - if (maybeNext == null) { - break; - } - pendingEvents.poll(); - - Log.d(TAG, String.format("transition: %s -(%s)-> %s", current.getClass().getSimpleName(), pending.getClass().getSimpleName(), maybeNext.getClass().getSimpleName())); - current = maybeNext; - } - - return persist(); - } finally { - handlingEvent = false; - } - } - - public boolean reset() { - SharedPreferences.Editor editor = activationContext.getPreferences().edit(); - for (Field f : ActivationStateMachine.PersistKeys.class.getDeclaredFields()) { - try { - editor.remove((String) f.get(null)); - } catch (IllegalAccessException e) { - throw new RuntimeException(e); - } - } - try { - return editor.commit(); - } finally { - loadPersisted(); - } - } - - private boolean persist() { - SharedPreferences.Editor editor = activationContext.getPreferences().edit(); - - if (current instanceof ActivationStateMachine.PersistentState) { - editor.putString(ActivationStateMachine.PersistKeys.CURRENT_STATE, current.getClass().getName()); - } - - editor.putInt(ActivationStateMachine.PersistKeys.PENDING_EVENTS_LENGTH, pendingEvents.size()); - int i = 0; - for (ActivationStateMachine.Event e : pendingEvents) { - editor.putString( - String.format("%s[%d]", ActivationStateMachine.PersistKeys.PENDING_EVENTS_PREFIX, i), - e.getClass().getName() - ); - - i++; - } - - return editor.commit(); - } - - private ActivationStateMachine.State getPersistedState() { - try { - Class stateClass; - - String className = activationContext.getPreferences().getString(ActivationStateMachine.PersistKeys.CURRENT_STATE, ""); - if (className.endsWith("$AfterRegistrationUpdateFailed")) { - stateClass = AfterRegistrationSyncFailed.class; - } else { - stateClass = (Class) Class.forName(className); - } - - Constructor constructor = stateClass.getConstructor(ActivationStateMachine.class); - return constructor.newInstance(this); - } catch (Exception e) { - return new ActivationStateMachine.NotActivated(this); - } - } - - private ArrayDeque getPersistedPendingEvents() { - int length = activationContext.getPreferences().getInt(ActivationStateMachine.PersistKeys.PENDING_EVENTS_LENGTH, 0); - ArrayDeque deque = new ArrayDeque<>(length); - for (int i = 0; i < length; i++) { - try { - String className = activationContext.getPreferences().getString(String.format("%s[%d]", ActivationStateMachine.PersistKeys.PENDING_EVENTS_PREFIX, i), ""); - Class eventClass = (Class) Class.forName(className); - ActivationStateMachine.Event event; - try { - event = eventClass.newInstance(); - } catch(InstantiationException e) { - // We aren't properly persisting events with a non-nullary constructor. Those events - // are supposed to be handled by states that aren't persisted (until - // https://github.com/ably/ably-java/issues/546 is fixed), so it should be safe to - // just drop them. - Log.d(TAG, String.format("discarding improperly persisted event: %s", className)); - continue; - } - deque.add(event); - } catch(Exception e) { - throw new RuntimeException(e); - } - } - return deque; - } - - public static class PersistKeys { - public static final String CURRENT_STATE = "ABLY_PUSH_CURRENT_STATE"; - static final String PENDING_EVENTS_LENGTH = "ABLY_PUSH_PENDING_EVENTS_LENGTH"; - static final String PENDING_EVENTS_PREFIX = "ABLY_PUSH_PENDING_EVENTS"; - static final String PUSH_CUSTOM_REGISTRAR = "ABLY_PUSH_REGISTRATION_HANDLER"; - } - - private static final String TAG = "AblyActivation"; + public static class CalledActivate extends ActivationStateMachine.Event { + public static ActivationStateMachine.CalledActivate useCustomRegistrar(boolean useCustomRegistrar, SharedPreferences prefs) { + prefs.edit().putBoolean(ActivationStateMachine.PersistKeys.PUSH_CUSTOM_REGISTRAR, useCustomRegistrar).apply(); + return new ActivationStateMachine.CalledActivate(); + } + } + + public static class CalledDeactivate extends ActivationStateMachine.Event { + static ActivationStateMachine.CalledDeactivate useCustomRegistrar(boolean useCustomRegistrar, SharedPreferences prefs) { + prefs.edit().putBoolean(ActivationStateMachine.PersistKeys.PUSH_CUSTOM_REGISTRAR, useCustomRegistrar).apply(); + return new ActivationStateMachine.CalledDeactivate(); + } + } + + public static class GotPushDeviceDetails extends ActivationStateMachine.Event {} + + public static class GotDeviceRegistration extends ActivationStateMachine.Event { + final String deviceIdentityToken; + GotDeviceRegistration(String token) { this.deviceIdentityToken = token; } + } + + public static class GettingDeviceRegistrationFailed extends ActivationStateMachine.ErrorEvent { + GettingDeviceRegistrationFailed(ErrorInfo reason) { super(reason); } + } + + public static class GettingPushDeviceDetailsFailed extends ActivationStateMachine.ErrorEvent { + GettingPushDeviceDetailsFailed(ErrorInfo reason) { super(reason); } + } + + public static class RegistrationSynced extends ActivationStateMachine.Event {} + + public static class SyncRegistrationFailed extends ActivationStateMachine.ErrorEvent { + public SyncRegistrationFailed(ErrorInfo reason) { super(reason); } + } + + public static class Deregistered extends ActivationStateMachine.Event {} + + public static class DeregistrationFailed extends ActivationStateMachine.ErrorEvent { + public DeregistrationFailed(ErrorInfo reason) { super(reason); } + } + + public abstract static class Event {} + + public abstract static class ErrorEvent extends ActivationStateMachine.Event { + public final ErrorInfo reason; + ErrorEvent(ErrorInfo reason) { this.reason = reason; } + } + + public static class NotActivated extends ActivationStateMachine.PersistentState { + public NotActivated(ActivationStateMachine machine) { super(machine); } + public ActivationStateMachine.State transition(ActivationStateMachine.Event event) { + if (event instanceof ActivationStateMachine.CalledDeactivate) { + machine.callDeactivatedCallback(null); + return this; + } else if (event instanceof ActivationStateMachine.CalledActivate) { + LocalDevice device = machine.getDevice(); + + if (device.isRegistered()) { + machine.validateRegistration(); + return new ActivationStateMachine.WaitingForRegistrationSync(machine, event); + } + + if (device.getRegistrationToken() != null) { + machine.pendingEvents.add(new ActivationStateMachine.GotPushDeviceDetails()); + } else { + machine.getRegistrationToken(); + } + + if(!device.isCreated()) { + device.create(); + } + + return new ActivationStateMachine.WaitingForPushDeviceDetails(machine); + } else if (event instanceof ActivationStateMachine.GotPushDeviceDetails) { + return this; + } + return null; + } + } + + public static class WaitingForPushDeviceDetails extends ActivationStateMachine.PersistentState { + public WaitingForPushDeviceDetails(ActivationStateMachine machine) { super(machine); } + public ActivationStateMachine.State transition(final ActivationStateMachine.Event event) { + if (event instanceof ActivationStateMachine.CalledActivate) { + return this; + } else if (event instanceof ActivationStateMachine.CalledDeactivate) { + machine.callDeactivatedCallback(null); + return new ActivationStateMachine.NotActivated(machine); + } else if (event instanceof ActivationStateMachine.GettingPushDeviceDetailsFailed) { + machine.callDeactivatedCallback(((ActivationStateMachine.GettingPushDeviceDetailsFailed)event).reason); + return new ActivationStateMachine.NotActivated(machine); + } else if (event instanceof ActivationStateMachine.GotPushDeviceDetails) { + final ActivationContext activationContext = machine.activationContext; + final LocalDevice device = activationContext.getLocalDevice(); + + boolean useCustomRegistrar = activationContext.getPreferences().getBoolean(ActivationStateMachine.PersistKeys.PUSH_CUSTOM_REGISTRAR, false); + if (useCustomRegistrar) { + machine.invokeCustomRegistration(device, true); + } else { + final AblyRest ably; + try { + ably = activationContext.getAbly(); + } catch(AblyException ae) { + ErrorInfo reason = ae.errorInfo; + Log.e(TAG, "exception registering " + device.id + ": " + reason.toString()); + machine.handleEvent(new ActivationStateMachine.GettingDeviceRegistrationFailed(reason)); + return new ActivationStateMachine.NotActivated(machine); + } + final HttpCore.RequestBody body = HttpUtils.requestBodyFromGson(device.toJsonObject(), ably.options.useBinaryProtocol); + ably.http.request(new Http.Execute() { + @Override + public void execute(HttpScheduler http, Callback callback) throws AblyException { + Param[] params = null; + if(ably.options.pushFullWait) { + params = Param.push(null, "fullWait", "true"); + } + /* this is authenticated using the Ably library credentials, plus the deviceSecret in the request body */ + http.post("/push/deviceRegistrations", HttpUtils.defaultAcceptHeaders(ably.options.useBinaryProtocol), params, body, new Serialisation.HttpResponseHandler(), true, callback); + } + }).async(new Callback() { + @Override + public void onSuccess(JsonObject response) { + Log.i(TAG, "registered " + device.id); + JsonObject deviceIdentityTokenJson = response.getAsJsonObject("deviceIdentityToken"); + if(deviceIdentityTokenJson == null) { + Log.e(TAG, "invalid device registration response (no deviceIdentityToken); deviceId = " + device.id); + machine.handleEvent(new ActivationStateMachine.GettingDeviceRegistrationFailed(new ErrorInfo("Invalid deviceIdentityToken in response", 40000, 400))); + return; + } + JsonPrimitive responseClientIdJson = response.getAsJsonPrimitive("clientId"); + if(responseClientIdJson != null) { + String responseClientId = responseClientIdJson.getAsString(); + if(device.clientId == null) { + /* Spec RSH8f: there is an implied clientId in our credentials that we didn't know about */ + activationContext.setClientId(responseClientId, false); + } + } + machine.handleEvent(new ActivationStateMachine.GotDeviceRegistration(deviceIdentityTokenJson.getAsJsonPrimitive("token").getAsString())); + } + @Override + public void onError(ErrorInfo reason) { + Log.e(TAG, "error registering " + device.id + ": " + reason.toString()); + machine.handleEvent(new ActivationStateMachine.GettingDeviceRegistrationFailed(reason)); + } + }); + } + + return new ActivationStateMachine.WaitingForDeviceRegistration(machine); + } + return null; + } + } + + public static class WaitingForDeviceRegistration extends ActivationStateMachine.State { + public WaitingForDeviceRegistration(ActivationStateMachine machine) { super(machine); } + public ActivationStateMachine.State transition(ActivationStateMachine.Event event) { + if (event instanceof ActivationStateMachine.CalledActivate) { + return this; + } else if (event instanceof ActivationStateMachine.GotDeviceRegistration) { + LocalDevice device = machine.getDevice(); + device.setDeviceIdentityToken(((ActivationStateMachine.GotDeviceRegistration) event).deviceIdentityToken); + machine.callActivatedCallback(null); + return new ActivationStateMachine.WaitingForNewPushDeviceDetails(machine); + } else if (event instanceof ActivationStateMachine.GettingDeviceRegistrationFailed) { + machine.callActivatedCallback(((ActivationStateMachine.GettingDeviceRegistrationFailed) event).reason); + return new ActivationStateMachine.NotActivated(machine); + } + return null; + } + } + + public static class WaitingForNewPushDeviceDetails extends ActivationStateMachine.PersistentState { + public WaitingForNewPushDeviceDetails(ActivationStateMachine machine) { super(machine); } + public ActivationStateMachine.State transition(ActivationStateMachine.Event event) { + if (event instanceof ActivationStateMachine.CalledActivate) { + machine.callActivatedCallback(null); + return this; + } else if (event instanceof ActivationStateMachine.CalledDeactivate) { + LocalDevice device = machine.getDevice(); + machine.deregister(); + return new ActivationStateMachine.WaitingForDeregistration(machine, this); + } else if (event instanceof ActivationStateMachine.GotPushDeviceDetails) { + machine.getDevice(); + machine.updateRegistration(); + + return new WaitingForRegistrationSync(machine, event); + } + return null; + } + } + + public static class WaitingForRegistrationSync extends ActivationStateMachine.State { + private final Event fromEvent; + + public WaitingForRegistrationSync(ActivationStateMachine machine, Event fromEvent) { + super(machine); + this.fromEvent = fromEvent; + } + + public ActivationStateMachine.State transition(ActivationStateMachine.Event event) { + if (event instanceof ActivationStateMachine.CalledActivate) { + if (fromEvent instanceof CalledActivate) { + // Don't handle; there's a CalledActivate ongoing already, so this one should + // be enqueued for when that one finishes. + return null; + } + machine.callActivatedCallback(null); + return this; + } else if (event instanceof RegistrationSynced) { + if (fromEvent instanceof CalledActivate) { + machine.callActivatedCallback(null); + } + return new ActivationStateMachine.WaitingForNewPushDeviceDetails(machine); + } else if (event instanceof SyncRegistrationFailed) { + // TODO: Here we could try to recover ourselves if the error is e. g. + // a networking error. Just notify the user for now. + ErrorInfo reason = ((SyncRegistrationFailed) event).reason; + if (fromEvent instanceof CalledActivate) { + machine.callActivatedCallback(reason); + } else { + machine.callSyncRegistrationFailedCallback(reason); + } + return new AfterRegistrationSyncFailed(machine); + } + return null; + } + } + + public static class AfterRegistrationSyncFailed extends ActivationStateMachine.PersistentState { + public AfterRegistrationSyncFailed(ActivationStateMachine machine) { super(machine); } + public ActivationStateMachine.State transition(ActivationStateMachine.Event event) { + if (event instanceof ActivationStateMachine.CalledActivate || event instanceof ActivationStateMachine.GotPushDeviceDetails) { + machine.validateRegistration(); + return new WaitingForRegistrationSync(machine, event); + } else if (event instanceof ActivationStateMachine.CalledDeactivate) { + machine.deregister(); + return new ActivationStateMachine.WaitingForDeregistration(machine, this); + } + return null; + } + } + + public static class WaitingForDeregistration extends ActivationStateMachine.State { + private ActivationStateMachine.State previousState; + + public WaitingForDeregistration(ActivationStateMachine machine, ActivationStateMachine.State previousState) { + super(machine); + this.previousState = previousState; + } + + public ActivationStateMachine.State transition(ActivationStateMachine.Event event) { + if (event instanceof ActivationStateMachine.CalledDeactivate) { + return this; + } else if (event instanceof ActivationStateMachine.Deregistered) { + LocalDevice device = machine.getDevice(); + device.reset(); + machine.callDeactivatedCallback(null); + return new ActivationStateMachine.NotActivated(machine); + } else if (event instanceof ActivationStateMachine.DeregistrationFailed) { + machine.callDeactivatedCallback(((ActivationStateMachine.DeregistrationFailed) event).reason); + return previousState; + } + return null; + } + } + + private LocalDevice getDevice() { + return activationContext.getLocalDevice(); + } + + public static abstract class State { + protected final ActivationStateMachine machine; + + public State(ActivationStateMachine machine) { + this.machine = machine; + } + + public abstract ActivationStateMachine.State transition(ActivationStateMachine.Event event); + } + + private static abstract class PersistentState extends ActivationStateMachine.State { + PersistentState(ActivationStateMachine machine) { super(machine); } + } + + private void callActivatedCallback(ErrorInfo reason) { + sendErrorIntent("PUSH_ACTIVATE", reason); + } + + private void callDeactivatedCallback(ErrorInfo reason) { + sendErrorIntent("PUSH_DEACTIVATE", reason); + } + + private void callSyncRegistrationFailedCallback(ErrorInfo reason) { + sendErrorIntent("PUSH_UPDATE_FAILED", reason); + } + + private void sendErrorIntent(String name, ErrorInfo error) { + Intent intent = new Intent(); + IntentUtils.addErrorInfo(intent, error); + sendIntent(name, intent); + } + + private void invokeCustomRegistration(final DeviceDetails device, final boolean isNew) { + registerOnceReceiver("PUSH_DEVICE_REGISTERED", new BroadcastReceiver() { + @Override + public void onReceive(Context context, Intent intent) { + ErrorInfo error = IntentUtils.getErrorInfo(intent); + if (error == null) { + Log.i(TAG, "custom registration for " + device.id); + if (isNew) { + handleEvent(new ActivationStateMachine.GotDeviceRegistration(intent.getStringExtra("deviceIdentityToken"))); + } else { + handleEvent(new RegistrationSynced()); + } + } else { + Log.e(TAG, "error from custom registration for " + device.id + ": " + error.toString()); + if (isNew) { + handleEvent(new ActivationStateMachine.GettingDeviceRegistrationFailed(error)); + } else { + handleEvent(new SyncRegistrationFailed(error)); + } + } + } + }); + + Intent intent = new Intent(); + intent.putExtra("isNew", isNew); + sendIntent("PUSH_REGISTER_DEVICE", intent); + } + + private void invokeCustomDeregistration(final DeviceDetails device) { + registerOnceReceiver("PUSH_DEVICE_DEREGISTERED", new BroadcastReceiver() { + @Override + public void onReceive(Context context, Intent intent) { + ErrorInfo error = IntentUtils.getErrorInfo(intent); + if (error == null) { + Log.i(TAG, "custom deregistration for " + device.id); + handleEvent(new ActivationStateMachine.Deregistered()); + } else { + Log.e(TAG, "error from custom deregisterer for " + device.id + ": " + error.toString()); + handleEvent(new ActivationStateMachine.DeregistrationFailed(error)); + } + } + }); + + Intent intent = new Intent(); + sendIntent("PUSH_DEREGISTER_DEVICE", intent); + } + + private void sendIntent(String name, Intent intent) { + intent.setAction("io.ably.broadcast." + name); + LocalBroadcastManager.getInstance(context).sendBroadcast(intent); + } + + private void registerOnceReceiver(String name, final BroadcastReceiver receiver) { + BroadcastReceiver onceReceiver = new BroadcastReceiver() { + @Override + public void onReceive(Context context, Intent intent) { + LocalBroadcastManager.getInstance(context.getApplicationContext()).unregisterReceiver(this); + receiver.onReceive(context, intent); + } + }; + IntentFilter filter = new IntentFilter("io.ably.broadcast." + name); + LocalBroadcastManager.getInstance(context).registerReceiver(onceReceiver, filter); + } + + protected void getRegistrationToken() { + activationContext.getRegistrationToken(new Callback() { + @Override + public void onSuccess(String token) { + Log.i(TAG, "getInstanceId completed with new token"); + activationContext.onNewRegistrationToken(RegistrationToken.Type.FCM, token); + } + + @Override + public void onError(ErrorInfo error) { + Log.e(TAG, "getInstanceId failed", AblyException.fromErrorInfo(error)); + handleEvent(new ActivationStateMachine.GettingPushDeviceDetailsFailed(error)); + + } + }); + } + + private void updateRegistration() { + final LocalDevice device = activationContext.getLocalDevice(); + boolean useCustomRegistrar = activationContext.getPreferences().getBoolean(ActivationStateMachine.PersistKeys.PUSH_CUSTOM_REGISTRAR, false); + if (useCustomRegistrar) { + invokeCustomRegistration(device, false); + } else { + final AblyRest ably; + try { + ably = activationContext.getAbly(); + } catch(AblyException ae) { + ErrorInfo reason = ae.errorInfo; + Log.e(TAG, "exception registering " + device.id + ": " + reason.toString()); + handleEvent(new SyncRegistrationFailed(reason)); + return; + } + final HttpCore.RequestBody body = HttpUtils.requestBodyFromGson(device.pushRecipientJsonObject(), ably.options.useBinaryProtocol); + ably.http.request(new Http.Execute() { + @Override + public void execute(HttpScheduler http, Callback callback) throws AblyException { + Param[] params = null; + if (ably.options.pushFullWait) { + params = Param.push(params, "fullWait", "true"); + } + + http.patch("/push/deviceRegistrations/" + device.id, ably.push.pushRequestHeaders(true), params, body, null, false, callback); + } + }).async(new Callback() { + @Override + public void onSuccess(Void response) { + Log.i(TAG, "updated registration " + device.id); + handleEvent(new RegistrationSynced()); + } + @Override + public void onError(ErrorInfo reason) { + Log.e(TAG, "error updating registration " + device.id + ": " + reason.toString()); + handleEvent(new SyncRegistrationFailed(reason)); + } + }); + } + } + + private void validateRegistration() { + final LocalDevice device = activationContext.getLocalDevice(); + final AblyRest ably; + try { + ably = activationContext.getAbly(); + } catch(AblyException ae) { + ErrorInfo reason = ae.errorInfo; + Log.e(TAG, "exception validating registration for " + device.id + ": " + reason.toString()); + handleEvent(new SyncRegistrationFailed(reason)); + return; + } + /* Spec: RSH3a2a1, RSH8g: verify that the existing registration is compatible with the present credentials */ + String presentClientId = ably.auth.clientId; + if(presentClientId != null && device.clientId != null && !presentClientId.equals(device.clientId)) { + ErrorInfo clientIdErr = new ErrorInfo("Activation failed: present clientId is not compatible with existing device registration", 400, 61002); + handleEvent(new SyncRegistrationFailed(clientIdErr)); + return; + } + + boolean useCustomRegistrar = activationContext.getPreferences().getBoolean(ActivationStateMachine.PersistKeys.PUSH_CUSTOM_REGISTRAR, false); + if (useCustomRegistrar) { + invokeCustomRegistration(device, false); + } else { + ably.http.request(new Http.Execute() { + @Override + public void execute(HttpScheduler http, Callback callback) throws AblyException { + Param[] params = null; + if (ably.options.pushFullWait) { + params = Param.push(params, "fullWait", "true"); + } + + final HttpCore.RequestBody body = HttpUtils.requestBodyFromGson(device.toJsonObject(), ably.options.useBinaryProtocol); + http.put("/push/deviceRegistrations/" + device.id, ably.push.pushRequestHeaders(true), params, body, new Serialisation.HttpResponseHandler(), true, callback); + } + }).async(new Callback() { + @Override + public void onSuccess(JsonObject response) { + Log.i(TAG, "updated registration " + device.id); + JsonPrimitive responseClientIdJson = response.getAsJsonPrimitive("clientId"); + if(responseClientIdJson != null) { + String responseClientId = responseClientIdJson.getAsString(); + if(device.clientId == null) { + /* Spec RSH8f: there is an implied clientId in our credentials that we didn't know about */ + activationContext.setClientId(responseClientId, false); + } + } + handleEvent(new RegistrationSynced()); + } + @Override + public void onError(ErrorInfo reason) { + Log.e(TAG, "error validating registration " + device.id + ": " + reason.toString()); + handleEvent(new SyncRegistrationFailed(reason)); + } + }); + } + } + + private void deregister() { + final LocalDevice device = activationContext.getLocalDevice(); + if (activationContext.getPreferences().getBoolean(ActivationStateMachine.PersistKeys.PUSH_CUSTOM_REGISTRAR, false)) { + invokeCustomDeregistration(device); + } else { + final AblyRest ably; + try { + ably = activationContext.getAbly(); + } catch(AblyException ae) { + ErrorInfo reason = ae.errorInfo; + Log.e(TAG, "exception registering " + device.id + ": " + reason.toString()); + handleEvent(new ActivationStateMachine.DeregistrationFailed(reason)); + return; + } + ably.http.request(new Http.Execute() { + @Override + public void execute(HttpScheduler http, Callback callback) throws AblyException { + Param[] params = new Param[0]; + if (ably.options.pushFullWait) { + params = Param.push(params, "fullWait", "true"); + } + http.del("/push/deviceRegistrations/" + device.id, ably.push.pushRequestHeaders(true), params, null, true, callback); + } + }).async(new Callback() { + @Override + public void onSuccess(Void response) { + Log.i(TAG, "deregistered " + device.id); + handleEvent(new ActivationStateMachine.Deregistered()); + } + @Override + public void onError(ErrorInfo reason) { + Log.e(TAG, "error deregistering " + device.id + ": " + reason.toString()); + handleEvent(new ActivationStateMachine.DeregistrationFailed(reason)); + } + }); + } + } + + protected final ActivationContext activationContext; + private final Context context; + public ActivationStateMachine.State current; + public ArrayDeque pendingEvents; + protected boolean handlingEvent; + + public ActivationStateMachine(ActivationContext activationContext) { + this.activationContext = activationContext; + this.context = activationContext.getContext(); + loadPersisted(); + handlingEvent = false; + } + + private void loadPersisted() { + current = getPersistedState(); + pendingEvents = getPersistedPendingEvents(); + } + + private void enqueueEvent(ActivationStateMachine.Event event) { + Log.d(TAG, "enqueuing event: " + event.getClass().getSimpleName()); + pendingEvents.add(event); + } + + public synchronized boolean handleEvent(ActivationStateMachine.Event event) { + if (handlingEvent) { + // An event's side effects may end up synchronously calling handleEvent while it's + // itself being handled. In that case, enqueue it so it's handled next (and still + // synchronously). + // + // We don't need to persist here, as the handleEvent call up the stack will eventually + // persist when done with the synchronous transitions. + enqueueEvent(event); + return true; + } + + handlingEvent = true; + try { + Log.d(TAG, String.format("handling event %s from %s", event.getClass().getSimpleName(), current.getClass().getSimpleName())); + + ActivationStateMachine.State maybeNext = current.transition(event); + if (maybeNext == null) { + enqueueEvent(event); + return persist(); + } + + Log.d(TAG, String.format("transition: %s -(%s)-> %s", current.getClass().getSimpleName(), event.getClass().getSimpleName(), maybeNext.getClass().getSimpleName())); + current = maybeNext; + + while (true) { + ActivationStateMachine.Event pending = pendingEvents.peek(); + if (pending == null) { + break; + } + + Log.d(TAG, "attempting to consume pending event: " + pending.getClass().getSimpleName()); + + maybeNext = current.transition(pending); + if (maybeNext == null) { + break; + } + pendingEvents.poll(); + + Log.d(TAG, String.format("transition: %s -(%s)-> %s", current.getClass().getSimpleName(), pending.getClass().getSimpleName(), maybeNext.getClass().getSimpleName())); + current = maybeNext; + } + + return persist(); + } finally { + handlingEvent = false; + } + } + + public boolean reset() { + SharedPreferences.Editor editor = activationContext.getPreferences().edit(); + for (Field f : ActivationStateMachine.PersistKeys.class.getDeclaredFields()) { + try { + editor.remove((String) f.get(null)); + } catch (IllegalAccessException e) { + throw new RuntimeException(e); + } + } + try { + return editor.commit(); + } finally { + loadPersisted(); + } + } + + private boolean persist() { + SharedPreferences.Editor editor = activationContext.getPreferences().edit(); + + if (current instanceof ActivationStateMachine.PersistentState) { + editor.putString(ActivationStateMachine.PersistKeys.CURRENT_STATE, current.getClass().getName()); + } + + editor.putInt(ActivationStateMachine.PersistKeys.PENDING_EVENTS_LENGTH, pendingEvents.size()); + int i = 0; + for (ActivationStateMachine.Event e : pendingEvents) { + editor.putString( + String.format("%s[%d]", ActivationStateMachine.PersistKeys.PENDING_EVENTS_PREFIX, i), + e.getClass().getName() + ); + + i++; + } + + return editor.commit(); + } + + private ActivationStateMachine.State getPersistedState() { + try { + Class stateClass; + + String className = activationContext.getPreferences().getString(ActivationStateMachine.PersistKeys.CURRENT_STATE, ""); + if (className.endsWith("$AfterRegistrationUpdateFailed")) { + stateClass = AfterRegistrationSyncFailed.class; + } else { + stateClass = (Class) Class.forName(className); + } + + Constructor constructor = stateClass.getConstructor(ActivationStateMachine.class); + return constructor.newInstance(this); + } catch (Exception e) { + return new ActivationStateMachine.NotActivated(this); + } + } + + private ArrayDeque getPersistedPendingEvents() { + int length = activationContext.getPreferences().getInt(ActivationStateMachine.PersistKeys.PENDING_EVENTS_LENGTH, 0); + ArrayDeque deque = new ArrayDeque<>(length); + for (int i = 0; i < length; i++) { + try { + String className = activationContext.getPreferences().getString(String.format("%s[%d]", ActivationStateMachine.PersistKeys.PENDING_EVENTS_PREFIX, i), ""); + Class eventClass = (Class) Class.forName(className); + ActivationStateMachine.Event event; + try { + event = eventClass.newInstance(); + } catch(InstantiationException e) { + // We aren't properly persisting events with a non-nullary constructor. Those events + // are supposed to be handled by states that aren't persisted (until + // https://github.com/ably/ably-java/issues/546 is fixed), so it should be safe to + // just drop them. + Log.d(TAG, String.format("discarding improperly persisted event: %s", className)); + continue; + } + deque.add(event); + } catch(Exception e) { + throw new RuntimeException(e); + } + } + return deque; + } + + public static class PersistKeys { + public static final String CURRENT_STATE = "ABLY_PUSH_CURRENT_STATE"; + static final String PENDING_EVENTS_LENGTH = "ABLY_PUSH_PENDING_EVENTS_LENGTH"; + static final String PENDING_EVENTS_PREFIX = "ABLY_PUSH_PENDING_EVENTS"; + static final String PUSH_CUSTOM_REGISTRAR = "ABLY_PUSH_REGISTRATION_HANDLER"; + } + + private static final String TAG = "AblyActivation"; } diff --git a/android/src/main/java/io/ably/lib/push/LocalDevice.java b/android/src/main/java/io/ably/lib/push/LocalDevice.java index cb629c6a9..5c2b5b2fc 100644 --- a/android/src/main/java/io/ably/lib/push/LocalDevice.java +++ b/android/src/main/java/io/ably/lib/push/LocalDevice.java @@ -21,172 +21,172 @@ import io.azam.ulidj.ULID; public class LocalDevice extends DeviceDetails { - public String deviceSecret; - public String deviceIdentityToken; - - private final ActivationContext activationContext; - - public LocalDevice(ActivationContext activationContext) { - super(); - Log.v(TAG, "LocalDevice(): initialising"); - this.platform = "android"; - this.formFactor = isTablet(activationContext.getContext()) ? "tablet" : "phone"; - this.activationContext = activationContext; - this.push = new DeviceDetails.Push(); - loadPersisted(); - } - - public JsonObject toJsonObject() { - JsonObject o = super.toJsonObject(); - if (deviceSecret != null) { - o.addProperty("deviceSecret", deviceSecret); - } - - return o; - } - - private void loadPersisted() { - /* Spec: RSH8a */ - SharedPreferences prefs = PreferenceManager.getDefaultSharedPreferences(activationContext.getContext()); - - String id = prefs.getString(SharedPrefKeys.DEVICE_ID, null); - this.id = id; - if(id != null) { - Log.v(TAG, "loadPersisted(): existing deviceId found; id: " + id); - deviceSecret = prefs.getString(SharedPrefKeys.DEVICE_SECRET, null); - } - this.clientId = prefs.getString(SharedPrefKeys.CLIENT_ID, null); - this.deviceIdentityToken = prefs.getString(SharedPrefKeys.DEVICE_TOKEN, null); - - RegistrationToken.Type type = RegistrationToken.Type.fromOrdinal( - prefs.getInt(SharedPrefKeys.TOKEN_TYPE, -1)); - - Log.d(TAG, "loadPersisted(): token type = " + type); - if(type != null) { - RegistrationToken token = null; - String tokenString = prefs.getString(SharedPrefKeys.TOKEN, null); - if(tokenString != null) { - Log.d(TAG, "loadPersisted(): token string = " + tokenString); - token = new RegistrationToken(type, tokenString); - setRegistrationToken(token); - } - } - } - - RegistrationToken getRegistrationToken() { - JsonObject recipient = push.recipient; - if(recipient == null) { - return null; - } - return new RegistrationToken( - RegistrationToken.Type.fromName(recipient.get("transportType").getAsString()), - recipient.get("registrationToken").getAsString() - ); - } - - private void setRegistrationToken(RegistrationToken token) { - push.recipient = new JsonObject(); - push.recipient.addProperty("transportType", token.type.toName()); - push.recipient.addProperty("registrationToken", token.token); - } - - private void clearRegistrationToken() { - push.recipient = null; - } - - void setAndPersistRegistrationToken(RegistrationToken token) { - Log.v(TAG, "setAndPersistRegistrationToken(): token: " + token.token); - setRegistrationToken(token); - SharedPreferences prefs = PreferenceManager.getDefaultSharedPreferences(activationContext.getContext()); - prefs.edit() - .putInt(SharedPrefKeys.TOKEN_TYPE, token.type.ordinal()) - .putString(SharedPrefKeys.TOKEN, token.token) - .apply(); - } - - void setClientId(String clientId) { - this.clientId = clientId; - SharedPreferences prefs = PreferenceManager.getDefaultSharedPreferences(activationContext.getContext()); - prefs.edit().putString(SharedPrefKeys.CLIENT_ID, clientId).apply(); - } - - public void setDeviceIdentityToken(String token) { - this.deviceIdentityToken = token; - SharedPreferences prefs = PreferenceManager.getDefaultSharedPreferences(activationContext.getContext()); - prefs.edit().putString(SharedPrefKeys.DEVICE_TOKEN, token).apply(); - } - - boolean isCreated() { - return id != null; - } - - boolean create() { - /* Spec: RSH8b */ - Log.v(TAG, "create()"); - SharedPreferences prefs = PreferenceManager.getDefaultSharedPreferences(activationContext.getContext()); - SharedPreferences.Editor editor = prefs.edit(); - - editor.putString(SharedPrefKeys.DEVICE_ID, (id = ULID.random())); - editor.putString(SharedPrefKeys.CLIENT_ID, (clientId = activationContext.clientId)); - editor.putString(SharedPrefKeys.DEVICE_SECRET, (deviceSecret = generateSecret())); - - return editor.commit(); - } - - public void reset() { - Log.v(TAG, "reset()"); - this.id = null; - this.deviceSecret = null; - this.deviceIdentityToken = null; - this.clientId = null; - this.clearRegistrationToken(); - - SharedPreferences.Editor editor = activationContext.getPreferences().edit(); - for (Field f : SharedPrefKeys.class.getDeclaredFields()) { - try { - editor.remove((String) f.get(null)); - } catch (IllegalAccessException e) { - throw new RuntimeException(e); - } - } - editor.commit(); - } - - boolean isRegistered() { - return (deviceIdentityToken != null); - } - - Param[] deviceIdentityHeaders() { - return deviceIdentityToken != null ? new Param[]{new Param(DEVICE_IDENTITY_HEADER, Base64Coder.encodeString(deviceIdentityToken))} : null; - } - - private static final String DEVICE_IDENTITY_HEADER = "X-Ably-DeviceToken"; - - private static boolean isTablet(Context context) { - return (context.getResources().getConfiguration().screenLayout - & Configuration.SCREENLAYOUT_SIZE_MASK) - >= Configuration.SCREENLAYOUT_SIZE_LARGE; - } - - private static class SharedPrefKeys { - static final String DEVICE_ID = "ABLY_DEVICE_ID"; - static final String CLIENT_ID = "ABLY_CLIENT_ID"; - static final String DEVICE_SECRET = "ABLY_DEVICE_SECRET"; - static final String DEVICE_TOKEN = "ABLY_DEVICE_IDENTITY_TOKEN"; - static final String TOKEN_TYPE = "ABLY_REGISTRATION_TOKEN_TYPE"; - static final String TOKEN = "ABLY_REGISTRATION_TOKEN"; - } - - private static String generateSecret() { - byte[] entropy = new byte[64]; - (new SecureRandom()).nextBytes(entropy); - MessageDigest digest = null; - try { - digest = MessageDigest.getInstance("SHA-256"); - } catch (NoSuchAlgorithmException e) {} - byte[] encodedhash = digest.digest(entropy); - return Base64Coder.encodeToString(encodedhash); - } - - private static final String TAG = LocalDevice.class.getName(); + public String deviceSecret; + public String deviceIdentityToken; + + private final ActivationContext activationContext; + + public LocalDevice(ActivationContext activationContext) { + super(); + Log.v(TAG, "LocalDevice(): initialising"); + this.platform = "android"; + this.formFactor = isTablet(activationContext.getContext()) ? "tablet" : "phone"; + this.activationContext = activationContext; + this.push = new DeviceDetails.Push(); + loadPersisted(); + } + + public JsonObject toJsonObject() { + JsonObject o = super.toJsonObject(); + if (deviceSecret != null) { + o.addProperty("deviceSecret", deviceSecret); + } + + return o; + } + + private void loadPersisted() { + /* Spec: RSH8a */ + SharedPreferences prefs = PreferenceManager.getDefaultSharedPreferences(activationContext.getContext()); + + String id = prefs.getString(SharedPrefKeys.DEVICE_ID, null); + this.id = id; + if(id != null) { + Log.v(TAG, "loadPersisted(): existing deviceId found; id: " + id); + deviceSecret = prefs.getString(SharedPrefKeys.DEVICE_SECRET, null); + } + this.clientId = prefs.getString(SharedPrefKeys.CLIENT_ID, null); + this.deviceIdentityToken = prefs.getString(SharedPrefKeys.DEVICE_TOKEN, null); + + RegistrationToken.Type type = RegistrationToken.Type.fromOrdinal( + prefs.getInt(SharedPrefKeys.TOKEN_TYPE, -1)); + + Log.d(TAG, "loadPersisted(): token type = " + type); + if(type != null) { + RegistrationToken token = null; + String tokenString = prefs.getString(SharedPrefKeys.TOKEN, null); + if(tokenString != null) { + Log.d(TAG, "loadPersisted(): token string = " + tokenString); + token = new RegistrationToken(type, tokenString); + setRegistrationToken(token); + } + } + } + + RegistrationToken getRegistrationToken() { + JsonObject recipient = push.recipient; + if(recipient == null) { + return null; + } + return new RegistrationToken( + RegistrationToken.Type.fromName(recipient.get("transportType").getAsString()), + recipient.get("registrationToken").getAsString() + ); + } + + private void setRegistrationToken(RegistrationToken token) { + push.recipient = new JsonObject(); + push.recipient.addProperty("transportType", token.type.toName()); + push.recipient.addProperty("registrationToken", token.token); + } + + private void clearRegistrationToken() { + push.recipient = null; + } + + void setAndPersistRegistrationToken(RegistrationToken token) { + Log.v(TAG, "setAndPersistRegistrationToken(): token: " + token.token); + setRegistrationToken(token); + SharedPreferences prefs = PreferenceManager.getDefaultSharedPreferences(activationContext.getContext()); + prefs.edit() + .putInt(SharedPrefKeys.TOKEN_TYPE, token.type.ordinal()) + .putString(SharedPrefKeys.TOKEN, token.token) + .apply(); + } + + void setClientId(String clientId) { + this.clientId = clientId; + SharedPreferences prefs = PreferenceManager.getDefaultSharedPreferences(activationContext.getContext()); + prefs.edit().putString(SharedPrefKeys.CLIENT_ID, clientId).apply(); + } + + public void setDeviceIdentityToken(String token) { + this.deviceIdentityToken = token; + SharedPreferences prefs = PreferenceManager.getDefaultSharedPreferences(activationContext.getContext()); + prefs.edit().putString(SharedPrefKeys.DEVICE_TOKEN, token).apply(); + } + + boolean isCreated() { + return id != null; + } + + boolean create() { + /* Spec: RSH8b */ + Log.v(TAG, "create()"); + SharedPreferences prefs = PreferenceManager.getDefaultSharedPreferences(activationContext.getContext()); + SharedPreferences.Editor editor = prefs.edit(); + + editor.putString(SharedPrefKeys.DEVICE_ID, (id = ULID.random())); + editor.putString(SharedPrefKeys.CLIENT_ID, (clientId = activationContext.clientId)); + editor.putString(SharedPrefKeys.DEVICE_SECRET, (deviceSecret = generateSecret())); + + return editor.commit(); + } + + public void reset() { + Log.v(TAG, "reset()"); + this.id = null; + this.deviceSecret = null; + this.deviceIdentityToken = null; + this.clientId = null; + this.clearRegistrationToken(); + + SharedPreferences.Editor editor = activationContext.getPreferences().edit(); + for (Field f : SharedPrefKeys.class.getDeclaredFields()) { + try { + editor.remove((String) f.get(null)); + } catch (IllegalAccessException e) { + throw new RuntimeException(e); + } + } + editor.commit(); + } + + boolean isRegistered() { + return (deviceIdentityToken != null); + } + + Param[] deviceIdentityHeaders() { + return deviceIdentityToken != null ? new Param[]{new Param(DEVICE_IDENTITY_HEADER, Base64Coder.encodeString(deviceIdentityToken))} : null; + } + + private static final String DEVICE_IDENTITY_HEADER = "X-Ably-DeviceToken"; + + private static boolean isTablet(Context context) { + return (context.getResources().getConfiguration().screenLayout + & Configuration.SCREENLAYOUT_SIZE_MASK) + >= Configuration.SCREENLAYOUT_SIZE_LARGE; + } + + private static class SharedPrefKeys { + static final String DEVICE_ID = "ABLY_DEVICE_ID"; + static final String CLIENT_ID = "ABLY_CLIENT_ID"; + static final String DEVICE_SECRET = "ABLY_DEVICE_SECRET"; + static final String DEVICE_TOKEN = "ABLY_DEVICE_IDENTITY_TOKEN"; + static final String TOKEN_TYPE = "ABLY_REGISTRATION_TOKEN_TYPE"; + static final String TOKEN = "ABLY_REGISTRATION_TOKEN"; + } + + private static String generateSecret() { + byte[] entropy = new byte[64]; + (new SecureRandom()).nextBytes(entropy); + MessageDigest digest = null; + try { + digest = MessageDigest.getInstance("SHA-256"); + } catch (NoSuchAlgorithmException e) {} + byte[] encodedhash = digest.digest(entropy); + return Base64Coder.encodeToString(encodedhash); + } + + private static final String TAG = LocalDevice.class.getName(); } diff --git a/android/src/main/java/io/ably/lib/push/Push.java b/android/src/main/java/io/ably/lib/push/Push.java index 44e19812c..5e00160a0 100644 --- a/android/src/main/java/io/ably/lib/push/Push.java +++ b/android/src/main/java/io/ably/lib/push/Push.java @@ -13,90 +13,90 @@ public class Push extends PushBase { - public Push(AblyBase rest) { - super(rest); - } - - public void activate() throws AblyException { - activate(false); - } - - public void activate(boolean useCustomRegistrar) throws AblyException { - Log.v(TAG, "activate(): useCustomRegistrar " + useCustomRegistrar); - Context context = getApplicationContext(); - SharedPreferences prefs = PreferenceManager.getDefaultSharedPreferences(context); - getStateMachine().handleEvent(ActivationStateMachine.CalledActivate.useCustomRegistrar(useCustomRegistrar, prefs)); - } - - public void deactivate() throws AblyException { - deactivate(false); - } - - public void deactivate(boolean useCustomRegistrar) throws AblyException { - Log.v(TAG, "deactivate(): useCustomRegistrar " + useCustomRegistrar); - Context context = getApplicationContext(); - SharedPreferences prefs = PreferenceManager.getDefaultSharedPreferences(context); - getStateMachine().handleEvent(ActivationStateMachine.CalledDeactivate.useCustomRegistrar(useCustomRegistrar, prefs)); - } - - synchronized ActivationStateMachine getStateMachine() throws AblyException { - return getActivationContext().getActivationStateMachine(); - } - - public void tryRequestRegistrationToken() { - try { - if (getLocalDevice().isRegistered()) { - getStateMachine().getRegistrationToken(); - } - } catch (AblyException e) { - Log.e(TAG, "couldn't validate existing push recipient device details", e); - } - } - - Context getApplicationContext() throws AblyException { - Context applicationContext = rest.platform.getApplicationContext(); - if(applicationContext == null) { - Log.e(TAG, "getApplicationContext(): Unable to get application context; not set"); - throw AblyException.fromErrorInfo(new ErrorInfo("Unable to get application context; not set", 40000, 400)); - } - return applicationContext; - } - - protected ActivationContext activationContext = null; - - public ActivationContext getActivationContext() throws AblyException { - if (activationContext == null) { - Context applicationContext = getApplicationContext(); - activationContext = ActivationContext.getActivationContext(applicationContext, (AblyRest)rest); - } - return activationContext; - } - - public LocalDevice getLocalDevice() throws AblyException { - return getActivationContext().getLocalDevice(); - } - - @Override - Param[] pushRequestHeaders(boolean forLocalDevice) { - Param[] headers = super.pushRequestHeaders(forLocalDevice); - if(forLocalDevice) { - try { - Param[] deviceIdentityHeaders = getLocalDevice().deviceIdentityHeaders(); - if(deviceIdentityHeaders != null) { - headers = HttpUtils.mergeHeaders(headers, deviceIdentityHeaders); - } - } catch (AblyException e) {} - } - return headers; - } - - Param[] pushRequestHeaders(String deviceId) { - boolean forLocalDevice = false; - try { - forLocalDevice = deviceId != null && deviceId.equals(getLocalDevice().id); - } catch (AblyException e) {} - return pushRequestHeaders(forLocalDevice); - } - - private static final String TAG = Push.class.getName(); + public Push(AblyBase rest) { + super(rest); + } + + public void activate() throws AblyException { + activate(false); + } + + public void activate(boolean useCustomRegistrar) throws AblyException { + Log.v(TAG, "activate(): useCustomRegistrar " + useCustomRegistrar); + Context context = getApplicationContext(); + SharedPreferences prefs = PreferenceManager.getDefaultSharedPreferences(context); + getStateMachine().handleEvent(ActivationStateMachine.CalledActivate.useCustomRegistrar(useCustomRegistrar, prefs)); + } + + public void deactivate() throws AblyException { + deactivate(false); + } + + public void deactivate(boolean useCustomRegistrar) throws AblyException { + Log.v(TAG, "deactivate(): useCustomRegistrar " + useCustomRegistrar); + Context context = getApplicationContext(); + SharedPreferences prefs = PreferenceManager.getDefaultSharedPreferences(context); + getStateMachine().handleEvent(ActivationStateMachine.CalledDeactivate.useCustomRegistrar(useCustomRegistrar, prefs)); + } + + synchronized ActivationStateMachine getStateMachine() throws AblyException { + return getActivationContext().getActivationStateMachine(); + } + + public void tryRequestRegistrationToken() { + try { + if (getLocalDevice().isRegistered()) { + getStateMachine().getRegistrationToken(); + } + } catch (AblyException e) { + Log.e(TAG, "couldn't validate existing push recipient device details", e); + } + } + + Context getApplicationContext() throws AblyException { + Context applicationContext = rest.platform.getApplicationContext(); + if(applicationContext == null) { + Log.e(TAG, "getApplicationContext(): Unable to get application context; not set"); + throw AblyException.fromErrorInfo(new ErrorInfo("Unable to get application context; not set", 40000, 400)); + } + return applicationContext; + } + + protected ActivationContext activationContext = null; + + public ActivationContext getActivationContext() throws AblyException { + if (activationContext == null) { + Context applicationContext = getApplicationContext(); + activationContext = ActivationContext.getActivationContext(applicationContext, (AblyRest)rest); + } + return activationContext; + } + + public LocalDevice getLocalDevice() throws AblyException { + return getActivationContext().getLocalDevice(); + } + + @Override + Param[] pushRequestHeaders(boolean forLocalDevice) { + Param[] headers = super.pushRequestHeaders(forLocalDevice); + if(forLocalDevice) { + try { + Param[] deviceIdentityHeaders = getLocalDevice().deviceIdentityHeaders(); + if(deviceIdentityHeaders != null) { + headers = HttpUtils.mergeHeaders(headers, deviceIdentityHeaders); + } + } catch (AblyException e) {} + } + return headers; + } + + Param[] pushRequestHeaders(String deviceId) { + boolean forLocalDevice = false; + try { + forLocalDevice = deviceId != null && deviceId.equals(getLocalDevice().id); + } catch (AblyException e) {} + return pushRequestHeaders(forLocalDevice); + } + + private static final String TAG = Push.class.getName(); } diff --git a/android/src/main/java/io/ably/lib/push/PushChannel.java b/android/src/main/java/io/ably/lib/push/PushChannel.java index 88a14bec7..5765a7a83 100644 --- a/android/src/main/java/io/ably/lib/push/PushChannel.java +++ b/android/src/main/java/io/ably/lib/push/PushChannel.java @@ -10,168 +10,168 @@ import io.ably.lib.types.*; public class PushChannel { - protected final Channel channel; - protected final AblyRest rest; - - public PushChannel(Channel channel, AblyRest rest) { - this.channel = channel; - this.rest = rest; - } - - public void subscribeClient() throws AblyException { - subscribeClientImpl().sync(); - } - - public void subscribeClientAsync(CompletionListener listener) { - subscribeClientImpl().async(new CompletionListener.ToCallback(listener)); - } - - protected Http.Request subscribeClientImpl() { - JsonObject bodyJson = new JsonObject(); - try { - bodyJson.addProperty("clientId", getClientId()); - } catch (AblyException e) { - return rest.http.failedRequest(e); - } - - return postSubscription(bodyJson); - } - - public void subscribeDevice() throws AblyException { - subscribeDeviceImpl().sync(); - } - - public void subscribeDeviceAsync(CompletionListener listener) { - subscribeDeviceImpl().async(new CompletionListener.ToCallback(listener)); - } - - protected Http.Request subscribeDeviceImpl() { - try { - DeviceDetails device = getDevice(); - JsonObject bodyJson = new JsonObject(); - bodyJson.addProperty("deviceId", device.id); - - return postSubscription(bodyJson); - } catch(AblyException e) { - return rest.http.failedRequest(e); - } - } - - protected Http.Request postSubscription(JsonObject bodyJson) { - bodyJson.addProperty("channel", channel.name); - final HttpCore.RequestBody body = HttpUtils.requestBodyFromGson(bodyJson, rest.options.useBinaryProtocol); - - return rest.http.request(new Http.Execute() { - @Override - public void execute(HttpScheduler http, Callback callback) throws AblyException { - Param[] params = null; - if (rest.options.pushFullWait) { - params = Param.push(params, "fullWait", "true"); - } - http.post("/push/channelSubscriptions", rest.push.pushRequestHeaders(true), params, body, null, true, callback); - } - }); - } - - public void unsubscribeClient() throws AblyException { - unsubscribeClientImpl().sync(); - } - - public void unsubscribeClientAsync(CompletionListener listener) { - unsubscribeClientImpl().async(new CompletionListener.ToCallback(listener)); - } - - protected Http.Request unsubscribeClientImpl() { - try { - Param[] params = new Param[] { new Param("channel", channel.name), new Param("clientId", getClientId()) }; - return delSubscription(params); - } catch(AblyException e) { - return rest.http.failedRequest(e); - } - } - - public void unsubscribeDevice() throws AblyException { - unsubscribeDeviceImpl().sync(); - } - - public void unsubscribeDeviceAsync(CompletionListener listener) { - unsubscribeDeviceImpl().async(new CompletionListener.ToCallback(listener)); - } - - protected Http.Request unsubscribeDeviceImpl() { - try { - DeviceDetails device = getDevice(); - Param[] params = new Param[] { new Param("channel", channel.name), new Param("deviceId", device.id) }; - return delSubscription(params); - } catch(AblyException e) { - return rest.http.failedRequest(e); - } - } - - protected Http.Request delSubscription(Param[] params) { - if (rest.options.pushFullWait) { - params = Param.push(params, "fullWait", "true"); - } - final Param[] finalParams = params; - return rest.http.request(new Http.Execute() { - @Override - public void execute(HttpScheduler http, Callback callback) throws AblyException { - http.del("/push/channelSubscriptions", rest.push.pushRequestHeaders(true), finalParams, null, true, callback); - } - }); - } - - public PaginatedResult listSubscriptions() throws AblyException { - return listSubscriptions(new Param[] {}); - } - - public PaginatedResult listSubscriptions(Param[] params) throws AblyException { - return listSubscriptionsImpl(params).sync(); - } - - public void listSubscriptionsAsync(Callback> callback) { - listSubscriptionsAsync(new Param[] {}, callback); - } - - public void listSubscriptionsAsync(Param[] params, Callback> callback) { - listSubscriptionsImpl(params).async(callback); - } - - protected BasePaginatedQuery.ResultRequest listSubscriptionsImpl(Param[] params) { - try { - params = Param.set(params, "deviceId", getDevice().id); - } catch(AblyException e) { - return new BasePaginatedQuery.ResultRequest.Failed(e); - } - params = Param.set(params, "channel", channel.name); - String clientId = rest.auth.clientId; - if (clientId != null) { - params = Param.set(params, "clientId", clientId); - } - params = Param.set(params, "concatFilters", "true"); - - return new BasePaginatedQuery(rest.http, "/push/channelSubscriptions", rest.push.pushRequestHeaders(true), params, Push.ChannelSubscription.httpBodyHandler).get(); - } - - protected String getClientId() throws AblyException { - String clientId = getDevice().clientId; - if (clientId == null) { - throw AblyException.fromThrowable(new Exception("cannot subscribe with null client ID")); - } - return clientId; - } - - protected DeviceDetails getDevice() throws AblyException { - LocalDevice localDevice = rest.push.getActivationContext().getLocalDevice(); - if (localDevice == null || localDevice.deviceIdentityToken == null) { - // Alternatively, we could store a queue of pending subscriptions in the - // device storage. But then, in order to know if this subscription operation - // succeeded, you would have to add a BroadcastReceiver in AndroidManifest.xml. - // Arguably that encourages just ignoring any errors, and forcing you to listen - // to the broadcast after push.activate has finished before subscribing is - // more robust. - throw AblyException.fromThrowable(new Exception("cannot use device before AblyRest.push.activate has finished")); - } - return localDevice; - } + protected final Channel channel; + protected final AblyRest rest; + + public PushChannel(Channel channel, AblyRest rest) { + this.channel = channel; + this.rest = rest; + } + + public void subscribeClient() throws AblyException { + subscribeClientImpl().sync(); + } + + public void subscribeClientAsync(CompletionListener listener) { + subscribeClientImpl().async(new CompletionListener.ToCallback(listener)); + } + + protected Http.Request subscribeClientImpl() { + JsonObject bodyJson = new JsonObject(); + try { + bodyJson.addProperty("clientId", getClientId()); + } catch (AblyException e) { + return rest.http.failedRequest(e); + } + + return postSubscription(bodyJson); + } + + public void subscribeDevice() throws AblyException { + subscribeDeviceImpl().sync(); + } + + public void subscribeDeviceAsync(CompletionListener listener) { + subscribeDeviceImpl().async(new CompletionListener.ToCallback(listener)); + } + + protected Http.Request subscribeDeviceImpl() { + try { + DeviceDetails device = getDevice(); + JsonObject bodyJson = new JsonObject(); + bodyJson.addProperty("deviceId", device.id); + + return postSubscription(bodyJson); + } catch(AblyException e) { + return rest.http.failedRequest(e); + } + } + + protected Http.Request postSubscription(JsonObject bodyJson) { + bodyJson.addProperty("channel", channel.name); + final HttpCore.RequestBody body = HttpUtils.requestBodyFromGson(bodyJson, rest.options.useBinaryProtocol); + + return rest.http.request(new Http.Execute() { + @Override + public void execute(HttpScheduler http, Callback callback) throws AblyException { + Param[] params = null; + if (rest.options.pushFullWait) { + params = Param.push(params, "fullWait", "true"); + } + http.post("/push/channelSubscriptions", rest.push.pushRequestHeaders(true), params, body, null, true, callback); + } + }); + } + + public void unsubscribeClient() throws AblyException { + unsubscribeClientImpl().sync(); + } + + public void unsubscribeClientAsync(CompletionListener listener) { + unsubscribeClientImpl().async(new CompletionListener.ToCallback(listener)); + } + + protected Http.Request unsubscribeClientImpl() { + try { + Param[] params = new Param[] { new Param("channel", channel.name), new Param("clientId", getClientId()) }; + return delSubscription(params); + } catch(AblyException e) { + return rest.http.failedRequest(e); + } + } + + public void unsubscribeDevice() throws AblyException { + unsubscribeDeviceImpl().sync(); + } + + public void unsubscribeDeviceAsync(CompletionListener listener) { + unsubscribeDeviceImpl().async(new CompletionListener.ToCallback(listener)); + } + + protected Http.Request unsubscribeDeviceImpl() { + try { + DeviceDetails device = getDevice(); + Param[] params = new Param[] { new Param("channel", channel.name), new Param("deviceId", device.id) }; + return delSubscription(params); + } catch(AblyException e) { + return rest.http.failedRequest(e); + } + } + + protected Http.Request delSubscription(Param[] params) { + if (rest.options.pushFullWait) { + params = Param.push(params, "fullWait", "true"); + } + final Param[] finalParams = params; + return rest.http.request(new Http.Execute() { + @Override + public void execute(HttpScheduler http, Callback callback) throws AblyException { + http.del("/push/channelSubscriptions", rest.push.pushRequestHeaders(true), finalParams, null, true, callback); + } + }); + } + + public PaginatedResult listSubscriptions() throws AblyException { + return listSubscriptions(new Param[] {}); + } + + public PaginatedResult listSubscriptions(Param[] params) throws AblyException { + return listSubscriptionsImpl(params).sync(); + } + + public void listSubscriptionsAsync(Callback> callback) { + listSubscriptionsAsync(new Param[] {}, callback); + } + + public void listSubscriptionsAsync(Param[] params, Callback> callback) { + listSubscriptionsImpl(params).async(callback); + } + + protected BasePaginatedQuery.ResultRequest listSubscriptionsImpl(Param[] params) { + try { + params = Param.set(params, "deviceId", getDevice().id); + } catch(AblyException e) { + return new BasePaginatedQuery.ResultRequest.Failed(e); + } + params = Param.set(params, "channel", channel.name); + String clientId = rest.auth.clientId; + if (clientId != null) { + params = Param.set(params, "clientId", clientId); + } + params = Param.set(params, "concatFilters", "true"); + + return new BasePaginatedQuery(rest.http, "/push/channelSubscriptions", rest.push.pushRequestHeaders(true), params, Push.ChannelSubscription.httpBodyHandler).get(); + } + + protected String getClientId() throws AblyException { + String clientId = getDevice().clientId; + if (clientId == null) { + throw AblyException.fromThrowable(new Exception("cannot subscribe with null client ID")); + } + return clientId; + } + + protected DeviceDetails getDevice() throws AblyException { + LocalDevice localDevice = rest.push.getActivationContext().getLocalDevice(); + if (localDevice == null || localDevice.deviceIdentityToken == null) { + // Alternatively, we could store a queue of pending subscriptions in the + // device storage. But then, in order to know if this subscription operation + // succeeded, you would have to add a BroadcastReceiver in AndroidManifest.xml. + // Arguably that encourages just ignoring any errors, and forcing you to listen + // to the broadcast after push.activate has finished before subscribing is + // more robust. + throw AblyException.fromThrowable(new Exception("cannot use device before AblyRest.push.activate has finished")); + } + return localDevice; + } } diff --git a/android/src/main/java/io/ably/lib/realtime/Channel.java b/android/src/main/java/io/ably/lib/realtime/Channel.java index f77764f58..3348b2719 100644 --- a/android/src/main/java/io/ably/lib/realtime/Channel.java +++ b/android/src/main/java/io/ably/lib/realtime/Channel.java @@ -5,18 +5,18 @@ import io.ably.lib.push.PushChannel; public class Channel extends ChannelBase { - /** - * The push instance for this channel. - */ - public final PushChannel push; + /** + * The push instance for this channel. + */ + public final PushChannel push; - Channel(AblyRealtime ably, String name, ChannelOptions options) throws AblyException { - super(ably, name, options); - this.push = ((io.ably.lib.rest.AblyRest) ably).channels.get(name, options).push; - } + Channel(AblyRealtime ably, String name, ChannelOptions options) throws AblyException { + super(ably, name, options); + this.push = ((io.ably.lib.rest.AblyRest) ably).channels.get(name, options).push; + } - /** - * An interface whereby a client maybe notified of messages changes on a channel. - */ - public interface MessageListener extends ChannelBase.MessageListener {} + /** + * An interface whereby a client maybe notified of messages changes on a channel. + */ + public interface MessageListener extends ChannelBase.MessageListener {} } 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 bdfd13980..09f2c31a5 100644 --- a/android/src/main/java/io/ably/lib/rest/AblyRest.java +++ b/android/src/main/java/io/ably/lib/rest/AblyRest.java @@ -7,57 +7,57 @@ import io.ably.lib.util.Log; public class AblyRest extends AblyBase { - /** - * Instance the Ably library using a key only. - * This is simply a convenience constructor for the - * simplest case of instancing the library with a key - * for basic authentication and no other options. - * @param key; String key (obtained from application dashboard) - * @throws AblyException - */ - public AblyRest(String key) throws AblyException { - super(key); - } + /** + * Instance the Ably library using a key only. + * This is simply a convenience constructor for the + * simplest case of instancing the library with a key + * for basic authentication and no other options. + * @param key; String key (obtained from application dashboard) + * @throws AblyException + */ + public AblyRest(String key) throws AblyException { + super(key); + } - /** - * Instance the Ably library with the given options. - * @param options: see {@link io.ably.lib.types.ClientOptions} for options - * @throws AblyException - */ - public AblyRest(ClientOptions options) throws AblyException { - super(options); - } + /** + * Instance the Ably library with the given options. + * @param options: see {@link io.ably.lib.types.ClientOptions} for options + * @throws AblyException + */ + public AblyRest(ClientOptions options) throws AblyException { + super(options); + } - /** - * Get the local device, if any - * @return an instance of LocalDevice, or null if this device is not capable of activation as a push target - * @throws AblyException - */ - public LocalDevice device() throws AblyException { - return this.push.getLocalDevice(); - } + /** + * Get the local device, if any + * @return an instance of LocalDevice, or null if this device is not capable of activation as a push target + * @throws AblyException + */ + public LocalDevice device() throws AblyException { + return this.push.getLocalDevice(); + } - /** - * Set the Android Context for this instance - */ - public void setAndroidContext(Context context) throws AblyException { - this.platform.setAndroidContext(context); - this.push.tryRequestRegistrationToken(); - } + /** + * Set the Android Context for this instance + */ + public void setAndroidContext(Context context) throws AblyException { + this.platform.setAndroidContext(context); + this.push.tryRequestRegistrationToken(); + } - /** - * clientId set by late initialisation - */ - protected void onClientIdSet(String clientId) { - /* we only need to propagate any update to clientId if this is a late init */ - if(push != null && platform.hasApplicationContext()) { - try { - push.getActivationContext().setClientId(clientId, true); - } catch(AblyException ae) { - Log.e(TAG, "unable to update local device state"); - } - } - } + /** + * clientId set by late initialisation + */ + protected void onClientIdSet(String clientId) { + /* we only need to propagate any update to clientId if this is a late init */ + if(push != null && platform.hasApplicationContext()) { + try { + push.getActivationContext().setClientId(clientId, true); + } catch(AblyException ae) { + Log.e(TAG, "unable to update local device state"); + } + } + } - private static final String TAG = AblyRest.class.getName(); + private static final String TAG = AblyRest.class.getName(); } diff --git a/android/src/main/java/io/ably/lib/rest/Channel.java b/android/src/main/java/io/ably/lib/rest/Channel.java index 4e5337482..f5dade377 100644 --- a/android/src/main/java/io/ably/lib/rest/Channel.java +++ b/android/src/main/java/io/ably/lib/rest/Channel.java @@ -5,13 +5,13 @@ import io.ably.lib.types.ChannelOptions; public class Channel extends ChannelBase { - /** - * The push instance for this channel. - */ - public final PushChannel push; + /** + * The push instance for this channel. + */ + public final PushChannel push; - Channel(AblyBase ably, String name, ChannelOptions options) throws AblyException { - super(ably, name, options); - this.push = new PushChannel(this, (AblyRest)ably); - } + Channel(AblyBase ably, String name, ChannelOptions options) throws AblyException { + super(ably, name, options); + this.push = new PushChannel(this, (AblyRest)ably); + } } \ No newline at end of file diff --git a/android/src/main/java/io/ably/lib/types/RegistrationToken.java b/android/src/main/java/io/ably/lib/types/RegistrationToken.java index ad3c0d8a4..69646d9e1 100644 --- a/android/src/main/java/io/ably/lib/types/RegistrationToken.java +++ b/android/src/main/java/io/ably/lib/types/RegistrationToken.java @@ -1,36 +1,36 @@ package io.ably.lib.types; public class RegistrationToken { - public Type type; - public String token; + public Type type; + public String token; - public RegistrationToken(Type type, String token) { - this.type = type; - this.token = token; - } + public RegistrationToken(Type type, String token) { + this.type = type; + this.token = token; + } - public enum Type { - @Deprecated GCM, - FCM; + public enum Type { + @Deprecated GCM, + FCM; - public static Type fromOrdinal(int i) { - try { - return Type.values()[i]; - } catch(Throwable t) { - return null; - } - } + public static Type fromOrdinal(int i) { + try { + return Type.values()[i]; + } catch(Throwable t) { + return null; + } + } - public static Type fromName(String name) { - try { - return Type.valueOf(name.toUpperCase()); - } catch(Throwable t) { - return null; - } - } + public static Type fromName(String name) { + try { + return Type.valueOf(name.toUpperCase()); + } catch(Throwable t) { + return null; + } + } - public String toName() { - return name().toLowerCase(); - } - } + public String toName() { + return name().toLowerCase(); + } + } } diff --git a/android/src/main/java/io/ably/lib/util/IntentUtils.java b/android/src/main/java/io/ably/lib/util/IntentUtils.java index 1c429bfac..3f3ec5fe6 100644 --- a/android/src/main/java/io/ably/lib/util/IntentUtils.java +++ b/android/src/main/java/io/ably/lib/util/IntentUtils.java @@ -4,23 +4,23 @@ import io.ably.lib.types.ErrorInfo; public class IntentUtils { - public static void addErrorInfo(Intent intent, ErrorInfo error) { - intent.putExtra("hasError", error != null); - if (error != null) { - intent.putExtra("error.message", error.message); - intent.putExtra("error.statusCode", error.statusCode); - intent.putExtra("error.code", error.code); - } - } + public static void addErrorInfo(Intent intent, ErrorInfo error) { + intent.putExtra("hasError", error != null); + if (error != null) { + intent.putExtra("error.message", error.message); + intent.putExtra("error.statusCode", error.statusCode); + intent.putExtra("error.code", error.code); + } + } - public static ErrorInfo getErrorInfo(Intent intent) { - if (!intent.getBooleanExtra("hasError", false)) { - return null; - } - return new ErrorInfo( - intent.getStringExtra("error.message"), - intent.getIntExtra("error.statusCode", 0), - intent.getIntExtra("error.code", 0) - ); - } + public static ErrorInfo getErrorInfo(Intent intent) { + if (!intent.getBooleanExtra("hasError", false)) { + return null; + } + return new ErrorInfo( + intent.getStringExtra("error.message"), + intent.getIntExtra("error.statusCode", 0), + intent.getIntExtra("error.code", 0) + ); + } } diff --git a/java/src/main/java/io/ably/lib/platform/Platform.java b/java/src/main/java/io/ably/lib/platform/Platform.java index 92f3516d5..ee8cdf1a0 100644 --- a/java/src/main/java/io/ably/lib/platform/Platform.java +++ b/java/src/main/java/io/ably/lib/platform/Platform.java @@ -4,13 +4,13 @@ import io.ably.lib.transport.NetworkConnectivity; public class Platform { - public static final String name = "java"; + public static final String name = "java"; - public Platform() {} + public Platform() {} - public NetworkConnectivity getNetworkConnectivity() { - return networkConnectivity; - } + public NetworkConnectivity getNetworkConnectivity() { + return networkConnectivity; + } - private final NetworkConnectivity networkConnectivity = new NetworkConnectivity.DefaultNetworkConnectivity(); + private final NetworkConnectivity networkConnectivity = new NetworkConnectivity.DefaultNetworkConnectivity(); } diff --git a/java/src/main/java/io/ably/lib/push/Push.java b/java/src/main/java/io/ably/lib/push/Push.java index 45c1f8f53..a6a2ba584 100644 --- a/java/src/main/java/io/ably/lib/push/Push.java +++ b/java/src/main/java/io/ably/lib/push/Push.java @@ -3,7 +3,7 @@ import io.ably.lib.rest.AblyBase; public class Push extends PushBase { - public Push(AblyBase rest) { - super(rest); - } + public Push(AblyBase rest) { + super(rest); + } } diff --git a/java/src/main/java/io/ably/lib/realtime/Channel.java b/java/src/main/java/io/ably/lib/realtime/Channel.java index 3e89f1a07..9c7f64995 100644 --- a/java/src/main/java/io/ably/lib/realtime/Channel.java +++ b/java/src/main/java/io/ably/lib/realtime/Channel.java @@ -4,9 +4,9 @@ import io.ably.lib.types.ChannelOptions; public class Channel extends ChannelBase { - Channel(AblyRealtime ably, String name, ChannelOptions options) throws AblyException { - super(ably, name, options); - } + Channel(AblyRealtime ably, String name, ChannelOptions options) throws AblyException { + super(ably, name, options); + } - public interface MessageListener extends ChannelBase.MessageListener {} + public interface MessageListener extends ChannelBase.MessageListener {} } 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 18a7f0bfe..9bd9d6b32 100644 --- a/java/src/main/java/io/ably/lib/rest/AblyRest.java +++ b/java/src/main/java/io/ably/lib/rest/AblyRest.java @@ -4,24 +4,24 @@ import io.ably.lib.types.ClientOptions; public class AblyRest extends AblyBase { - /** - * Instance the Ably library using a key only. - * This is simply a convenience constructor for the - * simplest case of instancing the library with a key - * for basic authentication and no other options. - * @param key; String key (obtained from application dashboard) - * @throws AblyException - */ - public AblyRest(String key) throws AblyException { - super(key); - } + /** + * Instance the Ably library using a key only. + * This is simply a convenience constructor for the + * simplest case of instancing the library with a key + * for basic authentication and no other options. + * @param key; String key (obtained from application dashboard) + * @throws AblyException + */ + public AblyRest(String key) throws AblyException { + super(key); + } - /** - * Instance the Ably library with the given options. - * @param options: see {@link io.ably.lib.types.ClientOptions} for options - * @throws AblyException - */ - public AblyRest(ClientOptions options) throws AblyException { - super(options); - } + /** + * Instance the Ably library with the given options. + * @param options: see {@link io.ably.lib.types.ClientOptions} for options + * @throws AblyException + */ + public AblyRest(ClientOptions options) throws AblyException { + super(options); + } } diff --git a/java/src/main/java/io/ably/lib/rest/Channel.java b/java/src/main/java/io/ably/lib/rest/Channel.java index b94eb2627..1c90efb41 100644 --- a/java/src/main/java/io/ably/lib/rest/Channel.java +++ b/java/src/main/java/io/ably/lib/rest/Channel.java @@ -4,7 +4,7 @@ import io.ably.lib.types.ChannelOptions; public class Channel extends ChannelBase { - Channel(AblyBase ably, String name, ChannelOptions options) throws AblyException { - super(ably, name, options); - } + Channel(AblyBase ably, String name, ChannelOptions options) throws AblyException { + super(ably, name, options); + } } diff --git a/java/src/test/java/io/ably/lib/test/loader/ArgumentLoader.java b/java/src/test/java/io/ably/lib/test/loader/ArgumentLoader.java index 378bf9fbe..241db394b 100644 --- a/java/src/test/java/io/ably/lib/test/loader/ArgumentLoader.java +++ b/java/src/test/java/io/ably/lib/test/loader/ArgumentLoader.java @@ -1,7 +1,7 @@ package io.ably.lib.test.loader; public class ArgumentLoader { - public String getTestArgument(String name) { - return System.getenv(name); - } + public String getTestArgument(String name) { + return System.getenv(name); + } } diff --git a/java/src/test/java/io/ably/lib/test/loader/ResourceLoader.java b/java/src/test/java/io/ably/lib/test/loader/ResourceLoader.java index 40e3b450d..df06dd5c1 100644 --- a/java/src/test/java/io/ably/lib/test/loader/ResourceLoader.java +++ b/java/src/test/java/io/ably/lib/test/loader/ResourceLoader.java @@ -9,21 +9,21 @@ * Implementation of ResourceLoader for JRE environment */ public class ResourceLoader { - public byte[] read(String resourceName) throws IOException { - FileInputStream fis = null; - System.out.println("Current dir: " + new File(".").getAbsolutePath()); - try { - try { - fis = new FileInputStream(new File("../lib/src/test/resources", resourceName)); - } catch(FileNotFoundException fnfe) { - fis = new FileInputStream(new File("lib/src/test/resources", resourceName)); - } - byte[] bytes = new byte[fis.available()]; - fis.read(bytes); - return bytes; - } finally { - if(fis != null) - fis.close(); - } - } + public byte[] read(String resourceName) throws IOException { + FileInputStream fis = null; + System.out.println("Current dir: " + new File(".").getAbsolutePath()); + try { + try { + fis = new FileInputStream(new File("../lib/src/test/resources", resourceName)); + } catch(FileNotFoundException fnfe) { + fis = new FileInputStream(new File("lib/src/test/resources", resourceName)); + } + byte[] bytes = new byte[fis.available()]; + fis.read(bytes); + return bytes; + } finally { + if(fis != null) + fis.close(); + } + } } diff --git a/lib/src/main/java/io/ably/lib/debug/DebugOptions.java b/lib/src/main/java/io/ably/lib/debug/DebugOptions.java index 31ae41b1d..5ecc7b6d4 100644 --- a/lib/src/main/java/io/ably/lib/debug/DebugOptions.java +++ b/lib/src/main/java/io/ably/lib/debug/DebugOptions.java @@ -11,24 +11,24 @@ import io.ably.lib.types.ProtocolMessage; public class DebugOptions extends ClientOptions { - public interface RawProtocolListener { - void onRawConnectRequested(String url); - void onRawConnect(String url); - void onRawMessageSend(ProtocolMessage message); - void onRawMessageRecv(ProtocolMessage message); - } + public interface RawProtocolListener { + void onRawConnectRequested(String url); + void onRawConnect(String url); + void onRawMessageSend(ProtocolMessage message); + void onRawMessageRecv(ProtocolMessage message); + } - public interface RawHttpListener { - HttpCore.Response onRawHttpRequest(String id, HttpURLConnection conn, String method, String authHeader, Map> requestHeaders, HttpCore.RequestBody requestBody); - void onRawHttpResponse(String id, String method, HttpCore.Response response); - void onRawHttpException(String id, String method, Throwable t); - } + public interface RawHttpListener { + HttpCore.Response onRawHttpRequest(String id, HttpURLConnection conn, String method, String authHeader, Map> requestHeaders, HttpCore.RequestBody requestBody); + void onRawHttpResponse(String id, String method, HttpCore.Response response); + void onRawHttpException(String id, String method, Throwable t); + } - public DebugOptions() { super(); pushFullWait = true; } + public DebugOptions() { super(); pushFullWait = true; } - public DebugOptions(String key) throws AblyException { super(key); pushFullWait = true; } + public DebugOptions(String key) throws AblyException { super(key); pushFullWait = true; } - public RawProtocolListener protocolListener; - public RawHttpListener httpListener; - public ITransport.Factory transportFactory; + public RawProtocolListener protocolListener; + public RawHttpListener httpListener; + public ITransport.Factory transportFactory; } diff --git a/lib/src/main/java/io/ably/lib/http/AsyncHttpPaginatedQuery.java b/lib/src/main/java/io/ably/lib/http/AsyncHttpPaginatedQuery.java index d0ecfb4b3..3acf5d443 100644 --- a/lib/src/main/java/io/ably/lib/http/AsyncHttpPaginatedQuery.java +++ b/lib/src/main/java/io/ably/lib/http/AsyncHttpPaginatedQuery.java @@ -16,149 +16,149 @@ public class AsyncHttpPaginatedQuery implements HttpCore.ResponseHandler { - public AsyncHttpPaginatedQuery(Http http, String method, String path, Param[] headers, Param[] params, - HttpCore.RequestBody requestBody) { - this.http = http; - this.method = method; - this.path = path; - this.headers = headers; - this.params = params; - this.requestBody = requestBody; - this.bodyHandler = HttpPaginatedQuery.jsonArrayResponseHandler; - } - - public void exec(final AsyncHttpPaginatedResponse.Callback callback) { - exec(params, callback); - } - - public void exec(final Param[] params, final AsyncHttpPaginatedResponse.Callback callback) { - final HttpCore.ResponseHandler responseHandler = this; - http.request(new Http.Execute() { - @Override - public void execute(HttpScheduler http, Callback callback) throws AblyException { - http.exec(path, method, headers, params, requestBody, responseHandler, true, callback); - } - }).async(wrap(callback)); - } - - /** - * A private class encapsulating the result of a single page response - * - */ - public class AsyncHttpPaginatedResult extends AsyncHttpPaginatedResponse { - private JsonElement[] contents; - - private AsyncHttpPaginatedResult(HttpCore.Response response, ErrorInfo error) { - statusCode = response.statusCode; - headers = HttpUtils.toParamArray(response.headers); - if(error != null) { - errorCode = error.code; - errorMessage = error.message; - } else { - success = true; - if(response.body != null) { - try { - contents = bodyHandler.handleResponseBody(response.contentType, response.body); - } catch (AblyException e) { - success = false; - errorCode = e.errorInfo.code; - errorMessage = e.errorInfo.message; - } - } - } - - List linkHeaders = response.getHeaderFields(HttpConstants.Headers.LINK); - if(linkHeaders != null) { - HashMap links = BasePaginatedQuery.parseLinks(linkHeaders); - relFirst = links.get("first"); - relCurrent = links.get("current"); - relNext = links.get("next"); - } else { - relFirst = null; - relCurrent = null; - relNext = null; - } - } - - @Override - public JsonElement[] items() { return contents; } - - @Override - public void first(AsyncHttpPaginatedResponse.Callback callback) { - execRel(relFirst, callback); - } - - @Override - public void current(AsyncHttpPaginatedResponse.Callback callback) { - execRel(relCurrent, callback); - } - - @Override - public void next(AsyncHttpPaginatedResponse.Callback callback) { - execRel(relNext, callback); - } - - private void execRel(String linkUrl, AsyncHttpPaginatedResponse.Callback callback) { - if(linkUrl == null) { - callback.onResponse(null); - return; - } - - /* we're expecting the format to be ./path-component?name=value&name=value... */ - Matcher urlMatch = BasePaginatedQuery.urlPattern.matcher(linkUrl); - if(!urlMatch.matches()) { - callback.onError(new ErrorInfo("Unexpected link URL format", 500, 50000)); - return; - } - - String[] paramSpecs = urlMatch.group(2).split("&"); - Param[] params = new Param[paramSpecs.length]; - try { - for(int i = 0; i < paramSpecs.length; i++) { - String[] split = paramSpecs[i].split("="); - String paramKey = split[0]; - String paramValue = (split.length >= 2) ? split[1] : ""; - params[i] = new Param(paramKey, URLDecoder.decode(paramValue, "UTF-8")); - } - } catch(UnsupportedEncodingException uee) {} - exec(params, callback); - } - - @Override - public boolean hasFirst() { return relFirst != null; } - - @Override - public boolean hasCurrent() { return relCurrent != null; } - - @Override - public boolean hasNext() { return relNext != null; } - - private final String relFirst, relCurrent, relNext; - } - - @Override - public AsyncHttpPaginatedResponse handleResponse(HttpCore.Response response, ErrorInfo error) { - return new AsyncHttpPaginatedResult(response, error); - } - - private static Callback wrap(final AsyncHttpPaginatedResponse.Callback callback) { - return new Callback() { - @Override - public void onSuccess(AsyncHttpPaginatedResponse result) { - callback.onResponse(result); - } - @Override - public void onError(ErrorInfo reason) { - callback.onError(reason); - } - }; - } - - private final Http http; - private final String method; - private final String path; - private final Param[] headers; - private final Param[] params; - private final HttpCore.RequestBody requestBody; - private final HttpCore.BodyHandler bodyHandler; + public AsyncHttpPaginatedQuery(Http http, String method, String path, Param[] headers, Param[] params, + HttpCore.RequestBody requestBody) { + this.http = http; + this.method = method; + this.path = path; + this.headers = headers; + this.params = params; + this.requestBody = requestBody; + this.bodyHandler = HttpPaginatedQuery.jsonArrayResponseHandler; + } + + public void exec(final AsyncHttpPaginatedResponse.Callback callback) { + exec(params, callback); + } + + public void exec(final Param[] params, final AsyncHttpPaginatedResponse.Callback callback) { + final HttpCore.ResponseHandler responseHandler = this; + http.request(new Http.Execute() { + @Override + public void execute(HttpScheduler http, Callback callback) throws AblyException { + http.exec(path, method, headers, params, requestBody, responseHandler, true, callback); + } + }).async(wrap(callback)); + } + + /** + * A private class encapsulating the result of a single page response + * + */ + public class AsyncHttpPaginatedResult extends AsyncHttpPaginatedResponse { + private JsonElement[] contents; + + private AsyncHttpPaginatedResult(HttpCore.Response response, ErrorInfo error) { + statusCode = response.statusCode; + headers = HttpUtils.toParamArray(response.headers); + if(error != null) { + errorCode = error.code; + errorMessage = error.message; + } else { + success = true; + if(response.body != null) { + try { + contents = bodyHandler.handleResponseBody(response.contentType, response.body); + } catch (AblyException e) { + success = false; + errorCode = e.errorInfo.code; + errorMessage = e.errorInfo.message; + } + } + } + + List linkHeaders = response.getHeaderFields(HttpConstants.Headers.LINK); + if(linkHeaders != null) { + HashMap links = BasePaginatedQuery.parseLinks(linkHeaders); + relFirst = links.get("first"); + relCurrent = links.get("current"); + relNext = links.get("next"); + } else { + relFirst = null; + relCurrent = null; + relNext = null; + } + } + + @Override + public JsonElement[] items() { return contents; } + + @Override + public void first(AsyncHttpPaginatedResponse.Callback callback) { + execRel(relFirst, callback); + } + + @Override + public void current(AsyncHttpPaginatedResponse.Callback callback) { + execRel(relCurrent, callback); + } + + @Override + public void next(AsyncHttpPaginatedResponse.Callback callback) { + execRel(relNext, callback); + } + + private void execRel(String linkUrl, AsyncHttpPaginatedResponse.Callback callback) { + if(linkUrl == null) { + callback.onResponse(null); + return; + } + + /* we're expecting the format to be ./path-component?name=value&name=value... */ + Matcher urlMatch = BasePaginatedQuery.urlPattern.matcher(linkUrl); + if(!urlMatch.matches()) { + callback.onError(new ErrorInfo("Unexpected link URL format", 500, 50000)); + return; + } + + String[] paramSpecs = urlMatch.group(2).split("&"); + Param[] params = new Param[paramSpecs.length]; + try { + for(int i = 0; i < paramSpecs.length; i++) { + String[] split = paramSpecs[i].split("="); + String paramKey = split[0]; + String paramValue = (split.length >= 2) ? split[1] : ""; + params[i] = new Param(paramKey, URLDecoder.decode(paramValue, "UTF-8")); + } + } catch(UnsupportedEncodingException uee) {} + exec(params, callback); + } + + @Override + public boolean hasFirst() { return relFirst != null; } + + @Override + public boolean hasCurrent() { return relCurrent != null; } + + @Override + public boolean hasNext() { return relNext != null; } + + private final String relFirst, relCurrent, relNext; + } + + @Override + public AsyncHttpPaginatedResponse handleResponse(HttpCore.Response response, ErrorInfo error) { + return new AsyncHttpPaginatedResult(response, error); + } + + private static Callback wrap(final AsyncHttpPaginatedResponse.Callback callback) { + return new Callback() { + @Override + public void onSuccess(AsyncHttpPaginatedResponse result) { + callback.onResponse(result); + } + @Override + public void onError(ErrorInfo reason) { + callback.onError(reason); + } + }; + } + + private final Http http; + private final String method; + private final String path; + private final Param[] headers; + private final Param[] params; + private final HttpCore.RequestBody requestBody; + private final HttpCore.BodyHandler bodyHandler; } 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 5b12b90c4..424f2dfd6 100644 --- a/lib/src/main/java/io/ably/lib/http/AsyncHttpScheduler.java +++ b/lib/src/main/java/io/ably/lib/http/AsyncHttpScheduler.java @@ -12,23 +12,23 @@ * A HttpScheduler that uses a thread pool to run HTTP operations. */ public class AsyncHttpScheduler extends HttpScheduler { - public AsyncHttpScheduler(HttpCore httpCore, ClientOptions options) { - super(httpCore, new ThreadPoolExecutor(options.asyncHttpThreadpoolSize, options.asyncHttpThreadpoolSize, KEEP_ALIVE_TIME, TimeUnit.MILLISECONDS, new LinkedBlockingQueue())); - executor.allowsCoreThreadTimeOut(); - } + public AsyncHttpScheduler(HttpCore httpCore, ClientOptions options) { + super(httpCore, new ThreadPoolExecutor(options.asyncHttpThreadpoolSize, options.asyncHttpThreadpoolSize, KEEP_ALIVE_TIME, TimeUnit.MILLISECONDS, new LinkedBlockingQueue())); + executor.allowsCoreThreadTimeOut(); + } - public void dispose() { - ThreadPoolExecutor threadPoolExecutor = executor; - threadPoolExecutor.shutdown(); - try { - threadPoolExecutor.awaitTermination(SHUTDOWN_TIME, TimeUnit.MILLISECONDS); - } catch (InterruptedException e) { - threadPoolExecutor.shutdownNow(); - } - } + public void dispose() { + ThreadPoolExecutor threadPoolExecutor = executor; + threadPoolExecutor.shutdown(); + try { + threadPoolExecutor.awaitTermination(SHUTDOWN_TIME, TimeUnit.MILLISECONDS); + } catch (InterruptedException e) { + threadPoolExecutor.shutdownNow(); + } + } - private static final long KEEP_ALIVE_TIME = 2000L; - private static final long SHUTDOWN_TIME = 5000L; + private static final long KEEP_ALIVE_TIME = 2000L; + private static final long SHUTDOWN_TIME = 5000L; - protected static final String TAG = AsyncHttpScheduler.class.getName(); + protected static final String TAG = AsyncHttpScheduler.class.getName(); } diff --git a/lib/src/main/java/io/ably/lib/http/AsyncPaginatedQuery.java b/lib/src/main/java/io/ably/lib/http/AsyncPaginatedQuery.java index 0d527097f..00e7e31c1 100644 --- a/lib/src/main/java/io/ably/lib/http/AsyncPaginatedQuery.java +++ b/lib/src/main/java/io/ably/lib/http/AsyncPaginatedQuery.java @@ -10,41 +10,41 @@ * @param the body response type. */ public class AsyncPaginatedQuery { - private final BasePaginatedQuery base; + private final BasePaginatedQuery base; - /** - * Construct a PaginatedQuery - * - * @param http. the httpCore instance - * @param path. the path of the resource being queried - * @param headers. headers to pass into the first and all relative queries - * @param params. params to pass into the initial query - * @param bodyHandler. handler to parse response bodies for first and all relative queries - */ - public AsyncPaginatedQuery(Http http, String path, Param[] headers, Param[] params, HttpCore.BodyHandler bodyHandler) { - this(http, path, headers, params, null, bodyHandler); - } + /** + * Construct a PaginatedQuery + * + * @param http. the httpCore instance + * @param path. the path of the resource being queried + * @param headers. headers to pass into the first and all relative queries + * @param params. params to pass into the initial query + * @param bodyHandler. handler to parse response bodies for first and all relative queries + */ + public AsyncPaginatedQuery(Http http, String path, Param[] headers, Param[] params, HttpCore.BodyHandler bodyHandler) { + this(http, path, headers, params, null, bodyHandler); + } - /** - * Construct a PaginatedQuery - * - * @param http. the http instance - * @param path. the path of the resource being queried - * @param headers. headers to pass into the first and all relative queries - * @param params. params to pass into the initial query - * @param bodyHandler. handler to parse response bodies for first and all relative queries - */ - public AsyncPaginatedQuery(Http http, String path, Param[] headers, Param[] params, HttpCore.RequestBody requestBody, HttpCore.BodyHandler bodyHandler) { - base = new BasePaginatedQuery(http, path, headers, params, requestBody, bodyHandler); - } + /** + * Construct a PaginatedQuery + * + * @param http. the http instance + * @param path. the path of the resource being queried + * @param headers. headers to pass into the first and all relative queries + * @param params. params to pass into the initial query + * @param bodyHandler. handler to parse response bodies for first and all relative queries + */ + public AsyncPaginatedQuery(Http http, String path, Param[] headers, Param[] params, HttpCore.RequestBody requestBody, HttpCore.BodyHandler bodyHandler) { + base = new BasePaginatedQuery(http, path, headers, params, requestBody, bodyHandler); + } - /** - * Get the result of the first query - * @param callback. On success returns A PaginatedResult giving the - * first page of results together with any available links to related results pages. - */ - public void get(Callback> callback) { - base.get().async(callback); - } + /** + * Get the result of the first query + * @param callback. On success returns A PaginatedResult giving the + * first page of results together with any available links to related results pages. + */ + public void get(Callback> callback) { + base.get().async(callback); + } } diff --git a/lib/src/main/java/io/ably/lib/http/BasePaginatedQuery.java b/lib/src/main/java/io/ably/lib/http/BasePaginatedQuery.java index 4d16cc200..dfed450a0 100644 --- a/lib/src/main/java/io/ably/lib/http/BasePaginatedQuery.java +++ b/lib/src/main/java/io/ably/lib/http/BasePaginatedQuery.java @@ -25,322 +25,322 @@ */ public class BasePaginatedQuery implements HttpCore.ResponseHandler> { - /** - * Construct a PaginatedQuery - * - * @param http. the http instance - * @param path. the path of the resource being queried - * @param headers. headers to pass into the first and all relative queries - * @param params. params to pass into the initial query - * @param bodyHandler. handler to parse response bodies for first and all relative queries - */ - public BasePaginatedQuery(Http http, String path, Param[] headers, Param[] params, HttpCore.BodyHandler bodyHandler) { - this(http, path, headers, params, null, bodyHandler); - } - - /** - * Construct a PaginatedQuery - * - * @param http. the http instance - * @param path. the path of the resource being queried - * @param headers. headers to pass into the first and all relative queries - * @param params. params to pass into the initial query - * @param bodyHandler. handler to parse response bodies for first and all relative queries - */ - public BasePaginatedQuery(Http http, String path, Param[] headers, Param[] params, HttpCore.RequestBody requestBody, HttpCore.BodyHandler bodyHandler) { - this.http = http; - this.path = path; - this.requestHeaders = headers; - this.requestParams = params; - this.requestBody = requestBody; - this.bodyHandler = bodyHandler; - } - - /** - * Get the result of the first query - * @return A ResultRequest giving the first page of results - * together with any available links to related results pages. - */ - public ResultRequest get() { - return new ResultRequest(this.exec(HttpConstants.Methods.GET)); - } - - /** - * Get the result of the first query - * @return A Http.Request> giving the first page of results - * together with any available links to related results pages. - */ - public Http.Request> exec(final String method) { - final HttpCore.ResponseHandler> responseHandler = this; - return http.request(new Http.Execute>() { - @Override - public void execute(HttpScheduler http, Callback> callback) throws AblyException { - http.exec(path, method, requestHeaders, requestParams, requestBody, responseHandler, true, callback); - } - }); - } - - /** - * A private class encapsulating the result of a single page response - */ - private class ResultPage implements BasePaginatedResult { - private T[] contents; - - private ResultPage(T[] contents, Collection linkHeaders) throws AblyException { - this.contents = contents; - - if(linkHeaders != null) { - HashMap links = parseLinks(linkHeaders); - relFirst = links.get("first"); - relCurrent = links.get("current"); - relNext = links.get("next"); - } - } - - @Override - public T[] items() { return contents; } - - @Override - public Http.Request> first() { return getRel(relFirst); } - - @Override - public Http.Request> current() { return getRel(relCurrent); } - - @Override - public Http.Request> next() { return getRel(relNext); } - - private Http.Request> getRel(final String linkUrl) { - return http.request(new Http.Execute>() { - @Override - public void execute(HttpScheduler http, Callback> callback) throws AblyException { - if(linkUrl == null) { - callback.onSuccess(null); - return; - } - /* we're expecting the format to be ./path-component?name=value&name=value... */ - Matcher urlMatch = urlPattern.matcher(linkUrl); - if(!urlMatch.matches()) { - throw AblyException.fromErrorInfo(new ErrorInfo("Unexpected link URL format", 500, 50000)); - } - String[] paramSpecs = urlMatch.group(2).split("&"); - Param[] params = new Param[paramSpecs.length]; - try { - for(int i = 0; i < paramSpecs.length; i++) { - String[] split = paramSpecs[i].split("="); - params[i] = new Param(split[0], URLDecoder.decode(split[1], "UTF-8")); - } - } catch(UnsupportedEncodingException uee) {} - http.get(path, requestHeaders, params, BasePaginatedQuery.this, true, callback); - } - }); - } - - private String relFirst, relCurrent, relNext; - - @Override - public boolean hasFirst() { return relFirst != null; } - - @Override - public boolean hasCurrent() { return relCurrent != null; } - - @Override - public boolean hasNext() { return relNext != null; } - - @Override - public boolean isLast() { - return relNext == null; - } - } - - @Override - public BasePaginatedResult handleResponse(HttpCore.Response response, ErrorInfo error) throws AblyException { - if(error != null) { - throw AblyException.fromErrorInfo(error); - } - T[] responseContents = bodyHandler.handleResponseBody(response.contentType, response.body); - return new BasePaginatedQuery.ResultPage(responseContents, response.getHeaderFields(HttpConstants.Headers.LINK)); - } - - /**************** - * internal - ****************/ - - protected static Pattern linkPattern = Pattern.compile("\\s*<(.*)>;\\s*rel=\"(.*)\""); - protected static Pattern urlPattern = Pattern.compile("\\./(.*)\\?(.*)"); - - protected static HashMap parseLinks(Collection linkHeaders) { - HashMap result = new HashMap(); - for(String link : linkHeaders) { - /* we're expecting the format to be ; rel="first current ..." */ - Matcher linkMatch = linkPattern.matcher(link); - if(linkMatch.matches()) { - String linkUrl = linkMatch.group(1); - for (String linkRel : linkMatch.group(2).toLowerCase(Locale.ENGLISH).split("\\s")) - result.put(linkRel, linkUrl); - } - } - return result; - } - - private final Http http; - private final String path; - private final Param[] requestHeaders; - private final Param[] requestParams; - private final HttpCore.RequestBody requestBody; - private final HttpCore.BodyHandler bodyHandler; - - /** - * Wraps a Http.Request> to fixate on either a sync or an async interface. - * - * The Http.Request gives you a BasePaginatedResult whether you call its sync() or its async() - * methods, but we'd like to give it us a PaginatedResult or a AsyncPaginatedResult - * respectively, so this does the necessary bridging. - * - * @param - */ - public static class ResultRequest { - private final Http.Request> wrappedRequest; - - private ResultRequest(Http.Request> wrappedRequest) { - this.wrappedRequest = wrappedRequest; - } - - public PaginatedResult sync() throws AblyException { - return new SyncResultPage(wrappedRequest.sync()); - } - - public void async(final Callback> callback) { - wrappedRequest.async(new Callback>() { - @Override - public void onSuccess(BasePaginatedResult result) { - callback.onSuccess(new AsyncResultPage(result)); - } - - @Override - public void onError(ErrorInfo reason) { - callback.onError(reason); - } - }); - } - - /** - * A ResultRequest that has already failed due to a previous condition. - * - * Useful when a method must return a ResultRequest but fails before it can make the "real" - * one. Such errors are reported as thrown AblyExceptions in sync scenarios, and as - * Callback.onError calls in async ones. This class helps converting from plain exceptions - * to cover the async case too. - * - * @param - */ - public static class Failed extends ResultRequest { - private final AblyException reason; - - public Failed(AblyException reason) { - super(null); - this.reason = reason; - } - - @Override - public PaginatedResult sync() throws AblyException { - throw reason; - } - - @Override - public void async(Callback> callback) { - callback.onError(reason.errorInfo); - } - } - } - - /** - * Base class for SyncResultPage and AsyncResultPage. For code sharing purposes only. - * @param - */ - private static abstract class ResultPageWrapper { - protected final BasePaginatedResult resultBase; - - protected ResultPageWrapper(BasePaginatedResult resultBase) { - this.resultBase = resultBase; - } - - public T[] items() { return resultBase.items(); } - - public boolean hasFirst() { return resultBase.hasFirst(); } - - public boolean hasCurrent() { return resultBase.hasCurrent(); } - - public boolean hasNext() { return resultBase.hasNext(); } - - public boolean isLast() { - return resultBase.isLast(); - } - } - - /** - * Bridge from BasePaginatedResult to the synchronous PaginatedResult. - * @param - */ - private static class SyncResultPage extends ResultPageWrapper implements PaginatedResult { - SyncResultPage(BasePaginatedResult resultBase) { - super(resultBase); - } - - @Override - public PaginatedResult first() throws AblyException { return new SyncResultPage(resultBase.first().sync()); } - - @Override - public PaginatedResult current() throws AblyException { return new SyncResultPage(resultBase.current().sync()); } - - @Override - public PaginatedResult next() throws AblyException { return new SyncResultPage(resultBase.next().sync()); } - } - - /** - * Bridge from BasePaginatedResult to the asynchronous AsyncPaginatedResult. - * @param - */ - private static class AsyncResultPage extends ResultPageWrapper implements AsyncPaginatedResult { - AsyncResultPage(BasePaginatedResult resultBase) { - super(resultBase); - } - - @Override - public void first(Callback> callback) { - resultBase.first().async(new CallbackBridge(callback)); - } - - @Override - public void current(Callback> callback) { - resultBase.current().async(new CallbackBridge(callback)); - } - - @Override - public void next(Callback> callback) { - resultBase.next().async(new CallbackBridge(callback)); - } - } - - /** - * Bridge from Callback> to Callback>. - * - * @param - */ - private static class CallbackBridge implements Callback> { - private final Callback> callback; - - CallbackBridge(Callback> callback) { - this.callback = callback; - } - - @Override - public void onSuccess(BasePaginatedResult result) { - this.callback.onSuccess(new AsyncResultPage(result)); - } - - @Override - public void onError(ErrorInfo reason) { - callback.onError(reason); - } - } + /** + * Construct a PaginatedQuery + * + * @param http. the http instance + * @param path. the path of the resource being queried + * @param headers. headers to pass into the first and all relative queries + * @param params. params to pass into the initial query + * @param bodyHandler. handler to parse response bodies for first and all relative queries + */ + public BasePaginatedQuery(Http http, String path, Param[] headers, Param[] params, HttpCore.BodyHandler bodyHandler) { + this(http, path, headers, params, null, bodyHandler); + } + + /** + * Construct a PaginatedQuery + * + * @param http. the http instance + * @param path. the path of the resource being queried + * @param headers. headers to pass into the first and all relative queries + * @param params. params to pass into the initial query + * @param bodyHandler. handler to parse response bodies for first and all relative queries + */ + public BasePaginatedQuery(Http http, String path, Param[] headers, Param[] params, HttpCore.RequestBody requestBody, HttpCore.BodyHandler bodyHandler) { + this.http = http; + this.path = path; + this.requestHeaders = headers; + this.requestParams = params; + this.requestBody = requestBody; + this.bodyHandler = bodyHandler; + } + + /** + * Get the result of the first query + * @return A ResultRequest giving the first page of results + * together with any available links to related results pages. + */ + public ResultRequest get() { + return new ResultRequest(this.exec(HttpConstants.Methods.GET)); + } + + /** + * Get the result of the first query + * @return A Http.Request> giving the first page of results + * together with any available links to related results pages. + */ + public Http.Request> exec(final String method) { + final HttpCore.ResponseHandler> responseHandler = this; + return http.request(new Http.Execute>() { + @Override + public void execute(HttpScheduler http, Callback> callback) throws AblyException { + http.exec(path, method, requestHeaders, requestParams, requestBody, responseHandler, true, callback); + } + }); + } + + /** + * A private class encapsulating the result of a single page response + */ + private class ResultPage implements BasePaginatedResult { + private T[] contents; + + private ResultPage(T[] contents, Collection linkHeaders) throws AblyException { + this.contents = contents; + + if(linkHeaders != null) { + HashMap links = parseLinks(linkHeaders); + relFirst = links.get("first"); + relCurrent = links.get("current"); + relNext = links.get("next"); + } + } + + @Override + public T[] items() { return contents; } + + @Override + public Http.Request> first() { return getRel(relFirst); } + + @Override + public Http.Request> current() { return getRel(relCurrent); } + + @Override + public Http.Request> next() { return getRel(relNext); } + + private Http.Request> getRel(final String linkUrl) { + return http.request(new Http.Execute>() { + @Override + public void execute(HttpScheduler http, Callback> callback) throws AblyException { + if(linkUrl == null) { + callback.onSuccess(null); + return; + } + /* we're expecting the format to be ./path-component?name=value&name=value... */ + Matcher urlMatch = urlPattern.matcher(linkUrl); + if(!urlMatch.matches()) { + throw AblyException.fromErrorInfo(new ErrorInfo("Unexpected link URL format", 500, 50000)); + } + String[] paramSpecs = urlMatch.group(2).split("&"); + Param[] params = new Param[paramSpecs.length]; + try { + for(int i = 0; i < paramSpecs.length; i++) { + String[] split = paramSpecs[i].split("="); + params[i] = new Param(split[0], URLDecoder.decode(split[1], "UTF-8")); + } + } catch(UnsupportedEncodingException uee) {} + http.get(path, requestHeaders, params, BasePaginatedQuery.this, true, callback); + } + }); + } + + private String relFirst, relCurrent, relNext; + + @Override + public boolean hasFirst() { return relFirst != null; } + + @Override + public boolean hasCurrent() { return relCurrent != null; } + + @Override + public boolean hasNext() { return relNext != null; } + + @Override + public boolean isLast() { + return relNext == null; + } + } + + @Override + public BasePaginatedResult handleResponse(HttpCore.Response response, ErrorInfo error) throws AblyException { + if(error != null) { + throw AblyException.fromErrorInfo(error); + } + T[] responseContents = bodyHandler.handleResponseBody(response.contentType, response.body); + return new BasePaginatedQuery.ResultPage(responseContents, response.getHeaderFields(HttpConstants.Headers.LINK)); + } + + /**************** + * internal + ****************/ + + protected static Pattern linkPattern = Pattern.compile("\\s*<(.*)>;\\s*rel=\"(.*)\""); + protected static Pattern urlPattern = Pattern.compile("\\./(.*)\\?(.*)"); + + protected static HashMap parseLinks(Collection linkHeaders) { + HashMap result = new HashMap(); + for(String link : linkHeaders) { + /* we're expecting the format to be ; rel="first current ..." */ + Matcher linkMatch = linkPattern.matcher(link); + if(linkMatch.matches()) { + String linkUrl = linkMatch.group(1); + for (String linkRel : linkMatch.group(2).toLowerCase(Locale.ENGLISH).split("\\s")) + result.put(linkRel, linkUrl); + } + } + return result; + } + + private final Http http; + private final String path; + private final Param[] requestHeaders; + private final Param[] requestParams; + private final HttpCore.RequestBody requestBody; + private final HttpCore.BodyHandler bodyHandler; + + /** + * Wraps a Http.Request> to fixate on either a sync or an async interface. + * + * The Http.Request gives you a BasePaginatedResult whether you call its sync() or its async() + * methods, but we'd like to give it us a PaginatedResult or a AsyncPaginatedResult + * respectively, so this does the necessary bridging. + * + * @param + */ + public static class ResultRequest { + private final Http.Request> wrappedRequest; + + private ResultRequest(Http.Request> wrappedRequest) { + this.wrappedRequest = wrappedRequest; + } + + public PaginatedResult sync() throws AblyException { + return new SyncResultPage(wrappedRequest.sync()); + } + + public void async(final Callback> callback) { + wrappedRequest.async(new Callback>() { + @Override + public void onSuccess(BasePaginatedResult result) { + callback.onSuccess(new AsyncResultPage(result)); + } + + @Override + public void onError(ErrorInfo reason) { + callback.onError(reason); + } + }); + } + + /** + * A ResultRequest that has already failed due to a previous condition. + * + * Useful when a method must return a ResultRequest but fails before it can make the "real" + * one. Such errors are reported as thrown AblyExceptions in sync scenarios, and as + * Callback.onError calls in async ones. This class helps converting from plain exceptions + * to cover the async case too. + * + * @param + */ + public static class Failed extends ResultRequest { + private final AblyException reason; + + public Failed(AblyException reason) { + super(null); + this.reason = reason; + } + + @Override + public PaginatedResult sync() throws AblyException { + throw reason; + } + + @Override + public void async(Callback> callback) { + callback.onError(reason.errorInfo); + } + } + } + + /** + * Base class for SyncResultPage and AsyncResultPage. For code sharing purposes only. + * @param + */ + private static abstract class ResultPageWrapper { + protected final BasePaginatedResult resultBase; + + protected ResultPageWrapper(BasePaginatedResult resultBase) { + this.resultBase = resultBase; + } + + public T[] items() { return resultBase.items(); } + + public boolean hasFirst() { return resultBase.hasFirst(); } + + public boolean hasCurrent() { return resultBase.hasCurrent(); } + + public boolean hasNext() { return resultBase.hasNext(); } + + public boolean isLast() { + return resultBase.isLast(); + } + } + + /** + * Bridge from BasePaginatedResult to the synchronous PaginatedResult. + * @param + */ + private static class SyncResultPage extends ResultPageWrapper implements PaginatedResult { + SyncResultPage(BasePaginatedResult resultBase) { + super(resultBase); + } + + @Override + public PaginatedResult first() throws AblyException { return new SyncResultPage(resultBase.first().sync()); } + + @Override + public PaginatedResult current() throws AblyException { return new SyncResultPage(resultBase.current().sync()); } + + @Override + public PaginatedResult next() throws AblyException { return new SyncResultPage(resultBase.next().sync()); } + } + + /** + * Bridge from BasePaginatedResult to the asynchronous AsyncPaginatedResult. + * @param + */ + private static class AsyncResultPage extends ResultPageWrapper implements AsyncPaginatedResult { + AsyncResultPage(BasePaginatedResult resultBase) { + super(resultBase); + } + + @Override + public void first(Callback> callback) { + resultBase.first().async(new CallbackBridge(callback)); + } + + @Override + public void current(Callback> callback) { + resultBase.current().async(new CallbackBridge(callback)); + } + + @Override + public void next(Callback> callback) { + resultBase.next().async(new CallbackBridge(callback)); + } + } + + /** + * Bridge from Callback> to Callback>. + * + * @param + */ + private static class CallbackBridge implements Callback> { + private final Callback> callback; + + CallbackBridge(Callback> callback) { + this.callback = callback; + } + + @Override + public void onSuccess(BasePaginatedResult result) { + this.callback.onSuccess(new AsyncResultPage(result)); + } + + @Override + public void onError(ErrorInfo reason) { + callback.onError(reason); + } + } } 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 0606418ba..425fd27ae 100644 --- a/lib/src/main/java/io/ably/lib/http/Http.java +++ b/lib/src/main/java/io/ably/lib/http/Http.java @@ -8,74 +8,74 @@ * A high level wrapper of both a sync and an async HttpScheduler. */ public class Http { - private final AsyncHttpScheduler asyncHttp; - private final SyncHttpScheduler syncHttp; + private final AsyncHttpScheduler asyncHttp; + private final SyncHttpScheduler syncHttp; - public Http(AsyncHttpScheduler asyncHttp, SyncHttpScheduler syncHttp) { - this.asyncHttp = asyncHttp; - this.syncHttp = syncHttp; - } + public Http(AsyncHttpScheduler asyncHttp, SyncHttpScheduler syncHttp) { + this.asyncHttp = asyncHttp; + this.syncHttp = syncHttp; + } - public class Request { - private final Execute execute; + public class Request { + private final Execute execute; - Request(Execute execute) { - this.execute = execute; - } + Request(Execute execute) { + this.execute = execute; + } - public Result sync() throws AblyException { - final SyncExecuteResult result = new SyncExecuteResult<>(); - execute.execute(Http.this.syncHttp, new Callback() { - @Override - public void onSuccess(Result r) { - result.ok = r; - } + public Result sync() throws AblyException { + final SyncExecuteResult result = new SyncExecuteResult<>(); + execute.execute(Http.this.syncHttp, new Callback() { + @Override + public void onSuccess(Result r) { + result.ok = r; + } - @Override - public void onError(ErrorInfo e) { - result.error = e; - } - }); - if (result.error != null) { - throw AblyException.fromErrorInfo(result.error); - } - return result.ok; - } + @Override + public void onError(ErrorInfo e) { + result.error = e; + } + }); + if (result.error != null) { + throw AblyException.fromErrorInfo(result.error); + } + return result.ok; + } - public void async(Callback callback) { - try { - execute.execute(Http.this.asyncHttp, callback); - } catch (AblyException e) { - callback.onError(e.errorInfo); - } - } - } + public void async(Callback callback) { + try { + execute.execute(Http.this.asyncHttp, callback); + } catch (AblyException e) { + callback.onError(e.errorInfo); + } + } + } - public Request request(Execute execute) { - return new Request(execute); - } + public Request request(Execute execute) { + return new Request(execute); + } - public Request failedRequest(final AblyException e) { - return new Request(new Execute() { - @Override - public void execute(HttpScheduler http, final Callback callback) throws AblyException { - //throw e; - http.executor.execute(new Runnable() { - @Override - public void run() { - callback.onError(e.errorInfo); - } - }); - } - }); - } + public Request failedRequest(final AblyException e) { + return new Request(new Execute() { + @Override + public void execute(HttpScheduler http, final Callback callback) throws AblyException { + //throw e; + http.executor.execute(new Runnable() { + @Override + public void run() { + callback.onError(e.errorInfo); + } + }); + } + }); + } - public interface Execute { - void execute(HttpScheduler http, Callback callback) throws AblyException; - } + public interface Execute { + void execute(HttpScheduler http, Callback callback) throws AblyException; + } - private static class SyncExecuteResult { - public Result ok = null; - public ErrorInfo error = null; - } + private static class SyncExecuteResult { + public Result ok = null; + public ErrorInfo error = null; + } } diff --git a/lib/src/main/java/io/ably/lib/http/HttpAuth.java b/lib/src/main/java/io/ably/lib/http/HttpAuth.java index 943761a9a..ced500dcb 100644 --- a/lib/src/main/java/io/ably/lib/http/HttpAuth.java +++ b/lib/src/main/java/io/ably/lib/http/HttpAuth.java @@ -17,221 +17,221 @@ public class HttpAuth { - public enum Type { - BASIC, - DIGEST, - X_ABLY_TOKEN - } - - HttpAuth(String username, String password, Type prefType) { - this.username = username; - this.password = password; - this.prefType = prefType; - } - - boolean hasChallenge() { - return (type != null); - } - - /** - * Split a compound authenticate header string to get details for each auth type - * @param authenticateHeaders - * @return - * @throws AblyException - */ - public static Map sortAuthenticateHeaders(Collection authenticateHeaders) throws AblyException { - Map sortedHeaders = new HashMap<>(); - for(String header : authenticateHeaders) { - int delimiterIdx = header.indexOf(' '); - if(delimiterIdx == -1) { throw AblyException.fromErrorInfo(new ErrorInfo("Invalid authenticate header (no delimiter)", 40000, 400)); } - String authType = header.substring(0, delimiterIdx).trim(); - String authDetails = header.substring(delimiterIdx + 1).trim(); - sortedHeaders.put(Type.valueOf(authType.toUpperCase().replace('-', '_')), authDetails); - } - return sortedHeaders; - } - - /** - * Get authorization header based on the last-received server nonce. - * This increments nc, and generates a new cnonce - * @param method - * @param uri - * @param requestBody - * @return - * @throws AblyException - */ - public String getAuthorizationHeader(String method, String uri, byte[] requestBody) throws AblyException { - switch(type) { - case BASIC: - return "Basic " + Base64Coder.encodeString(username + ':' + password); - case DIGEST: - return getDigestHeader(method, uri, requestBody); - default: - return null; - } - } - - /** - * Process a challenge; this selects the auth type to use and caches all - * possible values based on the challenge in the case of digest auth - * @param authenticateHeaders - * @throws AblyException - */ - public void processAuthenticateHeaders(Map authenticateHeaders) throws AblyException { - String authDetails = authenticateHeaders.get(type = prefType); - if(authDetails == null) { - Entry firstEntry = authenticateHeaders.entrySet().iterator().next(); - if(firstEntry == null) { throw AblyException.fromErrorInfo(new ErrorInfo("Invalid authenticate header (no entries)", 40000, 400)); } - type = firstEntry.getKey(); - authDetails = firstEntry.getValue(); - } - if(type == Type.DIGEST) { - processDigestHeader(authDetails); - } - } - - /** - * For digest auth, process the challenge details - * @param detailsString - * @throws AblyException - */ - private synchronized void processDigestHeader(String detailsString) throws AblyException { - HashMap authFields = splitAuthFields(detailsString); - realm = authFields.get("realm"); - nonce = authFields.get("nonce"); - opaque = authFields.get("opaque"); - HA1 = digestString(username + ':' + realm + ':' + password); - - String qopStr = authFields.get("qop"); - if(qopStr != null) { - qops = qopStr.split(","); - } - } - - /** - * Get the Digest authorization header for a given request, based on already-processed challenge - * @param method - * @param uri - * @param requestBody - * @return - * @throws AblyException - */ - private String getDigestHeader(String method, String uri, byte[] requestBody) throws AblyException { - String qop = null; - if(qops != null) { - for(String candidateQop : qops) { - if(requestBody != null && candidateQop.trim().equals("auth-int")) { - qop = "auth-int"; - break; - } - if(candidateQop.trim().equals("auth")) { - qop = "auth"; - break; - } - } - } - - String HA2, HA3, nc = null, cnonce = null; - if(qop == null) { - HA2 = digestString(method + ':' + uri); - HA3 = digestString(HA1 + ':' + nonce + ':' + HA2); - } else if(qop.equals("auth")) { - nc = String.format("%08X", ncCounter++); - cnonce = getClientNonce(); - HA2 = digestString(method + ':' + uri); - HA3 = digestString(HA1 + ':' + nonce + ':' + nc + ':' + cnonce + ':' + qop + ':' + HA2); - } else { - nc = String.format("%08X", ncCounter++); - cnonce = getClientNonce(); - HA2 = digestString(method + ':' + uri + ':' + digestBytes(requestBody)); - HA3 = digestString(HA1 + ':' + nonce + ':' + nc + ':' + cnonce + ':' + qop + ':' + HA2); - } - - StringBuilder sb = new StringBuilder(128); - sb.append("Digest "); - sb.append("username" ).append("=\"").append(username).append("\","); - sb.append("realm" ).append("=\"").append(realm ).append("\","); - sb.append("nonce" ).append("=\"").append(nonce ).append("\","); - sb.append("uri" ).append("=\"").append(uri ).append("\","); - sb.append("algorithm" ).append("=\"").append("MD5" ).append("\","); - - if(qop != null) { - sb.append("qop" ).append("=\"").append(qop ).append("\","); - sb.append("nc" ).append("=" ).append(nc ).append(","); - sb.append("cnonce" ).append("=\"").append(cnonce ).append("\","); - } - - if(opaque != null) { - sb.append("response").append("=\"").append(HA3 ).append("\","); - sb.append("opaque" ).append("=\"").append(opaque ).append("\""); - } else { - sb.append("response").append("=\"").append(HA3 ).append("\""); - } - return sb.toString(); - } - - private static HashMap splitAuthFields(String detailsString) { - HashMap values = new HashMap(); - String keyValueArray[] = detailsString.split(","); - for (String keyval : keyValueArray) { - if (keyval.contains("=")) { - String key = keyval.substring(0, keyval.indexOf("=")); - String value = keyval.substring(keyval.indexOf("=") + 1); - values.put(key.trim(), value.replaceAll("\"", "").trim()); - } - } - return values; - } - - private static String digestBytes(byte[] buf) { - md5.reset(); - md5.update(buf); - byte[] ha1bytes = md5.digest(); - return bytesToHexString(ha1bytes); - } - - private static String digestString(String text) { - try{ - return digestBytes(text.getBytes("ISO-8859-1")); - } - catch(UnsupportedEncodingException e){ return null; } - } - - private static final String HEX_LOOKUP = "0123456789abcdef"; - private static String bytesToHexString(byte[] bytes) - { - StringBuilder sb = new StringBuilder(bytes.length * 2); - for(int i = 0; i < bytes.length; i++){ - sb.append(HEX_LOOKUP.charAt((bytes[i] & 0xF0) >> 4)); - sb.append(HEX_LOOKUP.charAt((bytes[i] & 0x0F) >> 0)); - } - return sb.toString(); - } - - private static String getClientNonce() { - String fmtDate = (new SimpleDateFormat("yyyy:MM:dd:hh:mm:ss")).format(new Date()); - Integer randomInt = (new Random(100000)).nextInt(); - return digestString(fmtDate + randomInt.toString()).substring(0, 8); - } - - private static MessageDigest md5 = null; - static { - try{ - md5 = MessageDigest.getInstance("MD5"); - } - catch(NoSuchAlgorithmException e) {} - } - - private String realm; - private String nonce; - private String[] qops; - private String opaque; - private Type type; - private int ncCounter = 1; - - private String HA1; - - private final String username; - private final String password; - private final Type prefType; + public enum Type { + BASIC, + DIGEST, + X_ABLY_TOKEN + } + + HttpAuth(String username, String password, Type prefType) { + this.username = username; + this.password = password; + this.prefType = prefType; + } + + boolean hasChallenge() { + return (type != null); + } + + /** + * Split a compound authenticate header string to get details for each auth type + * @param authenticateHeaders + * @return + * @throws AblyException + */ + public static Map sortAuthenticateHeaders(Collection authenticateHeaders) throws AblyException { + Map sortedHeaders = new HashMap<>(); + for(String header : authenticateHeaders) { + int delimiterIdx = header.indexOf(' '); + if(delimiterIdx == -1) { throw AblyException.fromErrorInfo(new ErrorInfo("Invalid authenticate header (no delimiter)", 40000, 400)); } + String authType = header.substring(0, delimiterIdx).trim(); + String authDetails = header.substring(delimiterIdx + 1).trim(); + sortedHeaders.put(Type.valueOf(authType.toUpperCase().replace('-', '_')), authDetails); + } + return sortedHeaders; + } + + /** + * Get authorization header based on the last-received server nonce. + * This increments nc, and generates a new cnonce + * @param method + * @param uri + * @param requestBody + * @return + * @throws AblyException + */ + public String getAuthorizationHeader(String method, String uri, byte[] requestBody) throws AblyException { + switch(type) { + case BASIC: + return "Basic " + Base64Coder.encodeString(username + ':' + password); + case DIGEST: + return getDigestHeader(method, uri, requestBody); + default: + return null; + } + } + + /** + * Process a challenge; this selects the auth type to use and caches all + * possible values based on the challenge in the case of digest auth + * @param authenticateHeaders + * @throws AblyException + */ + public void processAuthenticateHeaders(Map authenticateHeaders) throws AblyException { + String authDetails = authenticateHeaders.get(type = prefType); + if(authDetails == null) { + Entry firstEntry = authenticateHeaders.entrySet().iterator().next(); + if(firstEntry == null) { throw AblyException.fromErrorInfo(new ErrorInfo("Invalid authenticate header (no entries)", 40000, 400)); } + type = firstEntry.getKey(); + authDetails = firstEntry.getValue(); + } + if(type == Type.DIGEST) { + processDigestHeader(authDetails); + } + } + + /** + * For digest auth, process the challenge details + * @param detailsString + * @throws AblyException + */ + private synchronized void processDigestHeader(String detailsString) throws AblyException { + HashMap authFields = splitAuthFields(detailsString); + realm = authFields.get("realm"); + nonce = authFields.get("nonce"); + opaque = authFields.get("opaque"); + HA1 = digestString(username + ':' + realm + ':' + password); + + String qopStr = authFields.get("qop"); + if(qopStr != null) { + qops = qopStr.split(","); + } + } + + /** + * Get the Digest authorization header for a given request, based on already-processed challenge + * @param method + * @param uri + * @param requestBody + * @return + * @throws AblyException + */ + private String getDigestHeader(String method, String uri, byte[] requestBody) throws AblyException { + String qop = null; + if(qops != null) { + for(String candidateQop : qops) { + if(requestBody != null && candidateQop.trim().equals("auth-int")) { + qop = "auth-int"; + break; + } + if(candidateQop.trim().equals("auth")) { + qop = "auth"; + break; + } + } + } + + String HA2, HA3, nc = null, cnonce = null; + if(qop == null) { + HA2 = digestString(method + ':' + uri); + HA3 = digestString(HA1 + ':' + nonce + ':' + HA2); + } else if(qop.equals("auth")) { + nc = String.format("%08X", ncCounter++); + cnonce = getClientNonce(); + HA2 = digestString(method + ':' + uri); + HA3 = digestString(HA1 + ':' + nonce + ':' + nc + ':' + cnonce + ':' + qop + ':' + HA2); + } else { + nc = String.format("%08X", ncCounter++); + cnonce = getClientNonce(); + HA2 = digestString(method + ':' + uri + ':' + digestBytes(requestBody)); + HA3 = digestString(HA1 + ':' + nonce + ':' + nc + ':' + cnonce + ':' + qop + ':' + HA2); + } + + StringBuilder sb = new StringBuilder(128); + sb.append("Digest "); + sb.append("username" ).append("=\"").append(username).append("\","); + sb.append("realm" ).append("=\"").append(realm ).append("\","); + sb.append("nonce" ).append("=\"").append(nonce ).append("\","); + sb.append("uri" ).append("=\"").append(uri ).append("\","); + sb.append("algorithm" ).append("=\"").append("MD5" ).append("\","); + + if(qop != null) { + sb.append("qop" ).append("=\"").append(qop ).append("\","); + sb.append("nc" ).append("=" ).append(nc ).append(","); + sb.append("cnonce" ).append("=\"").append(cnonce ).append("\","); + } + + if(opaque != null) { + sb.append("response").append("=\"").append(HA3 ).append("\","); + sb.append("opaque" ).append("=\"").append(opaque ).append("\""); + } else { + sb.append("response").append("=\"").append(HA3 ).append("\""); + } + return sb.toString(); + } + + private static HashMap splitAuthFields(String detailsString) { + HashMap values = new HashMap(); + String keyValueArray[] = detailsString.split(","); + for (String keyval : keyValueArray) { + if (keyval.contains("=")) { + String key = keyval.substring(0, keyval.indexOf("=")); + String value = keyval.substring(keyval.indexOf("=") + 1); + values.put(key.trim(), value.replaceAll("\"", "").trim()); + } + } + return values; + } + + private static String digestBytes(byte[] buf) { + md5.reset(); + md5.update(buf); + byte[] ha1bytes = md5.digest(); + return bytesToHexString(ha1bytes); + } + + private static String digestString(String text) { + try{ + return digestBytes(text.getBytes("ISO-8859-1")); + } + catch(UnsupportedEncodingException e){ return null; } + } + + private static final String HEX_LOOKUP = "0123456789abcdef"; + private static String bytesToHexString(byte[] bytes) + { + StringBuilder sb = new StringBuilder(bytes.length * 2); + for(int i = 0; i < bytes.length; i++){ + sb.append(HEX_LOOKUP.charAt((bytes[i] & 0xF0) >> 4)); + sb.append(HEX_LOOKUP.charAt((bytes[i] & 0x0F) >> 0)); + } + return sb.toString(); + } + + private static String getClientNonce() { + String fmtDate = (new SimpleDateFormat("yyyy:MM:dd:hh:mm:ss")).format(new Date()); + Integer randomInt = (new Random(100000)).nextInt(); + return digestString(fmtDate + randomInt.toString()).substring(0, 8); + } + + private static MessageDigest md5 = null; + static { + try{ + md5 = MessageDigest.getInstance("MD5"); + } + catch(NoSuchAlgorithmException e) {} + } + + private String realm; + private String nonce; + private String[] qops; + private String opaque; + private Type type; + private int ncCounter = 1; + + private String HA1; + + private final String username; + private final String password; + private final Type prefType; } diff --git a/lib/src/main/java/io/ably/lib/http/HttpConstants.java b/lib/src/main/java/io/ably/lib/http/HttpConstants.java index c3cca61ad..7e9c04922 100644 --- a/lib/src/main/java/io/ably/lib/http/HttpConstants.java +++ b/lib/src/main/java/io/ably/lib/http/HttpConstants.java @@ -2,27 +2,27 @@ public class HttpConstants { - public static class ContentTypes { - public static final String JSON = "application/json"; - public static final String FORM_ENCODING = "application/x-www-form-urlencoded"; - } + public static class ContentTypes { + public static final String JSON = "application/json"; + public static final String FORM_ENCODING = "application/x-www-form-urlencoded"; + } - public static class Headers { - public static final String CONTENT_LENGTH = "Content-Length"; - public static final String ACCEPT = "Accept"; - public static final String CONTENT_TYPE = "Content-Type"; - public static final String WWW_AUTHENTICATE = "WWW-Authenticate"; - public static final String PROXY_AUTHENTICATE = "Proxy-Authenticate"; - public static final String AUTHORIZATION = "Authorization"; - public static final String PROXY_AUTHORIZATION = "Proxy-Authorization"; - public static final String LINK = "Link"; - } + public static class Headers { + public static final String CONTENT_LENGTH = "Content-Length"; + public static final String ACCEPT = "Accept"; + public static final String CONTENT_TYPE = "Content-Type"; + public static final String WWW_AUTHENTICATE = "WWW-Authenticate"; + public static final String PROXY_AUTHENTICATE = "Proxy-Authenticate"; + public static final String AUTHORIZATION = "Authorization"; + public static final String PROXY_AUTHORIZATION = "Proxy-Authorization"; + public static final String LINK = "Link"; + } - public static class Methods { - public static final String GET = "GET"; - public static final String PUT = "PUT"; - public static final String POST = "POST"; - public static final String DELETE = "DELETE"; - public static final String PATCH = "PATCH"; - } + public static class Methods { + public static final String GET = "GET"; + public static final String PUT = "PUT"; + public static final String POST = "POST"; + public static final String DELETE = "DELETE"; + public static final String PATCH = "PATCH"; + } } 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 bdea82c72..95ab6e580 100644 --- a/lib/src/main/java/io/ably/lib/http/HttpCore.java +++ b/lib/src/main/java/io/ably/lib/http/HttpCore.java @@ -27,558 +27,558 @@ */ public class HttpCore { - /************************* - * Public API - *************************/ - - public HttpCore(ClientOptions options, Auth auth) throws AblyException { - this.options = options; - this.auth = auth; - this.scheme = options.tls ? "https://" : "http://"; - this.port = Defaults.getPort(options); - this.hosts = new Hosts(options.restHost, Defaults.HOST_REST, options); - - this.proxyOptions = options.proxy; - if(proxyOptions != null) { - String proxyHost = proxyOptions.host; - if(proxyHost == null) { throw AblyException.fromErrorInfo(new ErrorInfo("Unable to configure proxy without proxy host", 40000, 400)); } - int proxyPort = proxyOptions.port; - if(proxyPort == 0) { throw AblyException.fromErrorInfo(new ErrorInfo("Unable to configure proxy without proxy port", 40000, 400)); } - this.proxy = new Proxy(Proxy.Type.HTTP, new InetSocketAddress(proxyHost, proxyPort)); - String proxyUser = proxyOptions.username; - if(proxyUser != null) { - String proxyPassword = proxyOptions.password; - if(proxyPassword == null) { throw AblyException.fromErrorInfo(new ErrorInfo("Unable to configure proxy without proxy password", 40000, 400)); } - proxyAuth = new HttpAuth(proxyUser, proxyPassword, proxyOptions.prefAuthType); - } - } - } - - /** - * Make a synchronous HTTP request specified by URL and proxy, retrying if necessary on WWW-Authenticate - * @param url - * @param method - * @param headers - * @param requestBody - * @param responseHandler - * @return - * @throws AblyException - */ - public T httpExecuteWithRetry(URL url, String method, Param[] headers, RequestBody requestBody, ResponseHandler responseHandler, boolean requireAblyAuth) throws AblyException { - boolean renewPending = true, proxyAuthPending = true; - if(requireAblyAuth) { - authorize(false); - } - while(true) { - try { - return httpExecute(url, getProxy(url), method, headers, requestBody, true, responseHandler); - } catch(AuthRequiredException are) { - if(are.authChallenge != null && requireAblyAuth) { - if(are.expired && renewPending) { - authorize(true); - renewPending = false; - continue; - } - } - if(are.proxyAuthChallenge != null && proxyAuthPending && proxyAuth != null) { - proxyAuth.processAuthenticateHeaders(are.proxyAuthChallenge); - proxyAuthPending = false; - continue; - } - throw are; - } - } - } - - /** - * Sets host for this HTTP client - * - * @param host URL string - */ - public void setPreferredHost(String host) { - hosts.setPreferredHost(host, false); - } - - /** - * Gets host for this HTTP client - * - * @return + /************************* + * Public API + *************************/ + + public HttpCore(ClientOptions options, Auth auth) throws AblyException { + this.options = options; + this.auth = auth; + this.scheme = options.tls ? "https://" : "http://"; + this.port = Defaults.getPort(options); + this.hosts = new Hosts(options.restHost, Defaults.HOST_REST, options); + + this.proxyOptions = options.proxy; + if(proxyOptions != null) { + String proxyHost = proxyOptions.host; + if(proxyHost == null) { throw AblyException.fromErrorInfo(new ErrorInfo("Unable to configure proxy without proxy host", 40000, 400)); } + int proxyPort = proxyOptions.port; + if(proxyPort == 0) { throw AblyException.fromErrorInfo(new ErrorInfo("Unable to configure proxy without proxy port", 40000, 400)); } + this.proxy = new Proxy(Proxy.Type.HTTP, new InetSocketAddress(proxyHost, proxyPort)); + String proxyUser = proxyOptions.username; + if(proxyUser != null) { + String proxyPassword = proxyOptions.password; + if(proxyPassword == null) { throw AblyException.fromErrorInfo(new ErrorInfo("Unable to configure proxy without proxy password", 40000, 400)); } + proxyAuth = new HttpAuth(proxyUser, proxyPassword, proxyOptions.prefAuthType); + } + } + } + + /** + * Make a synchronous HTTP request specified by URL and proxy, retrying if necessary on WWW-Authenticate + * @param url + * @param method + * @param headers + * @param requestBody + * @param responseHandler + * @return + * @throws AblyException + */ + public T httpExecuteWithRetry(URL url, String method, Param[] headers, RequestBody requestBody, ResponseHandler responseHandler, boolean requireAblyAuth) throws AblyException { + boolean renewPending = true, proxyAuthPending = true; + if(requireAblyAuth) { + authorize(false); + } + while(true) { + try { + return httpExecute(url, getProxy(url), method, headers, requestBody, true, responseHandler); + } catch(AuthRequiredException are) { + if(are.authChallenge != null && requireAblyAuth) { + if(are.expired && renewPending) { + authorize(true); + renewPending = false; + continue; + } + } + if(are.proxyAuthChallenge != null && proxyAuthPending && proxyAuth != null) { + proxyAuth.processAuthenticateHeaders(are.proxyAuthChallenge); + proxyAuthPending = false; + continue; + } + throw are; + } + } + } + + /** + * Sets host for this HTTP client + * + * @param host URL string + */ + public void setPreferredHost(String host) { + hosts.setPreferredHost(host, false); + } + + /** + * Gets host for this HTTP client + * + * @return + + */ + public String getPreferredHost() { + return hosts.getPreferredHost(); + } + + /** + * Gets host for this HTTP client + * + * @return + */ + public String getPrimaryHost() { + return hosts.getPrimaryHost(); + } + + /************************** + * Internal API + **************************/ + + void authorize(boolean renew) throws AblyException { + auth.assertAuthorizationHeader(renew); + } + + synchronized void dispose() { + if(!isDisposed) { + isDisposed = true; + } + } + + public void finalize() { + dispose(); + } + + /** + * Make a synchronous HTTP request specified by URL and proxy + * @param url + * @param proxy + * @param method + * @param headers + * @param requestBody + * @param withCredentials + * @param responseHandler + * @return + * @throws AblyException + */ + public T httpExecute(URL url, Proxy proxy, String method, Param[] headers, RequestBody requestBody, boolean withCredentials, ResponseHandler responseHandler) throws AblyException { + HttpURLConnection conn = null; + try { + conn = (HttpURLConnection)url.openConnection(proxy); + boolean withProxyCredentials = (proxy != Proxy.NO_PROXY) && (proxyAuth != null); + return httpExecute(conn, method, headers, requestBody, withCredentials, withProxyCredentials, responseHandler); + } catch(IOException ioe) { + throw AblyException.fromThrowable(ioe); + } finally { + if(conn != null) { + conn.disconnect(); + } + } + } + + /** + * Make a synchronous HTTP request with a given HttpURLConnection + * @param conn + * @param method + * @param headers + * @param requestBody + * @param withCredentials + * @param responseHandler + * @return + * @throws AblyException + */ + T httpExecute(HttpURLConnection conn, String method, Param[] headers, RequestBody requestBody, boolean withCredentials, boolean withProxyCredentials, ResponseHandler responseHandler) throws AblyException { + Response response; + boolean credentialsIncluded = false; + RawHttpListener rawHttpListener = null; + String id = null; + try { + /* prepare connection */ + conn.setRequestMethod(method); + conn.setConnectTimeout(options.httpOpenTimeout); + conn.setReadTimeout(options.httpRequestTimeout); + conn.setDoInput(true); + + String authHeader = Param.getFirst(headers, HttpConstants.Headers.AUTHORIZATION); + if (authHeader == null && auth != null) { + authHeader = auth.getAuthorizationHeader(); + } + if(withCredentials && authHeader != null) { + conn.setRequestProperty(HttpConstants.Headers.AUTHORIZATION, authHeader); + credentialsIncluded = true; + } + if(withProxyCredentials && proxyAuth.hasChallenge()) { + byte[] encodedRequestBody = (requestBody != null) ? requestBody.getEncoded() : null; + String proxyAuthorizationHeader = proxyAuth.getAuthorizationHeader(method, conn.getURL().getPath(), encodedRequestBody); + conn.setRequestProperty(HttpConstants.Headers.PROXY_AUTHORIZATION, proxyAuthorizationHeader); + } + boolean acceptSet = false; + if(headers != null) { + for(Param header: headers) { + conn.setRequestProperty(header.key, header.value); + if(header.key.equals(HttpConstants.Headers.ACCEPT)) { acceptSet = true; } + } + } + if(!acceptSet) { conn.setRequestProperty(HttpConstants.Headers.ACCEPT, HttpConstants.ContentTypes.JSON); } + + /* pass required headers */ + conn.setRequestProperty(Defaults.ABLY_VERSION_HEADER, Defaults.ABLY_VERSION); + conn.setRequestProperty(Defaults.ABLY_LIB_HEADER, Defaults.ABLY_LIB_VERSION); + + /* prepare request body */ + byte[] body = null; + if(requestBody != null) { + body = prepareRequestBody(requestBody, conn); + if (Log.level <= Log.VERBOSE) + Log.v(TAG, System.lineSeparator() + new String(body)); + } + + /* log raw request details */ + Map> requestProperties = conn.getRequestProperties(); + if (Log.level <= Log.VERBOSE) { + Log.v(TAG, "HTTP request: " + conn.getURL() + " " + method); + if (credentialsIncluded) + Log.v(TAG, " " + HttpConstants.Headers.AUTHORIZATION + ": " + authHeader); + for (Map.Entry> entry : requestProperties.entrySet()) + for (String val : entry.getValue()) + Log.v(TAG, " " + entry.getKey() + ": " + val); + } + + if(options instanceof DebugOptions) { + rawHttpListener = ((DebugOptions)options).httpListener; + if(rawHttpListener != null) { + id = String.valueOf(Math.random()).substring(2); + response = rawHttpListener.onRawHttpRequest(id, conn, method, (credentialsIncluded ? authHeader : null), requestProperties, requestBody); + if (response != null) { + return handleResponse(conn, credentialsIncluded, response, responseHandler); + } + } + } + + /* send request body */ + if(requestBody != null) { + writeRequestBody(body, conn); + } + response = readResponse(conn); + if(rawHttpListener != null) { + rawHttpListener.onRawHttpResponse(id, method, response); + } + } catch(IOException ioe) { + ioe.printStackTrace(); + if(rawHttpListener != null) { + rawHttpListener.onRawHttpException(id, method, ioe); + } + throw AblyException.fromThrowable(ioe); + } + + return handleResponse(conn, credentialsIncluded, response, responseHandler); + } + + /** + * Handle HTTP response + * @param conn + * @param credentialsIncluded + * @param response + * @param responseHandler + * @return + * @throws AblyException + */ + private T handleResponse(HttpURLConnection conn, boolean credentialsIncluded, Response response, ResponseHandler responseHandler) throws AblyException { + if (response.statusCode == 0) { + return null; + } + + if (response.statusCode >=500 && response.statusCode <= 504) { + ErrorInfo error = ErrorInfo.fromResponseStatus(response.statusLine, response.statusCode); + throw AblyException.fromErrorInfo(error); + } + + if(response.statusCode >= 200 && response.statusCode < 300) { + return (responseHandler != null) ? responseHandler.handleResponse(response, null) : null; + } + + /* get any in-body error details */ + ErrorInfo error = null; + if(response.body != null && response.body.length > 0) { + if(response.contentType != null && response.contentType.contains("msgpack")) { + try { + error = ErrorInfo.fromMsgpackBody(response.body); + } catch (IOException e) { + /* error pages aren't necessarily going to satisfy our Accept criteria ... */ + System.err.println("Unable to parse msgpack error response"); + } + } else { + /* assume json */ + String bodyText = new String(response.body); + try { + ErrorResponse errorResponse = ErrorResponse.fromJSON(bodyText); + if(errorResponse != null) { + error = errorResponse.error; + } + } catch(JsonParseException jse) { + /* error pages aren't necessarily going to satisfy our Accept criteria ... */ + System.err.println("Error message in unexpected format: " + bodyText); + } + } + } + + /* handle error details in header */ + if(error == null) { + String errorCodeHeader = conn.getHeaderField("X-Ably-ErrorCode"); + String errorMessageHeader = conn.getHeaderField("X-Ably-ErrorMessage"); + if(errorCodeHeader != null) { + try { + error = new ErrorInfo(errorMessageHeader, response.statusCode, Integer.parseInt(errorCodeHeader)); + } catch(NumberFormatException e) {} + } + } + + /* handle www-authenticate */ + if(response.statusCode == 401) { + boolean stale = (error != null && error.code == 40140); + List wwwAuthHeaders = response.getHeaderFields(HttpConstants.Headers.WWW_AUTHENTICATE); + if(wwwAuthHeaders != null && wwwAuthHeaders.size() > 0) { + Map headersByType = HttpAuth.sortAuthenticateHeaders(wwwAuthHeaders); + String tokenHeader = headersByType.get(HttpAuth.Type.X_ABLY_TOKEN); + if(tokenHeader != null) { stale |= (tokenHeader.indexOf("stale") > -1); } + AuthRequiredException exception = new AuthRequiredException(null, error); + exception.authChallenge = headersByType; + if(stale) { + exception.expired = true; + throw exception; + } + if(!credentialsIncluded) { + throw exception; + } + } + } + /* handle proxy-authenticate */ + if(response.statusCode == 407) { + List proxyAuthHeaders = response.getHeaderFields(HttpConstants.Headers.PROXY_AUTHENTICATE); + if(proxyAuthHeaders != null && proxyAuthHeaders.size() > 0) { + AuthRequiredException exception = new AuthRequiredException(null, error); + exception.proxyAuthChallenge = HttpAuth.sortAuthenticateHeaders(proxyAuthHeaders); + throw exception; + } + } + if(error == null) { + error = ErrorInfo.fromResponseStatus(response.statusLine, response.statusCode); + } else { + } + Log.e(TAG, "Error response from server: err = " + error.toString()); + if(responseHandler != null) { + return responseHandler.handleResponse(response, error); + } + throw AblyException.fromErrorInfo(error); + } + + /** + * Emit the request body for an HTTP request + * @param requestBody + * @param conn + * @return body + * @throws IOException + */ + private byte[] prepareRequestBody(RequestBody requestBody, HttpURLConnection conn) throws IOException { + conn.setDoOutput(true); + byte[] body = requestBody.getEncoded(); + int length = body.length; + conn.setFixedLengthStreamingMode(length); + conn.setRequestProperty(HttpConstants.Headers.CONTENT_TYPE, requestBody.getContentType()); + conn.setRequestProperty(HttpConstants.Headers.CONTENT_LENGTH, Integer.toString(length)); + return body; + } + + private void writeRequestBody(byte[] body, HttpURLConnection conn) throws IOException { + OutputStream os = conn.getOutputStream(); + os.write(body); + } + + /** + * Read the response for an HTTP request + * @param connection + * @return + * @throws IOException + */ + private Response readResponse(HttpURLConnection connection) throws IOException { + Response response = new Response(); + response.statusCode = connection.getResponseCode(); + response.statusLine = connection.getResponseMessage(); + + /* Store all header field names in lower-case to eliminate case insensitivity */ + Log.v(TAG, "HTTP response:"); + Map> caseSensitiveHeaders = connection.getHeaderFields(); + response.headers = new HashMap<>(caseSensitiveHeaders.size(), 1f); + + for (Map.Entry> entry : caseSensitiveHeaders.entrySet()) { + if (entry.getKey() != null) { + response.headers.put(entry.getKey().toLowerCase(), entry.getValue()); + if (Log.level <= Log.VERBOSE) + for (String val : entry.getValue()) + Log.v(TAG, entry.getKey() + ": " + val); + } + } + + if(response.statusCode == HttpURLConnection.HTTP_NO_CONTENT) { + return response; + } + + response.contentType = connection.getContentType(); + response.contentLength = connection.getContentLength(); + + InputStream is = null; + try { + is = connection.getInputStream(); + } catch (Throwable e) {} + if (is == null) + is = connection.getErrorStream(); + + try { + response.body = readInputStream(is, response.contentLength); + Log.v(TAG, System.lineSeparator() + new String(response.body)); + } catch (NullPointerException e) { + /* nothing to read */ + } finally { + if (is != null) { + try { + is.close(); + } catch (IOException e) {} + } + } + + return response; + } + + private byte[] readInputStream(InputStream inputStream, int bytes) throws IOException { + /* If there is nothing to read */ + if (inputStream == null) { + throw new NullPointerException("inputStream == null"); + } + + int bytesRead = 0; + + if (bytes == -1) { + ByteArrayOutputStream outputStream = new ByteArrayOutputStream(); + byte[] buffer = new byte[4 * 1024]; + while((bytesRead = inputStream.read(buffer)) > -1) { + outputStream.write(buffer, 0, bytesRead); + } + + return outputStream.toByteArray(); + } + else { + int idx = 0; + byte[] output = new byte[bytes]; + while((bytesRead = inputStream.read(output, idx, bytes - idx)) > -1) { + idx += bytesRead; + } + + return output; + } + } + + Proxy getProxy(URL url) { + String host = url.getHost(); + return getProxy(host); + } + + private Proxy getProxy(String host) { + if(proxyOptions != null) { + String[] nonProxyHosts = proxyOptions.nonProxyHosts; + if(nonProxyHosts != null) { + for(String nonProxyHostPattern : nonProxyHosts) { + if(host.matches(nonProxyHostPattern)) { + return null; + } + } + } + } + return proxy; + } + + /************************* + * Private state + *************************/ + + static { + /* if on Android, check version */ + Field androidVersionField = null; + int androidVersion = 0; + try { + androidVersionField = Class.forName("android.os.Build$VERSION").getField("SDK_INT"); + androidVersion = androidVersionField.getInt(androidVersionField); + } catch (Exception e) {} + if(androidVersionField != null && androidVersion < 8) { + /* HTTP connection reuse which was buggy pre-froyo */ + System.setProperty("httpCore.keepAlive", "false"); + } + } + + public final String scheme; + public final int port; + final ClientOptions options; + final Hosts hosts; + + private final Auth auth; + private final ProxyOptions proxyOptions; + private HttpAuth proxyAuth; + private Proxy proxy = Proxy.NO_PROXY; + private boolean isDisposed; + + private static final String TAG = HttpCore.class.getName(); + + /** + * Interface for an entity that supplies an httpCore request body + */ + public interface RequestBody { + byte[] getEncoded(); + String getContentType(); + } + + /** + * Interface for an entity that performs type-specific processing on an httpCore response body + * @param + */ + public interface BodyHandler { + T[] handleResponseBody(String contentType, byte[] body) throws AblyException; + } + + /** + * Interface for an entity that performs type-specific processing on an httpCore response + * @param + */ + public interface ResponseHandler { + T handleResponse(Response response, ErrorInfo error) throws AblyException; + } + /** + * A type encapsulating an httpCore response + */ + public static class Response { + public int statusCode; + public String statusLine; + public Map> headers; + public String contentType; + public int contentLength; + public byte[] body; + + /** + * Returns the value of the named header field. + *

+ * If called on a connection that sets the same header multiple times + * with possibly different values, only the last value is returned. + * + * + * @param name the name of a header field. + * @return the value of the named header field, or {@code null} + * if there is no such field in the header. + */ + public List getHeaderFields(String name) { + if(headers == null) { + return null; + } + + return headers.get(name.toLowerCase()); + } + } + + /** + * Exception signifying that an httpCore request failed with a WWW-Authenticate response */ - public String getPreferredHost() { - return hosts.getPreferredHost(); - } - - /** - * Gets host for this HTTP client - * - * @return - */ - public String getPrimaryHost() { - return hosts.getPrimaryHost(); - } - - /************************** - * Internal API - **************************/ - - void authorize(boolean renew) throws AblyException { - auth.assertAuthorizationHeader(renew); - } - - synchronized void dispose() { - if(!isDisposed) { - isDisposed = true; - } - } - - public void finalize() { - dispose(); - } - - /** - * Make a synchronous HTTP request specified by URL and proxy - * @param url - * @param proxy - * @param method - * @param headers - * @param requestBody - * @param withCredentials - * @param responseHandler - * @return - * @throws AblyException - */ - public T httpExecute(URL url, Proxy proxy, String method, Param[] headers, RequestBody requestBody, boolean withCredentials, ResponseHandler responseHandler) throws AblyException { - HttpURLConnection conn = null; - try { - conn = (HttpURLConnection)url.openConnection(proxy); - boolean withProxyCredentials = (proxy != Proxy.NO_PROXY) && (proxyAuth != null); - return httpExecute(conn, method, headers, requestBody, withCredentials, withProxyCredentials, responseHandler); - } catch(IOException ioe) { - throw AblyException.fromThrowable(ioe); - } finally { - if(conn != null) { - conn.disconnect(); - } - } - } - - /** - * Make a synchronous HTTP request with a given HttpURLConnection - * @param conn - * @param method - * @param headers - * @param requestBody - * @param withCredentials - * @param responseHandler - * @return - * @throws AblyException - */ - T httpExecute(HttpURLConnection conn, String method, Param[] headers, RequestBody requestBody, boolean withCredentials, boolean withProxyCredentials, ResponseHandler responseHandler) throws AblyException { - Response response; - boolean credentialsIncluded = false; - RawHttpListener rawHttpListener = null; - String id = null; - try { - /* prepare connection */ - conn.setRequestMethod(method); - conn.setConnectTimeout(options.httpOpenTimeout); - conn.setReadTimeout(options.httpRequestTimeout); - conn.setDoInput(true); - - String authHeader = Param.getFirst(headers, HttpConstants.Headers.AUTHORIZATION); - if (authHeader == null && auth != null) { - authHeader = auth.getAuthorizationHeader(); - } - if(withCredentials && authHeader != null) { - conn.setRequestProperty(HttpConstants.Headers.AUTHORIZATION, authHeader); - credentialsIncluded = true; - } - if(withProxyCredentials && proxyAuth.hasChallenge()) { - byte[] encodedRequestBody = (requestBody != null) ? requestBody.getEncoded() : null; - String proxyAuthorizationHeader = proxyAuth.getAuthorizationHeader(method, conn.getURL().getPath(), encodedRequestBody); - conn.setRequestProperty(HttpConstants.Headers.PROXY_AUTHORIZATION, proxyAuthorizationHeader); - } - boolean acceptSet = false; - if(headers != null) { - for(Param header: headers) { - conn.setRequestProperty(header.key, header.value); - if(header.key.equals(HttpConstants.Headers.ACCEPT)) { acceptSet = true; } - } - } - if(!acceptSet) { conn.setRequestProperty(HttpConstants.Headers.ACCEPT, HttpConstants.ContentTypes.JSON); } - - /* pass required headers */ - conn.setRequestProperty(Defaults.ABLY_VERSION_HEADER, Defaults.ABLY_VERSION); - conn.setRequestProperty(Defaults.ABLY_LIB_HEADER, Defaults.ABLY_LIB_VERSION); - - /* prepare request body */ - byte[] body = null; - if(requestBody != null) { - body = prepareRequestBody(requestBody, conn); - if (Log.level <= Log.VERBOSE) - Log.v(TAG, System.lineSeparator() + new String(body)); - } - - /* log raw request details */ - Map> requestProperties = conn.getRequestProperties(); - if (Log.level <= Log.VERBOSE) { - Log.v(TAG, "HTTP request: " + conn.getURL() + " " + method); - if (credentialsIncluded) - Log.v(TAG, " " + HttpConstants.Headers.AUTHORIZATION + ": " + authHeader); - for (Map.Entry> entry : requestProperties.entrySet()) - for (String val : entry.getValue()) - Log.v(TAG, " " + entry.getKey() + ": " + val); - } - - if(options instanceof DebugOptions) { - rawHttpListener = ((DebugOptions)options).httpListener; - if(rawHttpListener != null) { - id = String.valueOf(Math.random()).substring(2); - response = rawHttpListener.onRawHttpRequest(id, conn, method, (credentialsIncluded ? authHeader : null), requestProperties, requestBody); - if (response != null) { - return handleResponse(conn, credentialsIncluded, response, responseHandler); - } - } - } - - /* send request body */ - if(requestBody != null) { - writeRequestBody(body, conn); - } - response = readResponse(conn); - if(rawHttpListener != null) { - rawHttpListener.onRawHttpResponse(id, method, response); - } - } catch(IOException ioe) { - ioe.printStackTrace(); - if(rawHttpListener != null) { - rawHttpListener.onRawHttpException(id, method, ioe); - } - throw AblyException.fromThrowable(ioe); - } - - return handleResponse(conn, credentialsIncluded, response, responseHandler); - } - - /** - * Handle HTTP response - * @param conn - * @param credentialsIncluded - * @param response - * @param responseHandler - * @return - * @throws AblyException - */ - private T handleResponse(HttpURLConnection conn, boolean credentialsIncluded, Response response, ResponseHandler responseHandler) throws AblyException { - if (response.statusCode == 0) { - return null; - } - - if (response.statusCode >=500 && response.statusCode <= 504) { - ErrorInfo error = ErrorInfo.fromResponseStatus(response.statusLine, response.statusCode); - throw AblyException.fromErrorInfo(error); - } - - if(response.statusCode >= 200 && response.statusCode < 300) { - return (responseHandler != null) ? responseHandler.handleResponse(response, null) : null; - } - - /* get any in-body error details */ - ErrorInfo error = null; - if(response.body != null && response.body.length > 0) { - if(response.contentType != null && response.contentType.contains("msgpack")) { - try { - error = ErrorInfo.fromMsgpackBody(response.body); - } catch (IOException e) { - /* error pages aren't necessarily going to satisfy our Accept criteria ... */ - System.err.println("Unable to parse msgpack error response"); - } - } else { - /* assume json */ - String bodyText = new String(response.body); - try { - ErrorResponse errorResponse = ErrorResponse.fromJSON(bodyText); - if(errorResponse != null) { - error = errorResponse.error; - } - } catch(JsonParseException jse) { - /* error pages aren't necessarily going to satisfy our Accept criteria ... */ - System.err.println("Error message in unexpected format: " + bodyText); - } - } - } - - /* handle error details in header */ - if(error == null) { - String errorCodeHeader = conn.getHeaderField("X-Ably-ErrorCode"); - String errorMessageHeader = conn.getHeaderField("X-Ably-ErrorMessage"); - if(errorCodeHeader != null) { - try { - error = new ErrorInfo(errorMessageHeader, response.statusCode, Integer.parseInt(errorCodeHeader)); - } catch(NumberFormatException e) {} - } - } - - /* handle www-authenticate */ - if(response.statusCode == 401) { - boolean stale = (error != null && error.code == 40140); - List wwwAuthHeaders = response.getHeaderFields(HttpConstants.Headers.WWW_AUTHENTICATE); - if(wwwAuthHeaders != null && wwwAuthHeaders.size() > 0) { - Map headersByType = HttpAuth.sortAuthenticateHeaders(wwwAuthHeaders); - String tokenHeader = headersByType.get(HttpAuth.Type.X_ABLY_TOKEN); - if(tokenHeader != null) { stale |= (tokenHeader.indexOf("stale") > -1); } - AuthRequiredException exception = new AuthRequiredException(null, error); - exception.authChallenge = headersByType; - if(stale) { - exception.expired = true; - throw exception; - } - if(!credentialsIncluded) { - throw exception; - } - } - } - /* handle proxy-authenticate */ - if(response.statusCode == 407) { - List proxyAuthHeaders = response.getHeaderFields(HttpConstants.Headers.PROXY_AUTHENTICATE); - if(proxyAuthHeaders != null && proxyAuthHeaders.size() > 0) { - AuthRequiredException exception = new AuthRequiredException(null, error); - exception.proxyAuthChallenge = HttpAuth.sortAuthenticateHeaders(proxyAuthHeaders); - throw exception; - } - } - if(error == null) { - error = ErrorInfo.fromResponseStatus(response.statusLine, response.statusCode); - } else { - } - Log.e(TAG, "Error response from server: err = " + error.toString()); - if(responseHandler != null) { - return responseHandler.handleResponse(response, error); - } - throw AblyException.fromErrorInfo(error); - } - - /** - * Emit the request body for an HTTP request - * @param requestBody - * @param conn - * @return body - * @throws IOException - */ - private byte[] prepareRequestBody(RequestBody requestBody, HttpURLConnection conn) throws IOException { - conn.setDoOutput(true); - byte[] body = requestBody.getEncoded(); - int length = body.length; - conn.setFixedLengthStreamingMode(length); - conn.setRequestProperty(HttpConstants.Headers.CONTENT_TYPE, requestBody.getContentType()); - conn.setRequestProperty(HttpConstants.Headers.CONTENT_LENGTH, Integer.toString(length)); - return body; - } - - private void writeRequestBody(byte[] body, HttpURLConnection conn) throws IOException { - OutputStream os = conn.getOutputStream(); - os.write(body); - } - - /** - * Read the response for an HTTP request - * @param connection - * @return - * @throws IOException - */ - private Response readResponse(HttpURLConnection connection) throws IOException { - Response response = new Response(); - response.statusCode = connection.getResponseCode(); - response.statusLine = connection.getResponseMessage(); - - /* Store all header field names in lower-case to eliminate case insensitivity */ - Log.v(TAG, "HTTP response:"); - Map> caseSensitiveHeaders = connection.getHeaderFields(); - response.headers = new HashMap<>(caseSensitiveHeaders.size(), 1f); - - for (Map.Entry> entry : caseSensitiveHeaders.entrySet()) { - if (entry.getKey() != null) { - response.headers.put(entry.getKey().toLowerCase(), entry.getValue()); - if (Log.level <= Log.VERBOSE) - for (String val : entry.getValue()) - Log.v(TAG, entry.getKey() + ": " + val); - } - } - - if(response.statusCode == HttpURLConnection.HTTP_NO_CONTENT) { - return response; - } - - response.contentType = connection.getContentType(); - response.contentLength = connection.getContentLength(); - - InputStream is = null; - try { - is = connection.getInputStream(); - } catch (Throwable e) {} - if (is == null) - is = connection.getErrorStream(); - - try { - response.body = readInputStream(is, response.contentLength); - Log.v(TAG, System.lineSeparator() + new String(response.body)); - } catch (NullPointerException e) { - /* nothing to read */ - } finally { - if (is != null) { - try { - is.close(); - } catch (IOException e) {} - } - } - - return response; - } - - private byte[] readInputStream(InputStream inputStream, int bytes) throws IOException { - /* If there is nothing to read */ - if (inputStream == null) { - throw new NullPointerException("inputStream == null"); - } - - int bytesRead = 0; - - if (bytes == -1) { - ByteArrayOutputStream outputStream = new ByteArrayOutputStream(); - byte[] buffer = new byte[4 * 1024]; - while((bytesRead = inputStream.read(buffer)) > -1) { - outputStream.write(buffer, 0, bytesRead); - } - - return outputStream.toByteArray(); - } - else { - int idx = 0; - byte[] output = new byte[bytes]; - while((bytesRead = inputStream.read(output, idx, bytes - idx)) > -1) { - idx += bytesRead; - } - - return output; - } - } - - Proxy getProxy(URL url) { - String host = url.getHost(); - return getProxy(host); - } - - private Proxy getProxy(String host) { - if(proxyOptions != null) { - String[] nonProxyHosts = proxyOptions.nonProxyHosts; - if(nonProxyHosts != null) { - for(String nonProxyHostPattern : nonProxyHosts) { - if(host.matches(nonProxyHostPattern)) { - return null; - } - } - } - } - return proxy; - } - - /************************* - * Private state - *************************/ - - static { - /* if on Android, check version */ - Field androidVersionField = null; - int androidVersion = 0; - try { - androidVersionField = Class.forName("android.os.Build$VERSION").getField("SDK_INT"); - androidVersion = androidVersionField.getInt(androidVersionField); - } catch (Exception e) {} - if(androidVersionField != null && androidVersion < 8) { - /* HTTP connection reuse which was buggy pre-froyo */ - System.setProperty("httpCore.keepAlive", "false"); - } - } - - public final String scheme; - public final int port; - final ClientOptions options; - final Hosts hosts; - - private final Auth auth; - private final ProxyOptions proxyOptions; - private HttpAuth proxyAuth; - private Proxy proxy = Proxy.NO_PROXY; - private boolean isDisposed; - - private static final String TAG = HttpCore.class.getName(); - - /** - * Interface for an entity that supplies an httpCore request body - */ - public interface RequestBody { - byte[] getEncoded(); - String getContentType(); - } - - /** - * Interface for an entity that performs type-specific processing on an httpCore response body - * @param - */ - public interface BodyHandler { - T[] handleResponseBody(String contentType, byte[] body) throws AblyException; - } - - /** - * Interface for an entity that performs type-specific processing on an httpCore response - * @param - */ - public interface ResponseHandler { - T handleResponse(Response response, ErrorInfo error) throws AblyException; - } - - /** - * A type encapsulating an httpCore response - */ - public static class Response { - public int statusCode; - public String statusLine; - public Map> headers; - public String contentType; - public int contentLength; - public byte[] body; - - /** - * Returns the value of the named header field. - *

- * If called on a connection that sets the same header multiple times - * with possibly different values, only the last value is returned. - * - * - * @param name the name of a header field. - * @return the value of the named header field, or {@code null} - * if there is no such field in the header. - */ - public List getHeaderFields(String name) { - if(headers == null) { - return null; - } - - return headers.get(name.toLowerCase()); - } - } - - /** - * Exception signifying that an httpCore request failed with a WWW-Authenticate response - */ - public static class AuthRequiredException extends AblyException { - private static final long serialVersionUID = 1L; - public AuthRequiredException(Throwable throwable, ErrorInfo reason) { - super(throwable, reason); - } - public boolean expired; - public Map authChallenge; - public Map proxyAuthChallenge; - } + public static class AuthRequiredException extends AblyException { + private static final long serialVersionUID = 1L; + public AuthRequiredException(Throwable throwable, ErrorInfo reason) { + super(throwable, reason); + } + public boolean expired; + public Map authChallenge; + public Map proxyAuthChallenge; + } } diff --git a/lib/src/main/java/io/ably/lib/http/HttpHelpers.java b/lib/src/main/java/io/ably/lib/http/HttpHelpers.java index 687b857d7..a12af1f1d 100644 --- a/lib/src/main/java/io/ably/lib/http/HttpHelpers.java +++ b/lib/src/main/java/io/ably/lib/http/HttpHelpers.java @@ -10,109 +10,109 @@ import static io.ably.lib.http.HttpUtils.buildURL; public class HttpHelpers { - /** - * Make a synchronous HTTP request to an Ably endpoint, using the Ably auth credentials and fallback hosts if necessary - * @param http - * @param path - * @param method - * @param headers - * @param params - * @param requestBody - * @param responseHandler - * @return - * @throws AblyException - */ - public static T ablyHttpExecute(Http http, final String path, final String method, final Param[] headers, final Param[] params, final HttpCore.RequestBody requestBody, final HttpCore.ResponseHandler responseHandler, final boolean requireAblyAuth) throws AblyException { - return http.request(new Http.Execute() { - @Override - public void execute(HttpScheduler http, Callback callback) throws AblyException { - http.exec(path, method, headers, params, requestBody, responseHandler, requireAblyAuth, callback); - } - }).sync(); - } + /** + * Make a synchronous HTTP request to an Ably endpoint, using the Ably auth credentials and fallback hosts if necessary + * @param http + * @param path + * @param method + * @param headers + * @param params + * @param requestBody + * @param responseHandler + * @return + * @throws AblyException + */ + public static T ablyHttpExecute(Http http, final String path, final String method, final Param[] headers, final Param[] params, final HttpCore.RequestBody requestBody, final HttpCore.ResponseHandler responseHandler, final boolean requireAblyAuth) throws AblyException { + return http.request(new Http.Execute() { + @Override + public void execute(HttpScheduler http, Callback callback) throws AblyException { + http.exec(path, method, headers, params, requestBody, responseHandler, requireAblyAuth, callback); + } + }).sync(); + } - private static final String TAG = HttpHelpers.class.getName(); + private static final String TAG = HttpHelpers.class.getName(); - /** - * Simple HTTP GET; no auth, headers, returning response body as string - * @param httpCore - * @param url - * @return - * @throws AblyException - */ - public static String getUrlString(HttpCore httpCore, String url) throws AblyException { - return new String(getUrl(httpCore, url)); - } + /** + * Simple HTTP GET; no auth, headers, returning response body as string + * @param httpCore + * @param url + * @return + * @throws AblyException + */ + public static String getUrlString(HttpCore httpCore, String url) throws AblyException { + return new String(getUrl(httpCore, url)); + } - /** - * Simple HTTP GET; no auth, headers, returning response body as byte[] - * @param httpCore - * @param url - * @return - * @throws AblyException - */ - public static byte[] getUrl(HttpCore httpCore, String url) throws AblyException { - try { - return httpExecute(httpCore, new URL(url), HttpConstants.Methods.GET, null, null, new HttpCore.ResponseHandler() { - @Override - public byte[] handleResponse(HttpCore.Response response, ErrorInfo error) throws AblyException { - if(error != null) { - throw AblyException.fromErrorInfo(error); - } - return response.body; - }}); - } catch(IOException ioe) { - throw AblyException.fromThrowable(ioe); - } - } + /** + * Simple HTTP GET; no auth, headers, returning response body as byte[] + * @param httpCore + * @param url + * @return + * @throws AblyException + */ + public static byte[] getUrl(HttpCore httpCore, String url) throws AblyException { + try { + return httpExecute(httpCore, new URL(url), HttpConstants.Methods.GET, null, null, new HttpCore.ResponseHandler() { + @Override + public byte[] handleResponse(HttpCore.Response response, ErrorInfo error) throws AblyException { + if(error != null) { + throw AblyException.fromErrorInfo(error); + } + return response.body; + }}); + } catch(IOException ioe) { + throw AblyException.fromThrowable(ioe); + } + } - /** - * HTTP GET for non-Ably host - * @param uri - * @param headers - * @param params - * @param responseHandler - * @return - * @throws AblyException - */ - public static T getUri(HttpCore httpCore, String uri, Param[] headers, Param[] params, HttpCore.ResponseHandler responseHandler) throws AblyException { - return httpExecute(httpCore, buildURL(uri, params), HttpConstants.Methods.GET, headers, null, responseHandler); - } + /** + * HTTP GET for non-Ably host + * @param uri + * @param headers + * @param params + * @param responseHandler + * @return + * @throws AblyException + */ + public static T getUri(HttpCore httpCore, String uri, Param[] headers, Param[] params, HttpCore.ResponseHandler responseHandler) throws AblyException { + return httpExecute(httpCore, buildURL(uri, params), HttpConstants.Methods.GET, headers, null, responseHandler); + } - /** - * HTTP POST with data in form encoding for non-Ably host - * @param uri - * @param headers - * @param queryParams - * @param responseHandler - * @return - * @throws AblyException - */ - public static T postUri(HttpCore httpCore, String uri, Param[] headers, Param[] queryParams, Param[] bodyParams, HttpCore.ResponseHandler responseHandler) throws AblyException { - return httpExecute(httpCore, buildURL(uri, queryParams), HttpConstants.Methods.POST, headers, new HttpUtils.FormRequestBody(bodyParams), responseHandler); - } + /** + * HTTP POST with data in form encoding for non-Ably host + * @param uri + * @param headers + * @param queryParams + * @param responseHandler + * @return + * @throws AblyException + */ + public static T postUri(HttpCore httpCore, String uri, Param[] headers, Param[] queryParams, Param[] bodyParams, HttpCore.ResponseHandler responseHandler) throws AblyException { + return httpExecute(httpCore, buildURL(uri, queryParams), HttpConstants.Methods.POST, headers, new HttpUtils.FormRequestBody(bodyParams), responseHandler); + } - /** - * Make a synchronous HTTP request to non-Ably endpoint, specified by URL and using the configured proxy, if any - * @param httpCore - * @param url - * @param method - * @param headers - * @param requestBody - * @param responseHandler - * @return - * @throws AblyException - */ - public static T httpExecute(HttpCore httpCore, URL url, String method, Param[] headers, HttpCore.RequestBody requestBody, HttpCore.ResponseHandler responseHandler) throws AblyException { - return httpCore.httpExecuteWithRetry(url, method, headers, requestBody, responseHandler, false); - } + /** + * Make a synchronous HTTP request to non-Ably endpoint, specified by URL and using the configured proxy, if any + * @param httpCore + * @param url + * @param method + * @param headers + * @param requestBody + * @param responseHandler + * @return + * @throws AblyException + */ + public static T httpExecute(HttpCore httpCore, URL url, String method, Param[] headers, HttpCore.RequestBody requestBody, HttpCore.ResponseHandler responseHandler) throws AblyException { + return httpCore.httpExecuteWithRetry(url, method, headers, requestBody, responseHandler, false); + } - public static T postSync(final Http http, final String path, final Param[] headers, final Param[] params, final HttpCore.RequestBody requestBody, final HttpCore.ResponseHandler responseHandler, final boolean requireAblyAuth) throws AblyException { - return http.request(new Http.Execute() { - @Override - public void execute(HttpScheduler http, Callback callback) throws AblyException { - http.post(path, headers, params, requestBody, responseHandler, requireAblyAuth, callback); - } - }).sync(); - } + public static T postSync(final Http http, final String path, final Param[] headers, final Param[] params, final HttpCore.RequestBody requestBody, final HttpCore.ResponseHandler responseHandler, final boolean requireAblyAuth) throws AblyException { + return http.request(new Http.Execute() { + @Override + public void execute(HttpScheduler http, Callback callback) throws AblyException { + http.post(path, headers, params, requestBody, responseHandler, requireAblyAuth, callback); + } + }).sync(); + } } diff --git a/lib/src/main/java/io/ably/lib/http/HttpPaginatedQuery.java b/lib/src/main/java/io/ably/lib/http/HttpPaginatedQuery.java index 897ecbe35..5112bf8b2 100644 --- a/lib/src/main/java/io/ably/lib/http/HttpPaginatedQuery.java +++ b/lib/src/main/java/io/ably/lib/http/HttpPaginatedQuery.java @@ -19,146 +19,146 @@ public class HttpPaginatedQuery implements HttpCore.ResponseHandler { - public HttpPaginatedQuery(Http http, String method, String path, Param[] headers, Param[] params, - HttpCore.RequestBody requestBody) { - this.http = http; - this.method = method; - this.path = path; - this.requestHeaders = headers; - this.requestParams = params; - this.requestBody = requestBody; - this.bodyHandler = jsonArrayResponseHandler; - } - - /** - * Get the result of the first query - * @return An HttpPaginatedResponse giving the first page of results - * together with any available links to related results pages. - * @throws AblyException - */ - public HttpPaginatedResponse exec() throws AblyException { - return exec(requestParams); - } - - /** - * Get the result of the first query - * @return An HttpPaginatedResponse giving the first page of results - * together with any available links to related results pages. - * @throws AblyException - */ - public HttpPaginatedResponse exec(final Param[] params) throws AblyException { - final HttpCore.ResponseHandler responseHandler = this; - return http.request(new Http.Execute() { - @Override - public void execute(HttpScheduler http, Callback callback) throws AblyException { - http.exec(path, method, requestHeaders, params, requestBody, responseHandler, true, callback); - } - }).sync(); - } - - @Override - public HttpPaginatedResponse handleResponse(HttpCore.Response response, ErrorInfo error) throws AblyException { - return new HttpPaginatedResult(response, error); - } - - public class HttpPaginatedResult extends HttpPaginatedResponse { - private JsonElement[] contents; - - private HttpPaginatedResult(HttpCore.Response response, ErrorInfo error) throws AblyException { - statusCode = response.statusCode; - headers = HttpUtils.toParamArray(response.headers); - if(error != null) { - errorCode = error.code; - errorMessage = error.message; - } else { - success = true; - if(response.body != null) { - contents = bodyHandler.handleResponseBody(response.contentType, response.body); - } - } - - List linkHeaders = response.getHeaderFields(HttpConstants.Headers.LINK); - if(linkHeaders != null) { - HashMap links = BasePaginatedQuery.parseLinks(linkHeaders); - relFirst = links.get("first"); - relCurrent = links.get("current"); - relNext = links.get("next"); - } - } - - @Override - public JsonElement[] items() { return contents; } - - @Override - public HttpPaginatedResponse first() throws AblyException { return execRel(relFirst); } - - @Override - public HttpPaginatedResponse current() throws AblyException { return execRel(relCurrent); } - - @Override - public HttpPaginatedResponse next() throws AblyException { return execRel(relNext); } - - private HttpPaginatedResponse execRel(String linkUrl) throws AblyException { - if(linkUrl == null) return null; - /* we're expecting the format to be ./path-component?name=value&name=value... */ - Matcher urlMatch = BasePaginatedQuery.urlPattern.matcher(linkUrl); - if(urlMatch.matches()) { - String[] paramSpecs = urlMatch.group(2).split("&"); - Param[] params = new Param[paramSpecs.length]; - try { - for(int i = 0; i < paramSpecs.length; i++) { - String[] split = paramSpecs[i].split("="); - String paramKey = split[0]; - String paramValue = (split.length >= 2) ? split[1] : ""; - params[i] = new Param(paramKey, URLDecoder.decode(paramValue, "UTF-8")); - } - } catch(UnsupportedEncodingException uee) {} - return exec(params); - } - throw AblyException.fromErrorInfo(new ErrorInfo("Unexpected link URL format", 500, 50000)); - } - - private String relFirst, relCurrent, relNext; - - @Override - public boolean hasFirst() { return relFirst != null; } - - @Override - public boolean hasCurrent() { return relCurrent != null; } - - @Override - public boolean hasNext() { return relNext != null; } - - @Override - public boolean isLast() { - return relNext == null; - } - } - - static final HttpCore.BodyHandler jsonArrayResponseHandler = new HttpCore.BodyHandler() { - @Override - public JsonElement[] handleResponseBody(String contentType, byte[] body) throws AblyException { - if(!"application/json".equals(contentType)) { - throw AblyException.fromErrorInfo(new ErrorInfo("Unexpected content type: " + contentType, 500, 50000)); - } - JsonElement jsonBody = Serialisation.gsonParser.parse(new String(body, Charset.forName("UTF-8"))); - if(!jsonBody.isJsonArray()) { - return new JsonElement[] { jsonBody }; - } - JsonArray jsonArray = jsonBody.getAsJsonArray(); - JsonElement[] items = new JsonElement[jsonArray.size()]; - for(int i = 0; i < items.length; i++) { - items[i] = jsonArray.get(i); - } - return items; - } - }; - - private final Http http; - private final String method; - private final String path; - private final Param[] requestHeaders; - private final Param[] requestParams; - private final HttpCore.RequestBody requestBody; - private final HttpCore.BodyHandler bodyHandler; + public HttpPaginatedQuery(Http http, String method, String path, Param[] headers, Param[] params, + HttpCore.RequestBody requestBody) { + this.http = http; + this.method = method; + this.path = path; + this.requestHeaders = headers; + this.requestParams = params; + this.requestBody = requestBody; + this.bodyHandler = jsonArrayResponseHandler; + } + + /** + * Get the result of the first query + * @return An HttpPaginatedResponse giving the first page of results + * together with any available links to related results pages. + * @throws AblyException + */ + public HttpPaginatedResponse exec() throws AblyException { + return exec(requestParams); + } + + /** + * Get the result of the first query + * @return An HttpPaginatedResponse giving the first page of results + * together with any available links to related results pages. + * @throws AblyException + */ + public HttpPaginatedResponse exec(final Param[] params) throws AblyException { + final HttpCore.ResponseHandler responseHandler = this; + return http.request(new Http.Execute() { + @Override + public void execute(HttpScheduler http, Callback callback) throws AblyException { + http.exec(path, method, requestHeaders, params, requestBody, responseHandler, true, callback); + } + }).sync(); + } + + @Override + public HttpPaginatedResponse handleResponse(HttpCore.Response response, ErrorInfo error) throws AblyException { + return new HttpPaginatedResult(response, error); + } + + public class HttpPaginatedResult extends HttpPaginatedResponse { + private JsonElement[] contents; + + private HttpPaginatedResult(HttpCore.Response response, ErrorInfo error) throws AblyException { + statusCode = response.statusCode; + headers = HttpUtils.toParamArray(response.headers); + if(error != null) { + errorCode = error.code; + errorMessage = error.message; + } else { + success = true; + if(response.body != null) { + contents = bodyHandler.handleResponseBody(response.contentType, response.body); + } + } + + List linkHeaders = response.getHeaderFields(HttpConstants.Headers.LINK); + if(linkHeaders != null) { + HashMap links = BasePaginatedQuery.parseLinks(linkHeaders); + relFirst = links.get("first"); + relCurrent = links.get("current"); + relNext = links.get("next"); + } + } + + @Override + public JsonElement[] items() { return contents; } + + @Override + public HttpPaginatedResponse first() throws AblyException { return execRel(relFirst); } + + @Override + public HttpPaginatedResponse current() throws AblyException { return execRel(relCurrent); } + + @Override + public HttpPaginatedResponse next() throws AblyException { return execRel(relNext); } + + private HttpPaginatedResponse execRel(String linkUrl) throws AblyException { + if(linkUrl == null) return null; + /* we're expecting the format to be ./path-component?name=value&name=value... */ + Matcher urlMatch = BasePaginatedQuery.urlPattern.matcher(linkUrl); + if(urlMatch.matches()) { + String[] paramSpecs = urlMatch.group(2).split("&"); + Param[] params = new Param[paramSpecs.length]; + try { + for(int i = 0; i < paramSpecs.length; i++) { + String[] split = paramSpecs[i].split("="); + String paramKey = split[0]; + String paramValue = (split.length >= 2) ? split[1] : ""; + params[i] = new Param(paramKey, URLDecoder.decode(paramValue, "UTF-8")); + } + } catch(UnsupportedEncodingException uee) {} + return exec(params); + } + throw AblyException.fromErrorInfo(new ErrorInfo("Unexpected link URL format", 500, 50000)); + } + + private String relFirst, relCurrent, relNext; + + @Override + public boolean hasFirst() { return relFirst != null; } + + @Override + public boolean hasCurrent() { return relCurrent != null; } + + @Override + public boolean hasNext() { return relNext != null; } + + @Override + public boolean isLast() { + return relNext == null; + } + } + + static final HttpCore.BodyHandler jsonArrayResponseHandler = new HttpCore.BodyHandler() { + @Override + public JsonElement[] handleResponseBody(String contentType, byte[] body) throws AblyException { + if(!"application/json".equals(contentType)) { + throw AblyException.fromErrorInfo(new ErrorInfo("Unexpected content type: " + contentType, 500, 50000)); + } + JsonElement jsonBody = Serialisation.gsonParser.parse(new String(body, Charset.forName("UTF-8"))); + if(!jsonBody.isJsonArray()) { + return new JsonElement[] { jsonBody }; + } + JsonArray jsonArray = jsonBody.getAsJsonArray(); + JsonElement[] items = new JsonElement[jsonArray.size()]; + for(int i = 0; i < items.length; i++) { + items[i] = jsonArray.get(i); + } + return items; + } + }; + + private final Http http; + private final String method; + private final String path; + private final Param[] requestHeaders; + private final Param[] requestParams; + private final HttpCore.RequestBody requestBody; + private final HttpCore.BodyHandler bodyHandler; } 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 9b20d86dd..54046b995 100644 --- a/lib/src/main/java/io/ably/lib/http/HttpScheduler.java +++ b/lib/src/main/java/io/ably/lib/http/HttpScheduler.java @@ -22,414 +22,414 @@ */ public class HttpScheduler { - /** - * Async HTTP GET for Ably host, with fallbacks - * @param path - * @param headers - * @param params - * @param responseHandler - * @param callback - */ - public Future get(String path, Param[] headers, Param[] params, HttpCore.ResponseHandler responseHandler, boolean requireAblyAuth, Callback callback) { - return ablyHttpExecuteWithFallback(path, HttpConstants.Methods.GET, headers, params, null, responseHandler, requireAblyAuth, callback); - } + /** + * Async HTTP GET for Ably host, with fallbacks + * @param path + * @param headers + * @param params + * @param responseHandler + * @param callback + */ + public Future get(String path, Param[] headers, Param[] params, HttpCore.ResponseHandler responseHandler, boolean requireAblyAuth, Callback callback) { + return ablyHttpExecuteWithFallback(path, HttpConstants.Methods.GET, headers, params, null, responseHandler, requireAblyAuth, callback); + } - /** - * Async HTTP PUT for Ably host, with fallbacks - * @param path - * @param headers - * @param params - * @param requestBody - * @param responseHandler - * @param callback - */ - public Future put(String path, Param[] headers, Param[] params, HttpCore.RequestBody requestBody, HttpCore.ResponseHandler responseHandler, boolean requireAblyAuth, Callback callback) { - return ablyHttpExecuteWithFallback(path, HttpConstants.Methods.PUT, headers, params, requestBody, responseHandler, requireAblyAuth, callback); - } + /** + * Async HTTP PUT for Ably host, with fallbacks + * @param path + * @param headers + * @param params + * @param requestBody + * @param responseHandler + * @param callback + */ + public Future put(String path, Param[] headers, Param[] params, HttpCore.RequestBody requestBody, HttpCore.ResponseHandler responseHandler, boolean requireAblyAuth, Callback callback) { + return ablyHttpExecuteWithFallback(path, HttpConstants.Methods.PUT, headers, params, requestBody, responseHandler, requireAblyAuth, callback); + } - /** - * Async HTTP POST for Ably host, with fallbacks - * @param path - * @param headers - * @param params - * @param requestBody - * @param responseHandler - * @param callback - */ - public Future post(String path, Param[] headers, Param[] params, HttpCore.RequestBody requestBody, HttpCore.ResponseHandler responseHandler, boolean requireAblyAuth, Callback callback) { - return ablyHttpExecuteWithFallback(path, HttpConstants.Methods.POST, headers, params, requestBody, responseHandler, requireAblyAuth, callback); - } + /** + * Async HTTP POST for Ably host, with fallbacks + * @param path + * @param headers + * @param params + * @param requestBody + * @param responseHandler + * @param callback + */ + public Future post(String path, Param[] headers, Param[] params, HttpCore.RequestBody requestBody, HttpCore.ResponseHandler responseHandler, boolean requireAblyAuth, Callback callback) { + return ablyHttpExecuteWithFallback(path, HttpConstants.Methods.POST, headers, params, requestBody, responseHandler, requireAblyAuth, callback); + } - /** - * Async HTTP PATCH for Ably host, with fallbacks - * @param path - * @param headers - * @param params - * @param requestBody - * @param responseHandler - * @param callback - */ - public Future patch(String path, Param[] headers, Param[] params, HttpCore.RequestBody requestBody, HttpCore.ResponseHandler responseHandler, boolean requireAblyAuth, Callback callback) { - return ablyHttpExecuteWithFallback(path, HttpConstants.Methods.PATCH, headers, params, requestBody, responseHandler, requireAblyAuth, callback); - } + /** + * Async HTTP PATCH for Ably host, with fallbacks + * @param path + * @param headers + * @param params + * @param requestBody + * @param responseHandler + * @param callback + */ + public Future patch(String path, Param[] headers, Param[] params, HttpCore.RequestBody requestBody, HttpCore.ResponseHandler responseHandler, boolean requireAblyAuth, Callback callback) { + return ablyHttpExecuteWithFallback(path, HttpConstants.Methods.PATCH, headers, params, requestBody, responseHandler, requireAblyAuth, callback); + } - /** - * Async HTTP DEL for Ably host, with fallbacks - * @param path - * @param headers - * @param params - * @param responseHandler - * @param callback - */ - public Future del(String path, Param[] headers, Param[] params, HttpCore.ResponseHandler responseHandler, boolean requireAblyAuth, Callback callback) { - return ablyHttpExecuteWithFallback(path, HttpConstants.Methods.DELETE, headers, params, null, responseHandler, requireAblyAuth, callback); - } + /** + * Async HTTP DEL for Ably host, with fallbacks + * @param path + * @param headers + * @param params + * @param responseHandler + * @param callback + */ + public Future del(String path, Param[] headers, Param[] params, HttpCore.ResponseHandler responseHandler, boolean requireAblyAuth, Callback callback) { + return ablyHttpExecuteWithFallback(path, HttpConstants.Methods.DELETE, headers, params, null, responseHandler, requireAblyAuth, callback); + } - /** - * Async HTTP request for Ably host, with fallbacks - * @param path - * @param method - * @param headers - * @param params - * @param requestBody - * @param responseHandler - * @param callback - */ - public Future exec(String path, String method, Param[] headers, Param[] params, HttpCore.RequestBody requestBody, HttpCore.ResponseHandler responseHandler, boolean requireAblyAuth, Callback callback) { - return ablyHttpExecuteWithFallback(path, method, headers, params, requestBody, responseHandler, requireAblyAuth, callback); - } + /** + * Async HTTP request for Ably host, with fallbacks + * @param path + * @param method + * @param headers + * @param params + * @param requestBody + * @param responseHandler + * @param callback + */ + public Future exec(String path, String method, Param[] headers, Param[] params, HttpCore.RequestBody requestBody, HttpCore.ResponseHandler responseHandler, boolean requireAblyAuth, Callback callback) { + return ablyHttpExecuteWithFallback(path, method, headers, params, requestBody, responseHandler, requireAblyAuth, callback); + } - /************************** - * Internal API - **************************/ + /************************** + * Internal API + **************************/ - /** - * An AsyncRequest type representing a request to a specific URL - * @param - */ - private class UrlRequest extends AsyncRequest { - private UrlRequest( - URL url, - final String method, - final Param[] headers, - final Param[] params, - final HttpCore.RequestBody requestBody, - final HttpCore.ResponseHandler responseHandler, - final Callback callback) { - super(method, headers, params, requestBody, responseHandler, callback); - this.url = url; - } - @Override - public void run() { - try { - T result = httpExecuteWithRetry(url); - setResult(result); - } catch(AblyException e) { - setError(e.errorInfo); - } finally { - disposeConnection(); - } - } - private final URL url; - } + /** + * An AsyncRequest type representing a request to a specific URL + * @param + */ + private class UrlRequest extends AsyncRequest { + private UrlRequest( + URL url, + final String method, + final Param[] headers, + final Param[] params, + final HttpCore.RequestBody requestBody, + final HttpCore.ResponseHandler responseHandler, + final Callback callback) { + super(method, headers, params, requestBody, responseHandler, callback); + this.url = url; + } + @Override + public void run() { + try { + T result = httpExecuteWithRetry(url); + setResult(result); + } catch(AblyException e) { + setError(e.errorInfo); + } finally { + disposeConnection(); + } + } + private final URL url; + } - /** - * An AsyncRequest type representing a request to an Ably endpoint specified by host and path, - * supporting reauthentication on receipt of WWW-Authenticate - * @param - */ - private class AblyRequestWithRetry extends AsyncRequest { - private AblyRequestWithRetry( - String host, - String path, - final String method, - final Param[] headers, - final Param[] params, - final HttpCore.RequestBody requestBody, - final HttpCore.ResponseHandler responseHandler, - final boolean requireAblyAuth, - final Callback callback) { - super(method, headers, params, requestBody, responseHandler, callback); - this.host = host; - this.path = path; - this.requireAblyAuth = requireAblyAuth; - } - @Override - public void run() { - try { - result = httpExecuteWithRetry(host, path, requireAblyAuth); - setResult(result); - } catch(AblyException e) { - setError(e.errorInfo); - } finally { - disposeConnection(); - } - } - private final String host; - private final String path; - private final Boolean requireAblyAuth; - } + /** + * An AsyncRequest type representing a request to an Ably endpoint specified by host and path, + * supporting reauthentication on receipt of WWW-Authenticate + * @param + */ + private class AblyRequestWithRetry extends AsyncRequest { + private AblyRequestWithRetry( + String host, + String path, + final String method, + final Param[] headers, + final Param[] params, + final HttpCore.RequestBody requestBody, + final HttpCore.ResponseHandler responseHandler, + final boolean requireAblyAuth, + final Callback callback) { + super(method, headers, params, requestBody, responseHandler, callback); + this.host = host; + this.path = path; + this.requireAblyAuth = requireAblyAuth; + } + @Override + public void run() { + try { + result = httpExecuteWithRetry(host, path, requireAblyAuth); + setResult(result); + } catch(AblyException e) { + setError(e.errorInfo); + } finally { + disposeConnection(); + } + } + private final String host; + private final String path; + private final Boolean requireAblyAuth; + } - /** - * An AsyncRequest type representing a request to an Ably endpoint specified by path, - * supporting host fallback and reauthentication on receipt of WWW-Authenticate - * @param - */ - private class AblyRequestWithFallback extends AsyncRequest { - private AblyRequestWithFallback( - String path, - final String method, - final Param[] headers, - final Param[] params, - final HttpCore.RequestBody requestBody, - final HttpCore.ResponseHandler responseHandler, - final boolean requireAblyAuth, - final Callback callback) { - super(method, headers, params, requestBody, responseHandler, callback); - this.path = path; - this.requireAblyAuth = requireAblyAuth; - } - @Override - public void run() { - String candidateHost = httpCore.hosts.getPreferredHost(); - int retryCountRemaining = (httpCore.hosts.fallbackHostsRemaining(candidateHost) > 0) ? httpCore.options.httpMaxRetryCount : 0; + /** + * An AsyncRequest type representing a request to an Ably endpoint specified by path, + * supporting host fallback and reauthentication on receipt of WWW-Authenticate + * @param + */ + private class AblyRequestWithFallback extends AsyncRequest { + private AblyRequestWithFallback( + String path, + final String method, + final Param[] headers, + final Param[] params, + final HttpCore.RequestBody requestBody, + final HttpCore.ResponseHandler responseHandler, + final boolean requireAblyAuth, + final Callback callback) { + super(method, headers, params, requestBody, responseHandler, callback); + this.path = path; + this.requireAblyAuth = requireAblyAuth; + } + @Override + public void run() { + String candidateHost = httpCore.hosts.getPreferredHost(); + int retryCountRemaining = (httpCore.hosts.fallbackHostsRemaining(candidateHost) > 0) ? httpCore.options.httpMaxRetryCount : 0; - while(!isCancelled) { - try { - result = httpExecuteWithRetry(candidateHost, path, requireAblyAuth); - setResult(result); - httpCore.hosts.setPreferredHost(candidateHost, true); - break; - } catch (AblyException.HostFailedException e) { - if(--retryCountRemaining < 0) { - setError(e.errorInfo); - break; - } - Log.d(TAG, "Connection failed to host `" + candidateHost + "`. Searching for new host..."); - candidateHost = httpCore.hosts.getFallback(candidateHost); - if (candidateHost == null) { - setError(e.errorInfo); - break; - } - Log.d(TAG, "Switched to `" + candidateHost + "`."); - } catch(AblyException e) { - setError(e.errorInfo); - break; - } finally { - disposeConnection(); - } - } - } - private final String path; - private final boolean requireAblyAuth; - } + while(!isCancelled) { + try { + result = httpExecuteWithRetry(candidateHost, path, requireAblyAuth); + setResult(result); + httpCore.hosts.setPreferredHost(candidateHost, true); + break; + } catch (AblyException.HostFailedException e) { + if(--retryCountRemaining < 0) { + setError(e.errorInfo); + break; + } + Log.d(TAG, "Connection failed to host `" + candidateHost + "`. Searching for new host..."); + candidateHost = httpCore.hosts.getFallback(candidateHost); + if (candidateHost == null) { + setError(e.errorInfo); + break; + } + Log.d(TAG, "Switched to `" + candidateHost + "`."); + } catch(AblyException e) { + setError(e.errorInfo); + break; + } finally { + disposeConnection(); + } + } + } + private final String path; + private final boolean requireAblyAuth; + } - /** - * A class encapsulating a scheduled or in-process async HTTP request - * @param - */ - private abstract class AsyncRequest implements Future, Runnable { - private AsyncRequest( - final String method, - final Param[] headers, - final Param[] params, - final HttpCore.RequestBody requestBody, - final HttpCore.ResponseHandler responseHandler, - final Callback callback) { - this.method = method; - this.headers = headers; - this.params = params; - this.requestBody = requestBody; - this.responseHandler = responseHandler; - this.callback = callback; - } + /** + * A class encapsulating a scheduled or in-process async HTTP request + * @param + */ + private abstract class AsyncRequest implements Future, Runnable { + private AsyncRequest( + final String method, + final Param[] headers, + final Param[] params, + final HttpCore.RequestBody requestBody, + final HttpCore.ResponseHandler responseHandler, + final Callback callback) { + this.method = method; + this.headers = headers; + this.params = params; + this.requestBody = requestBody; + this.responseHandler = responseHandler; + this.callback = callback; + } - /************************** - * Future methods - **************************/ - @Override - public boolean cancel(boolean mayInterruptIfRunning) { - isCancelled = true; - return disposeConnection(); - } - @Override - public boolean isCancelled() { - return isCancelled; - } - @Override - public boolean isDone() { - return isDone; - } - @Override - public T get() throws InterruptedException, ExecutionException { - synchronized(this) { - while(!isDone) { - wait(); - } - if(err != null) { - throw new ExecutionException(AblyException.fromErrorInfo(err)); - } - } - return result; - } - @Override - public T get(long timeout, TimeUnit unit) throws InterruptedException, ExecutionException, TimeoutException { - long remaining = unit.toMillis(timeout), deadline = System.currentTimeMillis() + remaining; - synchronized(this) { - while(remaining > 0) { - wait(remaining); - if(isDone) { break; } - remaining = deadline - System.currentTimeMillis(); - } - if(!isDone) { - throw new TimeoutException(); - } - if(err != null) { - throw new ExecutionException(AblyException.fromErrorInfo(err)); - } - } - return result; - } + /************************** + * Future methods + **************************/ + @Override + public boolean cancel(boolean mayInterruptIfRunning) { + isCancelled = true; + return disposeConnection(); + } + @Override + public boolean isCancelled() { + return isCancelled; + } + @Override + public boolean isDone() { + return isDone; + } + @Override + public T get() throws InterruptedException, ExecutionException { + synchronized(this) { + while(!isDone) { + wait(); + } + if(err != null) { + throw new ExecutionException(AblyException.fromErrorInfo(err)); + } + } + return result; + } + @Override + public T get(long timeout, TimeUnit unit) throws InterruptedException, ExecutionException, TimeoutException { + long remaining = unit.toMillis(timeout), deadline = System.currentTimeMillis() + remaining; + synchronized(this) { + while(remaining > 0) { + wait(remaining); + if(isDone) { break; } + remaining = deadline - System.currentTimeMillis(); + } + if(!isDone) { + throw new TimeoutException(); + } + if(err != null) { + throw new ExecutionException(AblyException.fromErrorInfo(err)); + } + } + return result; + } - /************************** - * Private - **************************/ + /************************** + * Private + **************************/ - protected T httpExecuteWithRetry(URL url) throws AblyException { - return httpCore.httpExecuteWithRetry(url, method, headers, requestBody, responseHandler, false); - } - protected T httpExecuteWithRetry(String host, String path, boolean requireAblyAuth) throws AblyException { - URL url = HttpUtils.buildURL(httpCore.scheme, host, httpCore.port, path, params); - return httpCore.httpExecuteWithRetry(url, method, headers, requestBody, responseHandler, requireAblyAuth); - } - protected void setResult(T result) { - synchronized(this) { - this.result = result; - this.isDone = true; - notifyAll(); - } - if(callback != null) { - callback.onSuccess(result); - } - } - protected void setError(ErrorInfo err) { - synchronized(this) { - this.err = err; - this.isDone = true; - notifyAll(); - } - if(callback != null) { - callback.onError(err); - } - } - protected synchronized boolean disposeConnection() { - boolean hasConnection = conn != null; - if(hasConnection) { - conn.disconnect(); - conn = null; - } - return hasConnection; - } + protected T httpExecuteWithRetry(URL url) throws AblyException { + return httpCore.httpExecuteWithRetry(url, method, headers, requestBody, responseHandler, false); + } + protected T httpExecuteWithRetry(String host, String path, boolean requireAblyAuth) throws AblyException { + URL url = HttpUtils.buildURL(httpCore.scheme, host, httpCore.port, path, params); + return httpCore.httpExecuteWithRetry(url, method, headers, requestBody, responseHandler, requireAblyAuth); + } + protected void setResult(T result) { + synchronized(this) { + this.result = result; + this.isDone = true; + notifyAll(); + } + if(callback != null) { + callback.onSuccess(result); + } + } + protected void setError(ErrorInfo err) { + synchronized(this) { + this.err = err; + this.isDone = true; + notifyAll(); + } + if(callback != null) { + callback.onError(err); + } + } + protected synchronized boolean disposeConnection() { + boolean hasConnection = conn != null; + if(hasConnection) { + conn.disconnect(); + conn = null; + } + return hasConnection; + } - protected HttpURLConnection conn; - protected T result; - protected ErrorInfo err; + protected HttpURLConnection conn; + protected T result; + protected ErrorInfo err; - protected final String method; - protected final Param[] headers; - protected final Param[] params; - protected final HttpCore.RequestBody requestBody; - protected final HttpCore.ResponseHandler responseHandler; - protected final Callback callback; - protected boolean isCancelled = false; - protected boolean isDone = false; - } + protected final String method; + protected final Param[] headers; + protected final Param[] params; + protected final HttpCore.RequestBody requestBody; + protected final HttpCore.ResponseHandler responseHandler; + protected final Callback callback; + protected boolean isCancelled = false; + protected boolean isDone = false; + } - protected HttpScheduler(HttpCore httpCore, Executor executor) { - this.httpCore = httpCore; - this.executor = executor; - } + protected HttpScheduler(HttpCore httpCore, Executor executor) { + this.httpCore = httpCore; + this.executor = executor; + } - /** - * Make an asynchronous HTTP request to a given URL - * @param url - * @param method - * @param headers - * @param requestBody - * @param responseHandler - * @param callback - * @return - */ - public Future httpExecute( - final URL url, - final String method, - final Param[] headers, - final HttpCore.RequestBody requestBody, - final HttpCore.ResponseHandler responseHandler, - final Callback callback) { + /** + * Make an asynchronous HTTP request to a given URL + * @param url + * @param method + * @param headers + * @param requestBody + * @param responseHandler + * @param callback + * @return + */ + public Future httpExecute( + final URL url, + final String method, + final Param[] headers, + final HttpCore.RequestBody requestBody, + final HttpCore.ResponseHandler responseHandler, + final Callback callback) { - UrlRequest request = new UrlRequest<>(url, method, headers, null, requestBody, responseHandler, callback); - executor.execute(request); - return request; - } + UrlRequest request = new UrlRequest<>(url, method, headers, null, requestBody, responseHandler, callback); + executor.execute(request); + return request; + } - /** - * Make an asynchronous HTTP request to an Ably endpoint, using the Ably auth credentials and fallback hosts if necessary - * @param path - * @param method - * @param headers - * @param params - * @param requestBody - * @param responseHandler - * @param callback - * @return - */ - public Future ablyHttpExecuteWithFallback( - final String path, - final String method, - final Param[] headers, - final Param[] params, - final HttpCore.RequestBody requestBody, - final HttpCore.ResponseHandler responseHandler, - final boolean requireAblyAuth, - final Callback callback) { + /** + * Make an asynchronous HTTP request to an Ably endpoint, using the Ably auth credentials and fallback hosts if necessary + * @param path + * @param method + * @param headers + * @param params + * @param requestBody + * @param responseHandler + * @param callback + * @return + */ + public Future ablyHttpExecuteWithFallback( + final String path, + final String method, + final Param[] headers, + final Param[] params, + final HttpCore.RequestBody requestBody, + final HttpCore.ResponseHandler responseHandler, + final boolean requireAblyAuth, + final Callback callback) { - AblyRequestWithFallback request = new AblyRequestWithFallback<>(path, method, headers, params, requestBody, responseHandler, requireAblyAuth, callback); - executor.execute(request); - return request; - } + AblyRequestWithFallback request = new AblyRequestWithFallback<>(path, method, headers, params, requestBody, responseHandler, requireAblyAuth, callback); + executor.execute(request); + return request; + } - /** - * Make an asynchronous HTTP request to an Ably endpoint, using the Ably auth credentials and reauthentication if necessary - * @param host - * @param path - * @param method - * @param headers - * @param params - * @param requestBody - * @param responseHandler - * @param callback - * @return - */ - public Future ablyHttpExecuteWithRetry( - final String host, - final String path, - final String method, - final Param[] headers, - final Param[] params, - final HttpCore.RequestBody requestBody, - final HttpCore.ResponseHandler responseHandler, - final boolean requireAblyAuth, - final Callback callback) { + /** + * Make an asynchronous HTTP request to an Ably endpoint, using the Ably auth credentials and reauthentication if necessary + * @param host + * @param path + * @param method + * @param headers + * @param params + * @param requestBody + * @param responseHandler + * @param callback + * @return + */ + public Future ablyHttpExecuteWithRetry( + final String host, + final String path, + final String method, + final Param[] headers, + final Param[] params, + final HttpCore.RequestBody requestBody, + final HttpCore.ResponseHandler responseHandler, + final boolean requireAblyAuth, + final Callback callback) { - AblyRequestWithRetry request = new AblyRequestWithRetry<>(host, path, method, headers, params, requestBody, responseHandler, requireAblyAuth, callback); - executor.execute(request); - return request; - } + AblyRequestWithRetry request = new AblyRequestWithRetry<>(host, path, method, headers, params, requestBody, responseHandler, requireAblyAuth, callback); + executor.execute(request); + return request; + } - protected final Executor executor; - private final HttpCore httpCore; + protected final Executor executor; + private final HttpCore httpCore; - protected static final String TAG = HttpScheduler.class.getName(); + protected static final String TAG = HttpScheduler.class.getName(); } diff --git a/lib/src/main/java/io/ably/lib/http/HttpUtils.java b/lib/src/main/java/io/ably/lib/http/HttpUtils.java index 0c5209bf8..02889449e 100644 --- a/lib/src/main/java/io/ably/lib/http/HttpUtils.java +++ b/lib/src/main/java/io/ably/lib/http/HttpUtils.java @@ -24,242 +24,242 @@ * */ public class HttpUtils { - public static Map mimeTypes; - - static { - mimeTypes = new HashMap(); - mimeTypes.put("json", "application/json"); - mimeTypes.put("xml", "application/xml"); - mimeTypes.put("html", "text/html"); - mimeTypes.put("msgpack", "application/x-msgpack"); - } - - public static Param[] defaultAcceptHeaders(boolean binary) { - Param[] headers; - if(binary) { - headers = new Param[]{ new Param("Accept", "application/x-msgpack,application/json") }; - } else { - headers = new Param[]{ new Param("Accept", "application/json") }; - } - return headers; - } - - public static Param[] mergeHeaders(Param[] target, Param[] src) { - Map merged = new HashMap(); - if(target != null) { - for(Param param : target) { merged.put(param.key, param); } - } - if(src != null) { - for(Param param : src) { merged.put(param.key, param); } - } - return merged.values().toArray(new Param[merged.size()]); - } - - public static String encodeParams(String path, Param[] params) { - StringBuilder builder = new StringBuilder(path); - if(params != null && params.length > 0) { - boolean first = true; - for(Param entry : params) { - builder.append(first ? '?' : '&'); - first = false; - builder.append(entry.key); - builder.append('='); - builder.append(encodeURIComponent(entry.value)); - } - } - return builder.toString(); - } - - public static URL parseUrl(String url) throws AblyException { - try { - return new URL(url); - } catch (MalformedURLException e) { - throw AblyException.fromThrowable(e); - } - } - - public static Map decodeParams(String query) { - Map params = new HashMap(); - String[] pairs = query.split("&"); - try { - for (String pair : pairs) { - int idx = pair.indexOf('='); - String key = URLDecoder.decode(pair.substring(0, idx), "UTF-8"), - value = URLDecoder.decode(pair.substring(idx + 1), "UTF-8"); - params.put(key, new Param(key, value)); - } - } catch (UnsupportedEncodingException e) {} - return params; - } - - public static Map indexParams(Param[] paramArray) { - Map params = new HashMap(); - for (Param param : paramArray) { - params.put(param.key, param); - } - return params; - } - - public static Map mergeParams(Map target, Map src) { - for(Param p : src.values()) { - target.put(p.key, p); - } - return target; - } - - public static Param[] flattenParams(Map map) { - Param[] result = null; - if(map != null) { - result = map.values().toArray(new Param[map.size()]); - } - return result; - } - - public static Param[] toParamArray(Map> indexedParams) { - List params = new ArrayList(); - for(Entry> entry : indexedParams.entrySet()) { - for(String value : entry.getValue()) { - params.add(new Param(entry.getKey(), value)); - } - } - return params.toArray(new Param[params.size()]); - } - - public static String getParam(Param[] params, String key) { - String result = null; - if(params != null) { - for(Param p : params) { - if(key.equals(p.key)) { - result = p.value; - break; - } - } - } - return result; - } - - /* copied from https://stackoverflow.com/a/52378025 */ - private static final String HEX = "0123456789ABCDEF"; - - public static String encodeURIComponent(String str) { - if (str == null) { - return null; - } - - byte[] bytes = str.getBytes(Charset.forName("UTF-8")); - StringBuilder builder = new StringBuilder(bytes.length); - - for (byte c : bytes) { - if (c >= 'a' ? c <= 'z' || c == '~' : - c >= 'A' ? c <= 'Z' || c == '_' : - c >= '0' ? c <= '9' : c == '-' || c == '.') - builder.append((char)c); - else - builder.append('%') - .append(HEX.charAt(c >> 4 & 0xf)) - .append(HEX.charAt(c & 0xf)); - } - - return builder.toString(); - } - - private static void appendParams(StringBuilder uri, Param[] params) { - if(params != null && params.length > 0) { - uri.append('?').append(params[0].key).append('=').append(params[0].value); - for(int i = 1; i < params.length; i++) { - uri.append('&').append(params[i].key).append('=').append(params[i].value); - } - } - } - - static URL buildURL(String scheme, String host, int port, String path, Param[] params) { - StringBuilder builder = new StringBuilder(scheme).append(host).append(':').append(port).append(path); - appendParams(builder, params); - - URL result = null; - try { - result = new URL(builder.toString()); - } catch (MalformedURLException e) {} - return result; - } - - static URL buildURL(String uri, Param[] params) { - StringBuilder builder = new StringBuilder(uri); - appendParams(builder, params); - - URL result = null; - try { - result = new URL(builder.toString()); - } catch (MalformedURLException e) {} - return result; - } - - /** - * A RequestBody wrapping a byte array - */ - public static class ByteArrayRequestBody implements HttpCore.RequestBody { - public ByteArrayRequestBody(byte[] bytes, String contentType) { this.bytes = bytes; this.contentType = contentType; } - - @Override - public byte[] getEncoded() { return bytes; } - @Override - public String getContentType() { return contentType; } - - private final byte[] bytes; - private final String contentType; - } - - public static class FormRequestBody implements HttpCore.RequestBody { - public FormRequestBody(Param[] formData) { - this.formData = formData; - } - - @Override - public byte[] getEncoded() { - try { - StringBuilder body = new StringBuilder(); - for (int i = 0; i < formData.length; i++) { - if (i != 0) - body.append('&'); - body.append(URLEncoder.encode(formData[i].key, "UTF-8")); - body.append('='); - body.append(URLEncoder.encode(formData[i].value, "UTF-8")); - } - return body.toString().getBytes("UTF-8"); - } catch (UnsupportedEncodingException e) { - return new byte[]{}; - } - } - - @Override - public String getContentType() { - return HttpConstants.ContentTypes.FORM_ENCODING; - } - - private Param[] formData; - } - - /** - * A RequestBody wrapping a JSON-serialisable object - */ - public static class JsonRequestBody implements HttpCore.RequestBody { - public JsonRequestBody(String jsonText) { this.jsonText = jsonText; } - public JsonRequestBody(Object ob) { this(Serialisation.gson.toJson(ob)); } - - @Override - public byte[] getEncoded() { return (bytes != null) ? bytes : (bytes = jsonText.getBytes(Charset.forName("UTF-8"))); } - @Override - public String getContentType() { return HttpConstants.ContentTypes.JSON; } - - private final String jsonText; - private byte[] bytes; - } - - public static HttpCore.RequestBody requestBodyFromGson(JsonElement json, boolean useBinaryProtocol) { - if (!useBinaryProtocol) { - return new JsonRequestBody(json); - } - - return new ByteArrayRequestBody(Serialisation.gsonToMsgpack(json), "application/x-msgpack"); - } + public static Map mimeTypes; + + static { + mimeTypes = new HashMap(); + mimeTypes.put("json", "application/json"); + mimeTypes.put("xml", "application/xml"); + mimeTypes.put("html", "text/html"); + mimeTypes.put("msgpack", "application/x-msgpack"); + } + + public static Param[] defaultAcceptHeaders(boolean binary) { + Param[] headers; + if(binary) { + headers = new Param[]{ new Param("Accept", "application/x-msgpack,application/json") }; + } else { + headers = new Param[]{ new Param("Accept", "application/json") }; + } + return headers; + } + + public static Param[] mergeHeaders(Param[] target, Param[] src) { + Map merged = new HashMap(); + if(target != null) { + for(Param param : target) { merged.put(param.key, param); } + } + if(src != null) { + for(Param param : src) { merged.put(param.key, param); } + } + return merged.values().toArray(new Param[merged.size()]); + } + + public static String encodeParams(String path, Param[] params) { + StringBuilder builder = new StringBuilder(path); + if(params != null && params.length > 0) { + boolean first = true; + for(Param entry : params) { + builder.append(first ? '?' : '&'); + first = false; + builder.append(entry.key); + builder.append('='); + builder.append(encodeURIComponent(entry.value)); + } + } + return builder.toString(); + } + + public static URL parseUrl(String url) throws AblyException { + try { + return new URL(url); + } catch (MalformedURLException e) { + throw AblyException.fromThrowable(e); + } + } + + public static Map decodeParams(String query) { + Map params = new HashMap(); + String[] pairs = query.split("&"); + try { + for (String pair : pairs) { + int idx = pair.indexOf('='); + String key = URLDecoder.decode(pair.substring(0, idx), "UTF-8"), + value = URLDecoder.decode(pair.substring(idx + 1), "UTF-8"); + params.put(key, new Param(key, value)); + } + } catch (UnsupportedEncodingException e) {} + return params; + } + + public static Map indexParams(Param[] paramArray) { + Map params = new HashMap(); + for (Param param : paramArray) { + params.put(param.key, param); + } + return params; + } + + public static Map mergeParams(Map target, Map src) { + for(Param p : src.values()) { + target.put(p.key, p); + } + return target; + } + + public static Param[] flattenParams(Map map) { + Param[] result = null; + if(map != null) { + result = map.values().toArray(new Param[map.size()]); + } + return result; + } + + public static Param[] toParamArray(Map> indexedParams) { + List params = new ArrayList(); + for(Entry> entry : indexedParams.entrySet()) { + for(String value : entry.getValue()) { + params.add(new Param(entry.getKey(), value)); + } + } + return params.toArray(new Param[params.size()]); + } + + public static String getParam(Param[] params, String key) { + String result = null; + if(params != null) { + for(Param p : params) { + if(key.equals(p.key)) { + result = p.value; + break; + } + } + } + return result; + } + + /* copied from https://stackoverflow.com/a/52378025 */ + private static final String HEX = "0123456789ABCDEF"; + + public static String encodeURIComponent(String str) { + if (str == null) { + return null; + } + + byte[] bytes = str.getBytes(Charset.forName("UTF-8")); + StringBuilder builder = new StringBuilder(bytes.length); + + for (byte c : bytes) { + if (c >= 'a' ? c <= 'z' || c == '~' : + c >= 'A' ? c <= 'Z' || c == '_' : + c >= '0' ? c <= '9' : c == '-' || c == '.') + builder.append((char)c); + else + builder.append('%') + .append(HEX.charAt(c >> 4 & 0xf)) + .append(HEX.charAt(c & 0xf)); + } + + return builder.toString(); + } + + private static void appendParams(StringBuilder uri, Param[] params) { + if(params != null && params.length > 0) { + uri.append('?').append(params[0].key).append('=').append(params[0].value); + for(int i = 1; i < params.length; i++) { + uri.append('&').append(params[i].key).append('=').append(params[i].value); + } + } + } + + static URL buildURL(String scheme, String host, int port, String path, Param[] params) { + StringBuilder builder = new StringBuilder(scheme).append(host).append(':').append(port).append(path); + appendParams(builder, params); + + URL result = null; + try { + result = new URL(builder.toString()); + } catch (MalformedURLException e) {} + return result; + } + + static URL buildURL(String uri, Param[] params) { + StringBuilder builder = new StringBuilder(uri); + appendParams(builder, params); + + URL result = null; + try { + result = new URL(builder.toString()); + } catch (MalformedURLException e) {} + return result; + } + + /** + * A RequestBody wrapping a byte array + */ + public static class ByteArrayRequestBody implements HttpCore.RequestBody { + public ByteArrayRequestBody(byte[] bytes, String contentType) { this.bytes = bytes; this.contentType = contentType; } + + @Override + public byte[] getEncoded() { return bytes; } + @Override + public String getContentType() { return contentType; } + + private final byte[] bytes; + private final String contentType; + } + + public static class FormRequestBody implements HttpCore.RequestBody { + public FormRequestBody(Param[] formData) { + this.formData = formData; + } + + @Override + public byte[] getEncoded() { + try { + StringBuilder body = new StringBuilder(); + for (int i = 0; i < formData.length; i++) { + if (i != 0) + body.append('&'); + body.append(URLEncoder.encode(formData[i].key, "UTF-8")); + body.append('='); + body.append(URLEncoder.encode(formData[i].value, "UTF-8")); + } + return body.toString().getBytes("UTF-8"); + } catch (UnsupportedEncodingException e) { + return new byte[]{}; + } + } + + @Override + public String getContentType() { + return HttpConstants.ContentTypes.FORM_ENCODING; + } + + private Param[] formData; + } + + /** + * A RequestBody wrapping a JSON-serialisable object + */ + public static class JsonRequestBody implements HttpCore.RequestBody { + public JsonRequestBody(String jsonText) { this.jsonText = jsonText; } + public JsonRequestBody(Object ob) { this(Serialisation.gson.toJson(ob)); } + + @Override + public byte[] getEncoded() { return (bytes != null) ? bytes : (bytes = jsonText.getBytes(Charset.forName("UTF-8"))); } + @Override + public String getContentType() { return HttpConstants.ContentTypes.JSON; } + + private final String jsonText; + private byte[] bytes; + } + + public static HttpCore.RequestBody requestBodyFromGson(JsonElement json, boolean useBinaryProtocol) { + if (!useBinaryProtocol) { + return new JsonRequestBody(json); + } + + return new ByteArrayRequestBody(Serialisation.gsonToMsgpack(json), "application/x-msgpack"); + } } diff --git a/lib/src/main/java/io/ably/lib/http/PaginatedQuery.java b/lib/src/main/java/io/ably/lib/http/PaginatedQuery.java index 0a516cf53..7d314a450 100644 --- a/lib/src/main/java/io/ably/lib/http/PaginatedQuery.java +++ b/lib/src/main/java/io/ably/lib/http/PaginatedQuery.java @@ -21,42 +21,42 @@ * @param the body response type. */ public class PaginatedQuery { - private final BasePaginatedQuery base; + private final BasePaginatedQuery base; - /** - * Construct a PaginatedQuery - * - * @param http. the http instance - * @param path. the path of the resource being queried - * @param headers. headers to pass into the first and all relative queries - * @param params. params to pass into the initial query - * @param bodyHandler. handler to parse response bodies for first and all relative queries - */ - public PaginatedQuery(Http http, String path, Param[] headers, Param[] params, HttpCore.BodyHandler bodyHandler) { - this(http, path, headers, params, null, bodyHandler); - } + /** + * Construct a PaginatedQuery + * + * @param http. the http instance + * @param path. the path of the resource being queried + * @param headers. headers to pass into the first and all relative queries + * @param params. params to pass into the initial query + * @param bodyHandler. handler to parse response bodies for first and all relative queries + */ + public PaginatedQuery(Http http, String path, Param[] headers, Param[] params, HttpCore.BodyHandler bodyHandler) { + this(http, path, headers, params, null, bodyHandler); + } - /** - * Construct a PaginatedQuery - * - * @param http. the http instance - * @param path. the path of the resource being queried - * @param headers. headers to pass into the first and all relative queries - * @param params. params to pass into the initial query - * @param bodyHandler. handler to parse response bodies for first and all relative queries - */ - public PaginatedQuery(Http http, String path, Param[] headers, Param[] params, HttpCore.RequestBody requestBody, HttpCore.BodyHandler bodyHandler) { - base = new BasePaginatedQuery(http, path, headers, params, requestBody, bodyHandler); - } + /** + * Construct a PaginatedQuery + * + * @param http. the http instance + * @param path. the path of the resource being queried + * @param headers. headers to pass into the first and all relative queries + * @param params. params to pass into the initial query + * @param bodyHandler. handler to parse response bodies for first and all relative queries + */ + public PaginatedQuery(Http http, String path, Param[] headers, Param[] params, HttpCore.RequestBody requestBody, HttpCore.BodyHandler bodyHandler) { + base = new BasePaginatedQuery(http, path, headers, params, requestBody, bodyHandler); + } - /** - * Get the result of the first query - * @return A PaginatedResult giving the first page of results - * together with any available links to related results pages. - * @throws AblyException - */ - public PaginatedResult get() throws AblyException { - return base.get().sync(); - } + /** + * Get the result of the first query + * @return A PaginatedResult giving the first page of results + * together with any available links to related results pages. + * @throws AblyException + */ + public PaginatedResult get() throws AblyException { + return base.get().sync(); + } } diff --git a/lib/src/main/java/io/ably/lib/http/SyncHttpScheduler.java b/lib/src/main/java/io/ably/lib/http/SyncHttpScheduler.java index 0f6cb50d5..8634be78d 100644 --- a/lib/src/main/java/io/ably/lib/http/SyncHttpScheduler.java +++ b/lib/src/main/java/io/ably/lib/http/SyncHttpScheduler.java @@ -7,7 +7,7 @@ */ public class SyncHttpScheduler extends HttpScheduler { - public SyncHttpScheduler(HttpCore httpCore) { - super(httpCore, CurrentThreadExecutor.INSTANCE); - } + public SyncHttpScheduler(HttpCore httpCore) { + super(httpCore, CurrentThreadExecutor.INSTANCE); + } } diff --git a/lib/src/main/java/io/ably/lib/push/PushBase.java b/lib/src/main/java/io/ably/lib/push/PushBase.java index 3dc86eca5..17ad98f05 100644 --- a/lib/src/main/java/io/ably/lib/push/PushBase.java +++ b/lib/src/main/java/io/ably/lib/push/PushBase.java @@ -14,352 +14,352 @@ public class PushBase { - public PushBase(AblyBase rest) { - this.rest = rest; - this.admin = new Admin(rest); - } - - public static class Admin { - public final DeviceRegistrations deviceRegistrations; - public final ChannelSubscriptions channelSubscriptions; - - Admin(AblyBase rest) { - this.rest = rest; - this.deviceRegistrations = new DeviceRegistrations(rest); - this.channelSubscriptions = new ChannelSubscriptions(rest); - } - - public void publish(Param[] recipient, JsonObject payload) throws AblyException { - publishImpl(recipient, payload).sync(); - } - - public void publishAsync(Param[] recipient, JsonObject payload, final CompletionListener listener) { - publishImpl(recipient, payload).async(new CompletionListener.ToCallback(listener)); - } - - private Http.Request publishImpl(final Param[] recipient, final JsonObject payload) { - return rest.http.request(new Http.Execute() { - @Override - public void execute(HttpScheduler http, Callback callback) throws AblyException { - if (recipient == null || recipient.length == 0) { - throw AblyException.fromThrowable(new Exception("recipient cannot be empty")); - } - if (payload == null || payload.entrySet().isEmpty()) { - throw AblyException.fromThrowable(new Exception("payload cannot be empty")); - } - - JsonObject recipientJson = new JsonObject(); - for (Param param : recipient) { - recipientJson.addProperty(param.key, param.value); - } - JsonObject bodyJson = new JsonObject(); - bodyJson.add("recipient", recipientJson); - for (Map.Entry entry : payload.entrySet()) { - bodyJson.add(entry.getKey(), entry.getValue()); - } - HttpCore.RequestBody body = HttpUtils.requestBodyFromGson(bodyJson, rest.options.useBinaryProtocol); - - Param[] params = null; - if (rest.options.pushFullWait) { - params = Param.push(params, "fullWait", "true"); - } - - http.post("/push/publish", HttpUtils.defaultAcceptHeaders(rest.options.useBinaryProtocol), params, body, null, true, callback); - } - }); - } - - private final AblyBase rest; - } - - public static class DeviceRegistrations { - public DeviceDetails save(DeviceDetails device) throws AblyException { - return saveImpl(device).sync(); - } - - public void saveAsync(DeviceDetails device, final Callback callback) { - saveImpl(device).async(callback); - } - - protected Http.Request saveImpl(final DeviceDetails device) { - final HttpCore.RequestBody body = HttpUtils.requestBodyFromGson(device.toJsonObject(), rest.options.useBinaryProtocol); - return rest.http.request(new Http.Execute() { - @Override - public void execute(HttpScheduler http, Callback callback) throws AblyException { - Param[] params = null; - if (rest.options.pushFullWait) { - params = Param.push(params, "fullWait", "true"); - } - http.put("/push/deviceRegistrations/" + device.id, rest.push.pushRequestHeaders(device.id), params, body, DeviceDetails.httpResponseHandler, true, callback); - } - }); - } - - public DeviceDetails get(String deviceId) throws AblyException { - return getImpl(deviceId).sync(); - } - - public void getAsync(String deviceId, final Callback callback) { - getImpl(deviceId).async(callback); - } - - protected Http.Request getImpl(final String deviceId) { - return rest.http.request(new Http.Execute() { - @Override - public void execute(HttpScheduler http, Callback callback) throws AblyException { - Param[] params = null; - if (rest.options.pushFullWait) { - params = Param.push(params, "fullWait", "true"); - } - http.get("/push/deviceRegistrations/" + deviceId, rest.push.pushRequestHeaders(deviceId), params, DeviceDetails.httpResponseHandler, true, callback); - } - }); - } - - public PaginatedResult list(Param[] params) throws AblyException { - return listImpl(params).sync(); - } - - public void listAsync(Param[] params, Callback> callback) { - listImpl(params).async(callback); - } - - protected BasePaginatedQuery.ResultRequest listImpl(Param[] params) { - return new BasePaginatedQuery(rest.http, "/push/deviceRegistrations", HttpUtils.defaultAcceptHeaders(rest.options.useBinaryProtocol), params, DeviceDetails.httpBodyHandler).get(); - } - - public void remove(DeviceDetails device) throws AblyException { - remove(device.id); - } - - public void removeAsync(DeviceDetails device, CompletionListener listener) { - removeAsync(device.id, listener); - } - - public void remove(String deviceId) throws AblyException { - removeImpl(deviceId).sync(); - } - - public void removeAsync(String deviceId, CompletionListener listener) { - removeImpl(deviceId).async(new CompletionListener.ToCallback(listener)); - } - - protected Http.Request removeImpl(final String deviceId) { - return rest.http.request(new Http.Execute() { - @Override - public void execute(HttpScheduler http, Callback callback) throws AblyException { - Param[] params = null; - if (rest.options.pushFullWait) { - params = Param.push(params, "fullWait", "true"); - } - http.del("/push/deviceRegistrations/" + deviceId, rest.push.pushRequestHeaders(deviceId), params, null, true, callback); - } - }); - } - - public void removeWhere(Param[] params) throws AblyException { - removeWhereImpl(params).sync(); - } - - public void removeWhereAsync(Param[] params, CompletionListener listener) { - removeWhereImpl(params).async(new CompletionListener.ToCallback(listener)); - } - - protected Http.Request removeWhereImpl(Param[] params) { - if (rest.options.pushFullWait) { - params = Param.push(params, "fullWait", "true"); - } - final Param[] finalParams = params; - return rest.http.request(new Http.Execute() { - @Override - public void execute(HttpScheduler http, Callback callback) throws AblyException { - http.del("/push/deviceRegistrations", HttpUtils.defaultAcceptHeaders(rest.options.useBinaryProtocol), finalParams, null, true, callback); - } - }); - } - - DeviceRegistrations(AblyBase rest) { - this.rest = rest; - } - - private final AblyBase rest; - } - - public static class ChannelSubscriptions { - public ChannelSubscription save(ChannelSubscription subscription) throws AblyException { - return saveImpl(subscription).sync(); - } - - public void saveAsync(ChannelSubscription subscription, final Callback callback) { - saveImpl(subscription).async(callback); - } - - protected Http.Request saveImpl(final ChannelSubscription subscription) { - final HttpCore.RequestBody body = HttpUtils.requestBodyFromGson(subscription.toJsonObject(), rest.options.useBinaryProtocol); - return rest.http.request(new Http.Execute() { - @Override - public void execute(HttpScheduler http, Callback callback) throws AblyException { - Param[] params = null; - if (rest.options.pushFullWait) { - params = Param.push(params, "fullWait", "true"); - } - http.post("/push/channelSubscriptions", rest.push.pushRequestHeaders(subscription.deviceId), params, body, ChannelSubscription.httpResponseHandler, true, callback); - } - }); - } - - public PaginatedResult list(Param[] params) throws AblyException { - return listImpl(params).sync(); - } - - public void listAsync(Param[] params, Callback> callback) { - listImpl(params).async(callback); - } - - protected BasePaginatedQuery.ResultRequest listImpl(Param[] params) { - String deviceId = HttpUtils.getParam(params, "deviceId"); - return new BasePaginatedQuery(rest.http, "/push/channelSubscriptions", rest.push.pushRequestHeaders(deviceId), params, ChannelSubscription.httpBodyHandler).get(); - } - - public void remove(ChannelSubscription subscription) throws AblyException { - removeImpl(subscription).sync(); - } - - public void removeAsync(ChannelSubscription subscription, CompletionListener listener) { - removeImpl(subscription).async(new CompletionListener.ToCallback(listener)); - } - - protected Http.Request removeImpl(ChannelSubscription subscription) { - Param[] params = new Param[] { new Param("channel", subscription.channel) }; - if (subscription.deviceId != null) { - params = Param.push(params, "deviceId", subscription.deviceId); - } else if (subscription.clientId != null) { - params = Param.push(params, "clientId", subscription.clientId); - } else { - return rest.http.failedRequest(AblyException.fromThrowable(new Exception("ChannelSubscription cannot be for both a deviceId and a clientId"))); - } - - return removeWhereImpl(params); - } - - - public void removeWhere(Param[] params) throws AblyException { - removeWhereImpl(params).sync(); - } - - public void removeWhereAsync(Param[] params, CompletionListener listener) { - removeWhereImpl(params).async(new CompletionListener.ToCallback(listener)); - } - - protected Http.Request removeWhereImpl(Param[] params) { - String deviceId = HttpUtils.getParam(params, "deviceId"); - if (rest.options.pushFullWait) { - params = Param.push(params, "fullWait", "true"); - } - final Param[] finalHeaders = rest.push.pushRequestHeaders(deviceId); - final Param[] finalParams = params; - return rest.http.request(new Http.Execute() { - @Override - public void execute(HttpScheduler http, Callback callback) throws AblyException { - http.del("/push/channelSubscriptions", finalHeaders, finalParams, null, true, callback); - } - }); - } - - public PaginatedResult listChannels(Param[] params) throws AblyException { - return listChannelsImpl(params).sync(); - } - - public void listChannelsAsync(Param[] params, Callback> callback) { - listChannelsImpl(params).async(callback); - } - - protected BasePaginatedQuery.ResultRequest listChannelsImpl(Param[] params) { - String deviceId = HttpUtils.getParam(params, "deviceId"); - return new BasePaginatedQuery(rest.http, "/push/channels", rest.push.pushRequestHeaders(deviceId), params, StringUtils.httpBodyHandler).get(); - } - - ChannelSubscriptions(AblyBase rest) { - this.rest = rest; - } - - private final AblyBase rest; - } - - public static class ChannelSubscription { - public final String channel; - public final String deviceId; - public final String clientId; - - public static ChannelSubscription forDevice(String channel, String deviceId) { - return new ChannelSubscription(channel, deviceId, null); - } - - public static ChannelSubscription forClientId(String channel, String clientId) { - return new ChannelSubscription(channel, null, clientId); - } - - private ChannelSubscription(String channel, String deviceId, String clientId) { - this.channel = channel; - this.deviceId = deviceId; - this.clientId = clientId; - } - - public JsonObject toJsonObject() { - JsonObject o = new JsonObject(); - - o.addProperty("channel", channel); - if (clientId != null) { - o.addProperty("clientId", clientId); - } - if (deviceId != null) { - o.addProperty("deviceId", deviceId); - } - - return o; - } - - @Override - public boolean equals(Object o) { - if (!(o instanceof ChannelSubscription)) { - return false; - } - ChannelSubscription other = (ChannelSubscription) o; - JsonObject thisJson = this.toJsonObject(); - JsonObject otherJson = other.toJsonObject(); - - return thisJson.equals(otherJson); - } - - @Override - public String toString() { - return this.toJsonObject().toString(); - } - - public static ChannelSubscription fromJsonObject(JsonObject o) { - return Serialisation.gson.fromJson(o, ChannelSubscription.class); - } - - private static Serialisation.FromJsonElement fromJsonElement = new Serialisation.FromJsonElement() { - @Override - public ChannelSubscription fromJsonElement(JsonElement e) { - return fromJsonObject((JsonObject) e); - } - }; - - protected static HttpCore.ResponseHandler httpResponseHandler = new Serialisation.HttpResponseHandler(ChannelSubscription.class, fromJsonElement); - - protected static HttpCore.BodyHandler httpBodyHandler = new Serialisation.HttpBodyHandler(ChannelSubscription[].class, fromJsonElement); - } - - Param[] pushRequestHeaders(boolean forLocalDevice) { - return HttpUtils.defaultAcceptHeaders(rest.options.useBinaryProtocol); - } - - Param[] pushRequestHeaders(String deviceId) { - return pushRequestHeaders(false); - } - - protected final AblyBase rest; - public final Admin admin; + public PushBase(AblyBase rest) { + this.rest = rest; + this.admin = new Admin(rest); + } + + public static class Admin { + public final DeviceRegistrations deviceRegistrations; + public final ChannelSubscriptions channelSubscriptions; + + Admin(AblyBase rest) { + this.rest = rest; + this.deviceRegistrations = new DeviceRegistrations(rest); + this.channelSubscriptions = new ChannelSubscriptions(rest); + } + + public void publish(Param[] recipient, JsonObject payload) throws AblyException { + publishImpl(recipient, payload).sync(); + } + + public void publishAsync(Param[] recipient, JsonObject payload, final CompletionListener listener) { + publishImpl(recipient, payload).async(new CompletionListener.ToCallback(listener)); + } + + private Http.Request publishImpl(final Param[] recipient, final JsonObject payload) { + return rest.http.request(new Http.Execute() { + @Override + public void execute(HttpScheduler http, Callback callback) throws AblyException { + if (recipient == null || recipient.length == 0) { + throw AblyException.fromThrowable(new Exception("recipient cannot be empty")); + } + if (payload == null || payload.entrySet().isEmpty()) { + throw AblyException.fromThrowable(new Exception("payload cannot be empty")); + } + + JsonObject recipientJson = new JsonObject(); + for (Param param : recipient) { + recipientJson.addProperty(param.key, param.value); + } + JsonObject bodyJson = new JsonObject(); + bodyJson.add("recipient", recipientJson); + for (Map.Entry entry : payload.entrySet()) { + bodyJson.add(entry.getKey(), entry.getValue()); + } + HttpCore.RequestBody body = HttpUtils.requestBodyFromGson(bodyJson, rest.options.useBinaryProtocol); + + Param[] params = null; + if (rest.options.pushFullWait) { + params = Param.push(params, "fullWait", "true"); + } + + http.post("/push/publish", HttpUtils.defaultAcceptHeaders(rest.options.useBinaryProtocol), params, body, null, true, callback); + } + }); + } + + private final AblyBase rest; + } + + public static class DeviceRegistrations { + public DeviceDetails save(DeviceDetails device) throws AblyException { + return saveImpl(device).sync(); + } + + public void saveAsync(DeviceDetails device, final Callback callback) { + saveImpl(device).async(callback); + } + + protected Http.Request saveImpl(final DeviceDetails device) { + final HttpCore.RequestBody body = HttpUtils.requestBodyFromGson(device.toJsonObject(), rest.options.useBinaryProtocol); + return rest.http.request(new Http.Execute() { + @Override + public void execute(HttpScheduler http, Callback callback) throws AblyException { + Param[] params = null; + if (rest.options.pushFullWait) { + params = Param.push(params, "fullWait", "true"); + } + http.put("/push/deviceRegistrations/" + device.id, rest.push.pushRequestHeaders(device.id), params, body, DeviceDetails.httpResponseHandler, true, callback); + } + }); + } + + public DeviceDetails get(String deviceId) throws AblyException { + return getImpl(deviceId).sync(); + } + + public void getAsync(String deviceId, final Callback callback) { + getImpl(deviceId).async(callback); + } + + protected Http.Request getImpl(final String deviceId) { + return rest.http.request(new Http.Execute() { + @Override + public void execute(HttpScheduler http, Callback callback) throws AblyException { + Param[] params = null; + if (rest.options.pushFullWait) { + params = Param.push(params, "fullWait", "true"); + } + http.get("/push/deviceRegistrations/" + deviceId, rest.push.pushRequestHeaders(deviceId), params, DeviceDetails.httpResponseHandler, true, callback); + } + }); + } + + public PaginatedResult list(Param[] params) throws AblyException { + return listImpl(params).sync(); + } + + public void listAsync(Param[] params, Callback> callback) { + listImpl(params).async(callback); + } + + protected BasePaginatedQuery.ResultRequest listImpl(Param[] params) { + return new BasePaginatedQuery(rest.http, "/push/deviceRegistrations", HttpUtils.defaultAcceptHeaders(rest.options.useBinaryProtocol), params, DeviceDetails.httpBodyHandler).get(); + } + + public void remove(DeviceDetails device) throws AblyException { + remove(device.id); + } + + public void removeAsync(DeviceDetails device, CompletionListener listener) { + removeAsync(device.id, listener); + } + + public void remove(String deviceId) throws AblyException { + removeImpl(deviceId).sync(); + } + + public void removeAsync(String deviceId, CompletionListener listener) { + removeImpl(deviceId).async(new CompletionListener.ToCallback(listener)); + } + + protected Http.Request removeImpl(final String deviceId) { + return rest.http.request(new Http.Execute() { + @Override + public void execute(HttpScheduler http, Callback callback) throws AblyException { + Param[] params = null; + if (rest.options.pushFullWait) { + params = Param.push(params, "fullWait", "true"); + } + http.del("/push/deviceRegistrations/" + deviceId, rest.push.pushRequestHeaders(deviceId), params, null, true, callback); + } + }); + } + + public void removeWhere(Param[] params) throws AblyException { + removeWhereImpl(params).sync(); + } + + public void removeWhereAsync(Param[] params, CompletionListener listener) { + removeWhereImpl(params).async(new CompletionListener.ToCallback(listener)); + } + + protected Http.Request removeWhereImpl(Param[] params) { + if (rest.options.pushFullWait) { + params = Param.push(params, "fullWait", "true"); + } + final Param[] finalParams = params; + return rest.http.request(new Http.Execute() { + @Override + public void execute(HttpScheduler http, Callback callback) throws AblyException { + http.del("/push/deviceRegistrations", HttpUtils.defaultAcceptHeaders(rest.options.useBinaryProtocol), finalParams, null, true, callback); + } + }); + } + + DeviceRegistrations(AblyBase rest) { + this.rest = rest; + } + + private final AblyBase rest; + } + + public static class ChannelSubscriptions { + public ChannelSubscription save(ChannelSubscription subscription) throws AblyException { + return saveImpl(subscription).sync(); + } + + public void saveAsync(ChannelSubscription subscription, final Callback callback) { + saveImpl(subscription).async(callback); + } + + protected Http.Request saveImpl(final ChannelSubscription subscription) { + final HttpCore.RequestBody body = HttpUtils.requestBodyFromGson(subscription.toJsonObject(), rest.options.useBinaryProtocol); + return rest.http.request(new Http.Execute() { + @Override + public void execute(HttpScheduler http, Callback callback) throws AblyException { + Param[] params = null; + if (rest.options.pushFullWait) { + params = Param.push(params, "fullWait", "true"); + } + http.post("/push/channelSubscriptions", rest.push.pushRequestHeaders(subscription.deviceId), params, body, ChannelSubscription.httpResponseHandler, true, callback); + } + }); + } + + public PaginatedResult list(Param[] params) throws AblyException { + return listImpl(params).sync(); + } + + public void listAsync(Param[] params, Callback> callback) { + listImpl(params).async(callback); + } + + protected BasePaginatedQuery.ResultRequest listImpl(Param[] params) { + String deviceId = HttpUtils.getParam(params, "deviceId"); + return new BasePaginatedQuery(rest.http, "/push/channelSubscriptions", rest.push.pushRequestHeaders(deviceId), params, ChannelSubscription.httpBodyHandler).get(); + } + + public void remove(ChannelSubscription subscription) throws AblyException { + removeImpl(subscription).sync(); + } + + public void removeAsync(ChannelSubscription subscription, CompletionListener listener) { + removeImpl(subscription).async(new CompletionListener.ToCallback(listener)); + } + + protected Http.Request removeImpl(ChannelSubscription subscription) { + Param[] params = new Param[] { new Param("channel", subscription.channel) }; + if (subscription.deviceId != null) { + params = Param.push(params, "deviceId", subscription.deviceId); + } else if (subscription.clientId != null) { + params = Param.push(params, "clientId", subscription.clientId); + } else { + return rest.http.failedRequest(AblyException.fromThrowable(new Exception("ChannelSubscription cannot be for both a deviceId and a clientId"))); + } + + return removeWhereImpl(params); + } + + + public void removeWhere(Param[] params) throws AblyException { + removeWhereImpl(params).sync(); + } + + public void removeWhereAsync(Param[] params, CompletionListener listener) { + removeWhereImpl(params).async(new CompletionListener.ToCallback(listener)); + } + + protected Http.Request removeWhereImpl(Param[] params) { + String deviceId = HttpUtils.getParam(params, "deviceId"); + if (rest.options.pushFullWait) { + params = Param.push(params, "fullWait", "true"); + } + final Param[] finalHeaders = rest.push.pushRequestHeaders(deviceId); + final Param[] finalParams = params; + return rest.http.request(new Http.Execute() { + @Override + public void execute(HttpScheduler http, Callback callback) throws AblyException { + http.del("/push/channelSubscriptions", finalHeaders, finalParams, null, true, callback); + } + }); + } + + public PaginatedResult listChannels(Param[] params) throws AblyException { + return listChannelsImpl(params).sync(); + } + + public void listChannelsAsync(Param[] params, Callback> callback) { + listChannelsImpl(params).async(callback); + } + + protected BasePaginatedQuery.ResultRequest listChannelsImpl(Param[] params) { + String deviceId = HttpUtils.getParam(params, "deviceId"); + return new BasePaginatedQuery(rest.http, "/push/channels", rest.push.pushRequestHeaders(deviceId), params, StringUtils.httpBodyHandler).get(); + } + + ChannelSubscriptions(AblyBase rest) { + this.rest = rest; + } + + private final AblyBase rest; + } + + public static class ChannelSubscription { + public final String channel; + public final String deviceId; + public final String clientId; + + public static ChannelSubscription forDevice(String channel, String deviceId) { + return new ChannelSubscription(channel, deviceId, null); + } + + public static ChannelSubscription forClientId(String channel, String clientId) { + return new ChannelSubscription(channel, null, clientId); + } + + private ChannelSubscription(String channel, String deviceId, String clientId) { + this.channel = channel; + this.deviceId = deviceId; + this.clientId = clientId; + } + + public JsonObject toJsonObject() { + JsonObject o = new JsonObject(); + + o.addProperty("channel", channel); + if (clientId != null) { + o.addProperty("clientId", clientId); + } + if (deviceId != null) { + o.addProperty("deviceId", deviceId); + } + + return o; + } + + @Override + public boolean equals(Object o) { + if (!(o instanceof ChannelSubscription)) { + return false; + } + ChannelSubscription other = (ChannelSubscription) o; + JsonObject thisJson = this.toJsonObject(); + JsonObject otherJson = other.toJsonObject(); + + return thisJson.equals(otherJson); + } + + @Override + public String toString() { + return this.toJsonObject().toString(); + } + + public static ChannelSubscription fromJsonObject(JsonObject o) { + return Serialisation.gson.fromJson(o, ChannelSubscription.class); + } + + private static Serialisation.FromJsonElement fromJsonElement = new Serialisation.FromJsonElement() { + @Override + public ChannelSubscription fromJsonElement(JsonElement e) { + return fromJsonObject((JsonObject) e); + } + }; + + protected static HttpCore.ResponseHandler httpResponseHandler = new Serialisation.HttpResponseHandler(ChannelSubscription.class, fromJsonElement); + + protected static HttpCore.BodyHandler httpBodyHandler = new Serialisation.HttpBodyHandler(ChannelSubscription[].class, fromJsonElement); + } + + Param[] pushRequestHeaders(boolean forLocalDevice) { + return HttpUtils.defaultAcceptHeaders(rest.options.useBinaryProtocol); + } + + Param[] pushRequestHeaders(String deviceId) { + return pushRequestHeaders(false); + } + + protected final AblyBase rest; + public final Admin admin; } 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 c3eba19f1..79af02ef6 100644 --- a/lib/src/main/java/io/ably/lib/realtime/AblyRealtime.java +++ b/lib/src/main/java/io/ably/lib/realtime/AblyRealtime.java @@ -25,187 +25,187 @@ */ public class AblyRealtime extends AblyRest implements AutoCloseable { - /** - * The {@link Connection} object for this instance. - */ - public final Connection connection; - - public final Channels channels; - - /** - * Instance the Ably library using a key only. - * This is simply a convenience constructor for the - * simplest case of instancing the library with a key - * for basic authentication and no other options. - * @param key; String key (obtained from application dashboard) - * @throws AblyException - */ - public AblyRealtime(String key) throws AblyException { - this(new ClientOptions(key)); - } - - /** - * Instance the Ably library with the given options. - * @param options: see {@link io.ably.lib.types.ClientOptions} for options - * @throws AblyException - */ - public AblyRealtime(ClientOptions options) throws AblyException { - super(options); - final InternalChannels channels = new InternalChannels(); - this.channels = channels; - connection = new Connection(this, channels); - - /* remove all channels when the connection is closed, to avoid stalled state */ - connection.on(ConnectionEvent.closed, new ConnectionStateListener() { - @Override - public void onConnectionStateChanged(ConnectionStateListener.ConnectionStateChange state) { - channels.clear(); - } - }); - - if(options.autoConnect) connection.connect(); - } - - /** - * Initiate a connection. - * {@link Connection#connect}. - */ - public void connect() { - connection.connect(); - } - - /** - * Close this instance. This closes the connection. - * The connection can be re-opened by calling - * {@link Connection#connect}. - */ - @Override - public void close() { - connection.close(); - } - - /** - * Authentication token has changed. - */ - @Override - protected void onAuthUpdated(String token, boolean waitForResponse) throws AblyException { - connection.connectionManager.onAuthUpdated(token, waitForResponse); - } - - /** - * Authentication error occurred - */ - protected void onAuthError(ErrorInfo errorInfo) { - connection.connectionManager.onAuthError(errorInfo); - } - - /** - * A collection of Channels associated with this Ably Realtime instance. - */ - public interface Channels extends ReadOnlyMap { - /** - * Get the named channel; if it does not already exist, - * create it with default options. - * @param channelName the name of the channel - * @return the channel - */ - Channel get(String channelName); - - /** - * Get the named channel and set the given options, creating it - * if it does not already exist. - * @param channelName the name of the channel - * @param channelOptions the options to set (null to clear options on an existing channel) - * @return the channel - * @throws AblyException - */ - Channel get(String channelName, ChannelOptions channelOptions) throws AblyException; - - /** - * Remove this channel from this AblyRealtime instance. This detaches from the channel - * and releases all other resources associated with the channel in this client. - * This silently does nothing if the channel does not already exist. - * @param channelName the name of the channel - */ - void release(String channelName); - } - - private class InternalChannels extends InternalMap implements Channels, ConnectionManager.Channels { - private InternalChannels() { - super(new ConcurrentHashMap()); - } - - /** - * Get the named channel; if it does not already exist, - * create it with default options. - * @param channelName the name of the channel - * @return the channel - */ - @Override - public Channel get(String channelName) { - try { - return get(channelName, null); - } catch (AblyException e) { return null; } - } - - @Override - public Channel get(String channelName, ChannelOptions channelOptions) throws AblyException { - Channel channel = map.get(channelName); - if (channel != null) { - if (channelOptions != null) { - if (channel.shouldReattachToSetOptions(channelOptions)) { - throw AblyException.fromErrorInfo(new ErrorInfo("Channels.get() cannot be used to set channel options that would cause the channel to reattach. Please, use Channel.setOptions() instead.", 40000, 400)); - } - channel.setOptions(channelOptions); - } - return channel; - } - - channel = new Channel(AblyRealtime.this, channelName, channelOptions); - map.put(channelName, channel); - return channel; - } - - @Override - public void release(String channelName) { - Channel channel = map.remove(channelName); - if(channel != null) { - try { - channel.detach(); - } catch (AblyException e) { - Log.e(TAG, "Unexpected exception detaching channel; channelName = " + channelName, e); - } - } - } - - @Override - public void onMessage(ProtocolMessage msg) { - String channelName = msg.channel; - Channel channel; - synchronized(this) { channel = channels.get(channelName); } - if(channel == null) { - Log.e(TAG, "Received channel message for non-existent channel"); - return; - } - channel.onChannelMessage(msg); - } - - @Override - public void suspendAll(ErrorInfo error, boolean notifyStateChange) { - for(Iterator> it = map.entrySet().iterator(); it.hasNext(); ) { - Map.Entry entry = it.next(); - entry.getValue().setSuspended(error, notifyStateChange); - } - } - - private void clear() { - map.clear(); - } - } - - /******************** - * internal - ********************/ - - private static final String TAG = AblyRealtime.class.getName(); + /** + * The {@link Connection} object for this instance. + */ + public final Connection connection; + + public final Channels channels; + + /** + * Instance the Ably library using a key only. + * This is simply a convenience constructor for the + * simplest case of instancing the library with a key + * for basic authentication and no other options. + * @param key; String key (obtained from application dashboard) + * @throws AblyException + */ + public AblyRealtime(String key) throws AblyException { + this(new ClientOptions(key)); + } + + /** + * Instance the Ably library with the given options. + * @param options: see {@link io.ably.lib.types.ClientOptions} for options + * @throws AblyException + */ + public AblyRealtime(ClientOptions options) throws AblyException { + super(options); + final InternalChannels channels = new InternalChannels(); + this.channels = channels; + connection = new Connection(this, channels); + + /* remove all channels when the connection is closed, to avoid stalled state */ + connection.on(ConnectionEvent.closed, new ConnectionStateListener() { + @Override + public void onConnectionStateChanged(ConnectionStateListener.ConnectionStateChange state) { + channels.clear(); + } + }); + + if(options.autoConnect) connection.connect(); + } + + /** + * Initiate a connection. + * {@link Connection#connect}. + */ + public void connect() { + connection.connect(); + } + + /** + * Close this instance. This closes the connection. + * The connection can be re-opened by calling + * {@link Connection#connect}. + */ + @Override + public void close() { + connection.close(); + } + + /** + * Authentication token has changed. + */ + @Override + protected void onAuthUpdated(String token, boolean waitForResponse) throws AblyException { + connection.connectionManager.onAuthUpdated(token, waitForResponse); + } + + /** + * Authentication error occurred + */ + protected void onAuthError(ErrorInfo errorInfo) { + connection.connectionManager.onAuthError(errorInfo); + } + + /** + * A collection of Channels associated with this Ably Realtime instance. + */ + public interface Channels extends ReadOnlyMap { + /** + * Get the named channel; if it does not already exist, + * create it with default options. + * @param channelName the name of the channel + * @return the channel + */ + Channel get(String channelName); + + /** + * Get the named channel and set the given options, creating it + * if it does not already exist. + * @param channelName the name of the channel + * @param channelOptions the options to set (null to clear options on an existing channel) + * @return the channel + * @throws AblyException + */ + Channel get(String channelName, ChannelOptions channelOptions) throws AblyException; + + /** + * Remove this channel from this AblyRealtime instance. This detaches from the channel + * and releases all other resources associated with the channel in this client. + * This silently does nothing if the channel does not already exist. + * @param channelName the name of the channel + */ + void release(String channelName); + } + + private class InternalChannels extends InternalMap implements Channels, ConnectionManager.Channels { + private InternalChannels() { + super(new ConcurrentHashMap()); + } + + /** + * Get the named channel; if it does not already exist, + * create it with default options. + * @param channelName the name of the channel + * @return the channel + */ + @Override + public Channel get(String channelName) { + try { + return get(channelName, null); + } catch (AblyException e) { return null; } + } + + @Override + public Channel get(String channelName, ChannelOptions channelOptions) throws AblyException { + Channel channel = map.get(channelName); + if (channel != null) { + if (channelOptions != null) { + if (channel.shouldReattachToSetOptions(channelOptions)) { + throw AblyException.fromErrorInfo(new ErrorInfo("Channels.get() cannot be used to set channel options that would cause the channel to reattach. Please, use Channel.setOptions() instead.", 40000, 400)); + } + channel.setOptions(channelOptions); + } + return channel; + } + + channel = new Channel(AblyRealtime.this, channelName, channelOptions); + map.put(channelName, channel); + return channel; + } + + @Override + public void release(String channelName) { + Channel channel = map.remove(channelName); + if(channel != null) { + try { + channel.detach(); + } catch (AblyException e) { + Log.e(TAG, "Unexpected exception detaching channel; channelName = " + channelName, e); + } + } + } + + @Override + public void onMessage(ProtocolMessage msg) { + String channelName = msg.channel; + Channel channel; + synchronized(this) { channel = channels.get(channelName); } + if(channel == null) { + Log.e(TAG, "Received channel message for non-existent channel"); + return; + } + channel.onChannelMessage(msg); + } + + @Override + public void suspendAll(ErrorInfo error, boolean notifyStateChange) { + for(Iterator> it = map.entrySet().iterator(); it.hasNext(); ) { + Map.Entry entry = it.next(); + entry.getValue().setSuspended(error, notifyStateChange); + } + } + + private void clear() { + map.clear(); + } + } + + /******************** + * internal + ********************/ + + private static final String TAG = AblyRealtime.class.getName(); } diff --git a/lib/src/main/java/io/ably/lib/realtime/ChannelBase.java b/lib/src/main/java/io/ably/lib/realtime/ChannelBase.java index 365380576..8b84a3081 100644 --- a/lib/src/main/java/io/ably/lib/realtime/ChannelBase.java +++ b/lib/src/main/java/io/ably/lib/realtime/ChannelBase.java @@ -46,1158 +46,1158 @@ */ public abstract class ChannelBase extends EventEmitter { - /************************************ - * ChannelState and state management - ************************************/ - - /** - * The name of this channel. - */ - public final String name; - - /** - * The {@link Presence} object for this channel. This controls this client's - * presence on the channel and may also be used to obtain presence information - * and change events for other members of the channel. - */ - public final Presence presence; - - /** - * The current channel state. - */ - public ChannelState state; - - /** - * Error information associated with a failed channel state. - */ - public ErrorInfo reason; - - /** - * Properties of Channel - */ - public ChannelProperties properties = new ChannelProperties(); - - /*** - * internal - * - */ - private void setState(ChannelState newState, ErrorInfo reason) { - setState(newState, reason, false, true); - } - private void setState(ChannelState newState, ErrorInfo reason, boolean resumed) { - setState(newState, reason, resumed, true); - } - private void setState(ChannelState newState, ErrorInfo reason, boolean resumed, boolean notifyStateChange) { - Log.v(TAG, "setState(): channel = " + name + "; setting " + newState); - ChannelStateListener.ChannelStateChange stateChange; - synchronized(this) { - stateChange = new ChannelStateListener.ChannelStateChange(newState, this.state, reason, resumed); - this.state = stateChange.current; - this.reason = stateChange.reason; - } - - if(notifyStateChange) { - /* broadcast state change */ - emit(newState, stateChange); - } - } - - /************************************ - * attach / detach - ************************************/ - - /** - * Attach to this channel. - * This call initiates the attach request, and the response - * is indicated asynchronously in the resulting state change. - * attach() is called implicitly when publishing or subscribing - * on this channel, so it is not usually necessary for a client - * to call attach() explicitly. - * @throws AblyException - */ - public void attach() throws AblyException { - attach(null); - } - - /** - * Attach to this channel. - * This call initiates the attach request, and the response - * is indicated asynchronously in the resulting state change. - * attach() is called implicitly when publishing or subscribing - * on this channel, so it is not usually necessary for a client - * to call attach() explicitly. - * - * @param listener When the channel is attached successfully or the attach fails and - * the ErrorInfo error is passed as an argument to the callback - * @throws AblyException - */ - public void attach(CompletionListener listener) throws AblyException { - this.attach(false, listener); - } - - private void attach(boolean forceReattach, CompletionListener listener) { - clearAttachTimers(); - attachWithTimeout(forceReattach, listener); - } - - private boolean attachResume; - - private void attachImpl(final boolean forceReattach, final CompletionListener listener) throws AblyException { - Log.v(TAG, "attach(); channel = " + name); - if(!forceReattach) { - /* check preconditions */ - switch(state) { - case attaching: - if(listener != null) { - on(new ChannelStateCompletionListener(listener, ChannelState.attached, ChannelState.failed)); - } - return; - case attached: - callCompletionListenerSuccess(listener); - return; - default: - } - } - ConnectionManager connectionManager = ably.connection.connectionManager; - if(!connectionManager.isActive()) { - throw AblyException.fromErrorInfo(connectionManager.getStateErrorInfo()); - } - - /* send attach request and pending state */ - Log.v(TAG, "attach(); channel = " + name + "; sending ATTACH request"); - ProtocolMessage attachMessage = new ProtocolMessage(Action.attach, this.name); - if(this.options != null) { - if(this.options.hasParams()) { - attachMessage.params = CollectionUtils.copy(this.options.params); - } - if(this.options.hasModes()) { - attachMessage.setFlags(options.getModeFlags()); - } - } - if(this.decodeFailureRecoveryInProgress) { - attachMessage.channelSerial = this.lastPayloadProtocolMessageChannelSerial; - } - try { - if (listener != null) { - on(new ChannelStateCompletionListener(listener, ChannelState.attached, ChannelState.failed)); - } - if (this.attachResume) { - attachMessage.setFlag(Flag.attach_resume); - } - - setState(ChannelState.attaching, null); - connectionManager.send(attachMessage, true, null); - } catch(AblyException e) { - throw e; - } - } - - /** - * Detach from this channel. - * This call initiates the detach request, and the response - * is indicated asynchronously in the resulting state change. - * @throws AblyException - */ - public void detach() throws AblyException { - detach(null); - } - - /** - * Detach from this channel. - * This call initiates the detach request, and the response - * is indicated asynchronously in the resulting state change. - * @throws AblyException - */ - public void detach(CompletionListener listener) throws AblyException { - clearAttachTimers(); - detachWithTimeout(listener); - } - - private void detachImpl(CompletionListener listener) throws AblyException { - Log.v(TAG, "detach(); channel = " + name); - /* check preconditions */ - switch(state) { - case initialized: - case detached: { - callCompletionListenerSuccess(listener); - return; - } - case detaching: - if (listener != null) { - on(new ChannelStateCompletionListener(listener, ChannelState.detached, ChannelState.failed)); - } - return; - default: - } - ConnectionManager connectionManager = ably.connection.connectionManager; - if(!connectionManager.isActive()) - throw AblyException.fromErrorInfo(connectionManager.getStateErrorInfo()); - - /* send detach request */ - ProtocolMessage detachMessage = new ProtocolMessage(Action.detach, this.name); - try { - if (listener != null) { - on(new ChannelStateCompletionListener(listener, ChannelState.detached, ChannelState.failed)); - } - - this.attachResume = false; - setState(ChannelState.detaching, null); - connectionManager.send(detachMessage, true, null); - } catch(AblyException e) { - throw e; - } - } - - public void sync() throws AblyException { - Log.v(TAG, "sync(); channel = " + name); - /* check preconditions */ - switch(state) { - case initialized: - case detaching: - case detached: - throw AblyException.fromErrorInfo(new ErrorInfo("Unable to sync to channel; not attached", 40000)); - default: - } - ConnectionManager connectionManager = ably.connection.connectionManager; - if(!connectionManager.isActive()) - throw AblyException.fromErrorInfo(connectionManager.getStateErrorInfo()); - - /* send sync request */ - ProtocolMessage syncMessage = new ProtocolMessage(Action.sync, this.name); - syncMessage.channelSerial = syncChannelSerial; - connectionManager.send(syncMessage, true, null); - } - - /*** - * internal - * - */ - private static void callCompletionListenerSuccess(CompletionListener listener) { - if(listener != null) { - try { - listener.onSuccess(); - } catch(Throwable t) { - Log.e(TAG, "Unexpected exception calling CompletionListener", t); - } - } - } - - private static void callCompletionListenerError(CompletionListener listener, ErrorInfo err) { - if(listener != null) { - try { - listener.onError(err); - } catch(Throwable t) { - Log.e(TAG, "Unexpected exception calling CompletionListener", t); - } - } - } - - private void setAttached(ProtocolMessage message) { - clearAttachTimers(); - boolean resumed = message.hasFlag(Flag.resumed); - Log.v(TAG, "setAttached(); channel = " + name + ", resumed = " + resumed); - properties.attachSerial = message.channelSerial; - params = message.params; - modes = ChannelMode.toSet(message.flags); - if(state == ChannelState.attached) { - Log.v(TAG, String.format("Server initiated attach for channel %s", name)); - /* emit UPDATE event according to RTL12 */ - emitUpdate(null, resumed); - } else { - this.attachResume = true; - setState(ChannelState.attached, message.error, resumed); - sendQueuedMessages(); - presence.setAttached(message.hasFlag(Flag.has_presence)); - } - } - - private void setDetached(ErrorInfo reason) { - clearAttachTimers(); - Log.v(TAG, "setDetached(); channel = " + name); - presence.setDetached(reason); - setState(ChannelState.detached, reason); - failQueuedMessages(reason); - } - - private void setFailed(ErrorInfo reason) { - clearAttachTimers(); - Log.v(TAG, "setFailed(); channel = " + name); - presence.setDetached(reason); - this.attachResume = false; - setState(ChannelState.failed, reason); - failQueuedMessages(reason); - } - - /* Timer for attach operation */ - private Timer attachTimer; - - /* Timer for reattaching if attach failed */ - private Timer reattachTimer; - - /** - * Cancel attach/reattach timers - */ - synchronized private void clearAttachTimers() { - Timer[] timers = new Timer[]{attachTimer, reattachTimer}; - attachTimer = reattachTimer = null; - for (Timer t: timers) { - if (t != null) { - t.cancel(); - t.purge(); - } - } - } - - private void attachWithTimeout(final CompletionListener listener) throws AblyException { - this.attachWithTimeout(false, listener); - } - - /** - * Attach channel, if not attached within timeout set state to suspended and - * set up timer to reattach it later - */ - synchronized private void attachWithTimeout(final boolean forceReattach, final CompletionListener listener) { - Timer currentAttachTimer; - try { - currentAttachTimer = new Timer(); - } catch(Throwable t) { - /* an exception instancing the timer can arise because the runtime is exiting */ - callCompletionListenerError(listener, ErrorInfo.fromThrowable(t)); - return; - } - attachTimer = currentAttachTimer; - - try { - attachImpl(forceReattach, new CompletionListener() { - @Override - public void onSuccess() { - clearAttachTimers(); - callCompletionListenerSuccess(listener); - } - - @Override - public void onError(ErrorInfo reason) { - clearAttachTimers(); - callCompletionListenerError(listener, reason); - } - }); - } catch(AblyException e) { - attachTimer = null; - callCompletionListenerError(listener, e.errorInfo); - } - - if(attachTimer == null) { - /* operation has already succeeded or failed, no need to set the timer */ - return; - } - - final Timer inProgressTimer = currentAttachTimer; - attachTimer.schedule( - new TimerTask() { - @Override - public void run() { - String errorMessage = String.format("Attach timed out for channel %s", name); - Log.v(TAG, errorMessage); - synchronized (ChannelBase.this) { - if(attachTimer != inProgressTimer) { - return; - } - attachTimer = null; - if(state == ChannelState.attaching) { - setSuspended(new ErrorInfo(errorMessage, 91200), true); - reattachAfterTimeout(); - } - } - } - }, Defaults.realtimeRequestTimeout); - } - - /** - * Must be called in suspended state. Wait for timeout specified in clientOptions, and then - * try to attach the channel - */ - synchronized private void reattachAfterTimeout() { - Timer currentReattachTimer; - try { - currentReattachTimer = new Timer(); - } catch(Throwable t) { - /* an exception instancing the timer can arise because the runtime is exiting */ - return; - } - reattachTimer = currentReattachTimer; - - final Timer inProgressTimer = currentReattachTimer; - reattachTimer.schedule(new TimerTask() { - @Override - public void run() { - synchronized (ChannelBase.this) { - if (inProgressTimer != reattachTimer) { - return; - } - reattachTimer = null; - if (state == ChannelState.suspended) { - try { - attachWithTimeout(null); - } catch (AblyException e) { - Log.e(TAG, "Reattach channel failed; channel = " + name, e); - } - } - } - } - }, ably.options.channelRetryTimeout); - } - - /** - * Try to detach the channel. If the server doesn't confirm the detach operation within realtime - * request timeout return channel to previous state - */ - synchronized private void detachWithTimeout(final CompletionListener listener) { - final ChannelState originalState = state; - Timer currentDetachTimer; - try { - currentDetachTimer = new Timer(); - } catch(Throwable t) { - /* an exception instancing the timer can arise because the runtime is exiting */ - callCompletionListenerError(listener, ErrorInfo.fromThrowable(t)); - return; - } - attachTimer = currentDetachTimer; - - try { - detachImpl(new CompletionListener() { - @Override - public void onSuccess() { - clearAttachTimers(); - callCompletionListenerSuccess(listener); - } - - @Override - public void onError(ErrorInfo reason) { - clearAttachTimers(); - callCompletionListenerError(listener, reason); - } - }); - } catch (AblyException e) { - attachTimer = null; - } - - if(attachTimer == null) { - /* operation has already succeeded or failed, no need to set the timer */ - return; - } - - final Timer inProgressTimer = currentDetachTimer; - attachTimer.schedule(new TimerTask() { - @Override - public void run() { - synchronized (ChannelBase.this) { - if (inProgressTimer != attachTimer) { - return; - } - attachTimer = null; - if (state == ChannelState.detaching) { - ErrorInfo reason = new ErrorInfo("Detach operation timed out", 90007); - callCompletionListenerError(listener, reason); - setState(originalState, reason); - } - } - } - }, Defaults.realtimeRequestTimeout); - } - - /* State changes provoked by ConnectionManager state changes. */ - - public void setConnected() { - if(state == ChannelState.attached) { - try { - sync(); - } catch (AblyException e) { - Log.e(TAG, "setConnected(): Unable to sync; channel = " + name, e); - } - } else if (state == ChannelState.suspended) { - /* (RTL3d) If the connection state enters the CONNECTED state, then - * a SUSPENDED channel will initiate an attach operation. If the - * attach operation for the channel times out and the channel - * returns to the SUSPENDED state (see #RTL4f) - */ - try { - attachWithTimeout(null); - } catch (AblyException e) { - Log.e(TAG, "setConnected(): Unable to initiate attach; channel = " + name, e); - } - } - } - - /** If the connection state enters the FAILED state, then an ATTACHING - * or ATTACHED channel state will transition to FAILED and set the - * Channel#errorReason - */ - public void setConnectionFailed(ErrorInfo reason) { - clearAttachTimers(); - if (state == ChannelState.attached || state == ChannelState.attaching) - setFailed(reason); - } - - /** (RTL3b) If the connection state enters the CLOSED state, then an - * ATTACHING or ATTACHED channel state will transition to DETACHED. */ - public void setConnectionClosed(ErrorInfo reason) { - clearAttachTimers(); - if (state == ChannelState.attached || state == ChannelState.attaching) - setDetached(reason); - } - - /** (RTL3c) If the connection state enters the SUSPENDED state, then an - * ATTACHING or ATTACHED channel state will transition to SUSPENDED. - * (RTN15c3) The client library should initiate an attach for channels - * that are in the SUSPENDED state. For all channels in the ATTACHING - * or ATTACHED state, the client library should fail any previously queued - * messages for that channel and initiate a new attach. - * This also gets called when a connection enters CONNECTED but with a - * non-fatal error for a failed reconnect (RTN16e). */ - public synchronized void setSuspended(ErrorInfo reason, boolean notifyStateChange) { - clearAttachTimers(); - if (state == ChannelState.attached || state == ChannelState.attaching) { - Log.v(TAG, "setSuspended(); channel = " + name); - presence.setSuspended(reason); - setState(ChannelState.suspended, reason, false, notifyStateChange); - failQueuedMessages(reason); - } - } - - @Override - protected void apply(ChannelStateListener listener, ChannelEvent event, Object... args) { - try { - listener.onChannelStateChanged((ChannelStateListener.ChannelStateChange)args[0]); - } catch (Throwable t) { - Log.e(TAG, "Unexpected exception calling ChannelStateListener", t); - } - } - - static ErrorInfo REASON_NOT_ATTACHED = new ErrorInfo("Channel not attached", 400, 90001); - - /************************************ - * subscriptions and MessageListener - ************************************/ - - /** - * An interface whereby a client maybe notified of message on a channel. - */ - public interface MessageListener { - void onMessage(Message message); - } - - /** - *

- * Unsubscribe all subscribed listeners from this channel. - *

- *

- * Spec: RTL8a - *

- */ - public synchronized void unsubscribe() { - Log.v(TAG, "unsubscribe(); channel = " + this.name); - listeners.clear(); - eventListeners.clear(); - } - - /** - * Subscribe for messages on this channel. This implicitly attaches the channel if - * not already attached. - * @param listener: the MessageListener - * @throws AblyException - */ - public synchronized void subscribe(MessageListener listener) throws AblyException { - Log.v(TAG, "subscribe(); channel = " + this.name); - listeners.add(listener); - attach(); - } - - /** - * Unsubscribe a previously subscribed listener from this channel. - * @param listener: the previously subscribed listener. - */ - public synchronized void unsubscribe(MessageListener listener) { - Log.v(TAG, "unsubscribe(); channel = " + this.name); - listeners.remove(listener); - for (MessageMulticaster multicaster: eventListeners.values()) { - multicaster.remove(listener); - } - } - - /** - * Subscribe for messages with a specific event name on this channel. - * This implicitly attaches the channel if not already attached. - * @param name: the event name - * @param listener: the MessageListener - * @throws AblyException - */ - public synchronized void subscribe(String name, MessageListener listener) throws AblyException { - Log.v(TAG, "subscribe(); channel = " + this.name + "; event = " + name); - subscribeImpl(name, listener); - attach(); - } - - /** - * Unsubscribe a previously subscribed event listener from this channel. - * @param name: the event name - * @param listener: the previously subscribed listener. - */ - public synchronized void unsubscribe(String name, MessageListener listener) { - Log.v(TAG, "unsubscribe(); channel = " + this.name + "; event = " + name); - unsubscribeImpl(name, listener); - } - - /** - * Subscribe for messages with an array of event names on this channel. - * This implicitly attaches the channel if not already attached. - * @param names: the event names - * @param listener: the MessageListener - * @throws AblyException - */ - public synchronized void subscribe(String[] names, MessageListener listener) throws AblyException { - Log.v(TAG, "subscribe(); channel = " + this.name + "; (multiple events)"); - for(String name : names) - subscribeImpl(name, listener); - attach(); - } - - /** - * Unsubscribe a previously subscribed event listener from this channel. - * @param names: the event names - * @param listener: the previously subscribed listener. - */ - public synchronized void unsubscribe(String[] names, MessageListener listener) { - Log.v(TAG, "unsubscribe(); channel = " + this.name + "; (multiple events)"); - for(String name : names) - unsubscribeImpl(name, listener); - } - - /*** - * internal - * - */ - private void onMessage(final ProtocolMessage protocolMessage) { - Log.v(TAG, "onMessage(); channel = " + name); - final Message[] messages = protocolMessage.messages; - final Message firstMessage = messages[0]; - final Message lastMessage = messages[messages.length - 1]; - - final DeltaExtras deltaExtras = (null == firstMessage.extras) ? null : firstMessage.extras.getDelta(); - if (null != deltaExtras && !deltaExtras.getFrom().equals(this.lastPayloadMessageId)) { - Log.e(TAG, String.format("Delta message decode failure - previous message not available. Message id = %s, channel = %s", firstMessage.id, name)); - startDecodeFailureRecovery(); - return; - } - - for(int i = 0; i < messages.length; i++) { - final Message msg = messages[i]; - - /* populate fields derived from protocol message */ - if(msg.connectionId == null) msg.connectionId = protocolMessage.connectionId; - if(msg.timestamp == 0) msg.timestamp = protocolMessage.timestamp; - if(msg.id == null) msg.id = protocolMessage.id + ':' + i; - - try { - msg.decode(options, decodingContext); - } catch (MessageDecodeException e) { - if (e.errorInfo.code == 40018) { - Log.e(TAG, String.format("Delta message decode failure - %s. Message id = %s, channel = %s", e.errorInfo.message, msg.id, name)); - startDecodeFailureRecovery(); - - // log messages skipped per RTL16 - for (int j = i + 1; j < messages.length; j++) { - final String jId = messages[j].id; // might be null - final String jIdToLog = (null == jId) ? protocolMessage.id + ':' + j : jId; - Log.v(TAG, String.format("Delta recovery in progress - message skipped. Message id = %s, channel = %s", jIdToLog, name)); - } - - return; - } - else { - Log.e(TAG, String.format("Message decode failure - %s. Message id = %s, channel = %s", e.errorInfo.message, msg.id, name)); - } - } - - /* broadcast */ - final MessageMulticaster listeners = eventListeners.get(msg.name); - if(listeners != null) - listeners.onMessage(msg); - } - - lastPayloadMessageId = lastMessage.id; - lastPayloadProtocolMessageChannelSerial = protocolMessage.channelSerial; - - for (final Message msg : messages) { - this.listeners.onMessage(msg); - } - } - - private void startDecodeFailureRecovery() { - if (this.decodeFailureRecoveryInProgress) { - return; - } - Log.w(TAG, "Starting delta decode failure recovery process"); - this.decodeFailureRecoveryInProgress = true; - this.attach(true, new CompletionListener() { - @Override - public void onSuccess() { - decodeFailureRecoveryInProgress = false; - } - - @Override - public void onError(ErrorInfo reason) { - decodeFailureRecoveryInProgress = false; - } - }); - } - - private void onPresence(ProtocolMessage message, String syncChannelSerial) { - Log.v(TAG, "onPresence(); channel = " + name + "; syncChannelSerial = " + syncChannelSerial); - PresenceMessage[] messages = message.presence; - for(int i = 0; i < messages.length; i++) { - PresenceMessage msg = messages[i]; - try { - msg.decode(options); - } catch (MessageDecodeException e) { - Log.e(TAG, String.format("%s on channel %s", e.errorInfo.message, name)); - } - /* populate fields derived from protocol message */ - if(msg.connectionId == null) msg.connectionId = message.connectionId; - if(msg.timestamp == 0) msg.timestamp = message.timestamp; - if(msg.id == null) msg.id = message.id + ':' + i; - } - presence.setPresence(messages, true, syncChannelSerial); - } - - private void onSync(ProtocolMessage message) { - Log.v(TAG, "onSync(); channel = " + name); - if(message.presence != null) - onPresence(message, (syncChannelSerial = message.channelSerial)); - } - - private MessageMulticaster listeners = new MessageMulticaster(); - private HashMap eventListeners = new HashMap(); - - private static class MessageMulticaster extends io.ably.lib.util.Multicaster implements MessageListener { - @Override - public void onMessage(Message message) { - for(MessageListener member : members) - try { - member.onMessage(message); - } catch (Throwable t) { - Log.e(TAG, "Unexpected exception calling listener", t); - } - } - } - - private void subscribeImpl(String name, MessageListener listener) throws AblyException { - MessageMulticaster listeners = eventListeners.get(name); - if(listeners == null) { - listeners = new MessageMulticaster(); - eventListeners.put(name, listeners); - } - listeners.add(listener); - } - - private void unsubscribeImpl(String name, MessageListener listener) { - MessageMulticaster listeners = eventListeners.get(name); - if(listeners != null) { - listeners.remove(listener); - if(listeners.isEmpty()) - eventListeners.remove(name); - } - } - - /************************************ - * publish and pending messages - ************************************/ - - /** - * Publish a message on this channel. This implicitly attaches the channel if - * not already attached. - * @param name: the event name - * @param data: the message payload - * @throws AblyException - */ - public void publish(String name, Object data) throws AblyException { - publish(name, data, null); - } - - /** - * Publish a message on this channel. This implicitly attaches the channel if - * not already attached. - * @param message: the message - * @throws AblyException - */ - public void publish(Message message) throws AblyException { - publish(message, null); - } - - /** - * Publish an array of messages on this channel. This implicitly attaches the channel if - * not already attached. - * @param messages: the message - * @throws AblyException - */ - public void publish(Message[] messages) throws AblyException { - publish(messages, null); - } - - /** - * Publish a message on this channel. This implicitly attaches the channel if - * not already attached. - * @param name: the event name - * @param data: the message payload. See {@link io.ably.types.Data} for supported datatypes - * @param listener: a listener to be notified of the outcome of this message. - * @throws AblyException - */ - public void publish(String name, Object data, CompletionListener listener) throws AblyException { - Log.v(TAG, "publish(String, Object); channel = " + this.name + "; event = " + name); - publish(new Message[] {new Message(name, data)}, listener); - } - - /** - * Publish a message on this channel. This implicitly attaches the channel if - * not already attached. - * @param message: the message - * @param listener: a listener to be notified of the outcome of this message. - * @throws AblyException - */ - public void publish(Message message, CompletionListener listener) throws AblyException { - Log.v(TAG, "publish(Message); channel = " + this.name + "; event = " + message.name); - publish(new Message[] {message}, listener); - } - - /** - * Publish an array of messages on this channel. This implicitly attaches the channel if - * not already attached. - * @param messages: the message - * @param listener: a listener to be notified of the outcome of this message. - * @throws AblyException - */ - public synchronized void publish(Message[] messages, CompletionListener listener) throws AblyException { - Log.v(TAG, "publish(Message[]); channel = " + this.name); - ConnectionManager connectionManager = ably.connection.connectionManager; - ConnectionManager.State connectionState = connectionManager.getConnectionState(); - boolean queueMessages = ably.options.queueMessages; - if(!connectionManager.isActive() || (connectionState.queueEvents && !queueMessages)) { - throw AblyException.fromErrorInfo(connectionState.defaultErrorInfo); - } - boolean connected = (connectionState.sendEvents); - try { - for(Message message : messages) { - /* RTL6g3: check validity of any clientId; - * RTL6g4: be lenient with a null clientId if we're not connected */ - ably.auth.checkClientId(message, true, connected); - message.encode(options); - } - } catch(AblyException e) { - callCompletionListenerError(listener, e.errorInfo); - return; - } - ProtocolMessage msg = new ProtocolMessage(Action.message, this.name); - msg.messages = messages; - switch(state) { - case failed: - case suspended: - throw AblyException.fromErrorInfo(new ErrorInfo("Unable to publish in failed or suspended state", 400, 40000)); - default: - connectionManager.send(msg, queueMessages, listener); - } - } - - /*** - * internal - * - */ - - private static class FailedMessage { - QueuedMessage msg; - ErrorInfo reason; - FailedMessage(QueuedMessage msg, ErrorInfo reason) { - this.msg = msg; - this.reason = reason; - } - } - - private void sendQueuedMessages() { - Log.v(TAG, "sendQueuedMessages()"); - ArrayList failedMessages = new ArrayList<>(); - synchronized (this) { - boolean queueMessages = ably.options.queueMessages; - ConnectionManager connectionManager = ably.connection.connectionManager; - for (QueuedMessage msg : queuedMessages) - try { - connectionManager.send(msg.msg, queueMessages, msg.listener); - } catch (AblyException e) { - Log.e(TAG, "sendQueuedMessages(): Unexpected exception sending message", e); - if (msg.listener != null) - failedMessages.add(new FailedMessage(msg, e.errorInfo)); - } - queuedMessages.clear(); - } - - /* Call completion callbacks for failed messages without holding the lock */ - for (FailedMessage failed: failedMessages) { - callCompletionListenerError(failed.msg.listener, failed.reason); - } - } - - private void failQueuedMessages(ErrorInfo reason) { - Log.v(TAG, "failQueuedMessages()"); - - ArrayList failedMessages = new ArrayList<>(); - synchronized (this) { - for (QueuedMessage msg: queuedMessages) { - if (msg.listener != null) - failedMessages.add(new FailedMessage(msg, reason)); - } - queuedMessages.clear(); - } - - for(FailedMessage failed : failedMessages) { - callCompletionListenerError(failed.msg.listener, failed.reason); - } - } - - static Param[] replacePlaceholderParams(Channel channel, Param[] placeholderParams) throws AblyException { - if (placeholderParams == null) { - return null; - } - - HashSet params = new HashSet<>(); - - Param param; - for(int i = 0; i < placeholderParams.length; i++) { - param = placeholderParams[i]; - - if(KEY_UNTIL_ATTACH.equals(param.key)) { - if("true".equalsIgnoreCase(param.value)) { - if (channel.state != ChannelState.attached) { - throw AblyException.fromErrorInfo(new ErrorInfo("option untilAttach requires the channel to be attached", 40000, 400)); - } - - params.add(new Param(KEY_FROM_SERIAL, channel.properties.attachSerial)); - } - else if(!"false".equalsIgnoreCase(param.value)) { - throw AblyException.fromErrorInfo(new ErrorInfo("option untilAttach is invalid. \"true\" or \"false\" expected", 40000, 400)); - } - } - else { - /* Add non-placeholder param as is */ - params.add(param); - } - } - - return params.toArray(new Param[params.size()]); - } - - - private static final String KEY_UNTIL_ATTACH = "untilAttach"; - private static final String KEY_FROM_SERIAL = "fromSerial"; - private List queuedMessages; - - /************************************ - * Channel history - ************************************/ - - /** - * Obtain recent history for this channel using the REST API. - * The history provided relqtes to all clients of this application, - * not just this instance. - * @param params: the request params. See the Ably REST API - * documentation for more details. - * @return: an array of Messgaes for this Channel. - * @throws AblyException - */ - public PaginatedResult history(Param[] params) throws AblyException { - return historyImpl(params).sync(); - } - - public void historyAsync(Param[] params, Callback> callback) { - historyImpl(params).async(callback); - } - - private BasePaginatedQuery.ResultRequest historyImpl(Param[] params) { - try { - params = replacePlaceholderParams((Channel) this, params); - } catch (AblyException e) { - return new BasePaginatedQuery.ResultRequest.Failed(e); - } - - HttpCore.BodyHandler bodyHandler = MessageSerializer.getMessageResponseHandler(options); - return new BasePaginatedQuery(ably.http, basePath + "/history", HttpUtils.defaultAcceptHeaders(ably.options.useBinaryProtocol), params, bodyHandler).get(); - } - - /************************************ - * Channel options - ************************************/ - - public void setOptions(ChannelOptions options) throws AblyException { - this.setOptions(options, null); - } - - public void setOptions(ChannelOptions options, CompletionListener listener) throws AblyException { - this.options = options; - if(this.shouldReattachToSetOptions(options)) { - this.attach(true, listener); - } else { - callCompletionListenerSuccess(listener); - } - } - - boolean shouldReattachToSetOptions(ChannelOptions options) { - return - (this.state == ChannelState.attached || this.state == ChannelState.attaching) && - (options.hasModes() || options.hasParams()); - } - - public Map getParams() { - return CollectionUtils.copy(params); - } - - public ChannelMode[] getModes() { - return modes.toArray(new ChannelMode[modes.size()]); - } - - /************************************ - * internal general - * @throws AblyException - ************************************/ - - private class ChannelStateCompletionListener implements ChannelStateListener { - private CompletionListener completionListener; - private final ChannelState successState; - private final ChannelState failureState; - - ChannelStateCompletionListener(CompletionListener completionListener, ChannelState successState, ChannelState failureState) { - this.completionListener = completionListener; - this.successState = successState; - this.failureState = failureState; - } - - @Override - public void onChannelStateChanged(ChannelStateListener.ChannelStateChange stateChange) { - if(stateChange.current.equals(successState)) { - ChannelBase.this.off(this); - completionListener.onSuccess(); - } - else if(stateChange.current.equals(failureState)) { - ChannelBase.this.off(this); - completionListener.onError(reason); - } - } - } - - ChannelBase(AblyRealtime ably, String name, ChannelOptions options) throws AblyException { - Log.v(TAG, "RealtimeChannel(); channel = " + name); - this.ably = ably; - this.name = name; - this.basePath = "/channels/" + HttpUtils.encodeURIComponent(name); - this.setOptions(options); - this.presence = new Presence((Channel) this); - this.attachResume = false; - state = ChannelState.initialized; - queuedMessages = new ArrayList(); - this.decodingContext = new DecodingContext(); - } - - void onChannelMessage(ProtocolMessage msg) { - switch(msg.action) { - case attached: - setAttached(msg); - break; - case detach: - case detached: - ChannelState oldState = state; - switch(oldState) { - case attached: - /* Unexpected detach, reattach when possible */ - setDetached((msg.error != null) ? msg.error : REASON_NOT_ATTACHED); - Log.v(TAG, String.format("Server initiated detach for channel %s; attempting reattach", name)); - try { - attachWithTimeout(null); - } catch (AblyException e) { - /* Send message error */ - Log.e(TAG, "Attempting reattach threw exception", e); - setDetached(e.errorInfo); - } - break; - case attaching: - /* RTL13b says we need to be suspended, but continue to retry */ - Log.v(TAG, String.format("Server initiated detach for channel %s whilst attaching; moving to suspended", name)); - setSuspended(msg.error, true); - reattachAfterTimeout(); - break; - case detaching: - setDetached((msg.error != null) ? msg.error : REASON_NOT_ATTACHED); - break; - case detached: - case suspended: - case failed: - default: - /* do nothing */ - break; - } - break; - case message: - if(state == ChannelState.attached) { - onMessage(msg); - } else { - final String errorMsgPrefix = decodeFailureRecoveryInProgress ? - "Delta recovery in progress - message skipped." : - "Message skipped on a channel that is not ATTACHED."; - - // log messages skipped per RTL17 - for (final Message skippedMessage : msg.messages) { - Log.v(TAG, String.format(errorMsgPrefix + " Message id = %s, channel = %s", skippedMessage.id, name)); - } - } - break; - case presence: - onPresence(msg, null); - break; - case sync: - onSync(msg); - break; - case error: - setFailed(msg.error); - break; - default: - Log.e(TAG, "onChannelMessage(): Unexpected message action (" + msg.action + ")"); - } - } - - /** - * Emits UPDATE event - * @param errorInfo - */ - void emitUpdate(ErrorInfo errorInfo, boolean resumed) { - if(state == ChannelState.attached) - emit(ChannelEvent.update, ChannelStateListener.ChannelStateChange.createUpdateEvent(errorInfo, resumed)); - } - - public void emit(ChannelState state, ChannelStateListener.ChannelStateChange channelStateChange) { - super.emit(state.getChannelEvent(), channelStateChange); - } - - public void on(ChannelState state, ChannelStateListener listener) { - super.on(state.getChannelEvent(), listener); - } - - public void once(ChannelState state, ChannelStateListener listener) { - super.once(state.getChannelEvent(), listener); - } - - private static final String TAG = Channel.class.getName(); - final AblyRealtime ably; - final String basePath; - ChannelOptions options; - String syncChannelSerial; - private Map params; - private Set modes; - private String lastPayloadMessageId; - private String lastPayloadProtocolMessageChannelSerial; - private boolean decodeFailureRecoveryInProgress; - private final DecodingContext decodingContext; + /************************************ + * ChannelState and state management + ************************************/ + + /** + * The name of this channel. + */ + public final String name; + + /** + * The {@link Presence} object for this channel. This controls this client's + * presence on the channel and may also be used to obtain presence information + * and change events for other members of the channel. + */ + public final Presence presence; + + /** + * The current channel state. + */ + public ChannelState state; + + /** + * Error information associated with a failed channel state. + */ + public ErrorInfo reason; + + /** + * Properties of Channel + */ + public ChannelProperties properties = new ChannelProperties(); + + /*** + * internal + * + */ + private void setState(ChannelState newState, ErrorInfo reason) { + setState(newState, reason, false, true); + } + private void setState(ChannelState newState, ErrorInfo reason, boolean resumed) { + setState(newState, reason, resumed, true); + } + private void setState(ChannelState newState, ErrorInfo reason, boolean resumed, boolean notifyStateChange) { + Log.v(TAG, "setState(): channel = " + name + "; setting " + newState); + ChannelStateListener.ChannelStateChange stateChange; + synchronized(this) { + stateChange = new ChannelStateListener.ChannelStateChange(newState, this.state, reason, resumed); + this.state = stateChange.current; + this.reason = stateChange.reason; + } + + if(notifyStateChange) { + /* broadcast state change */ + emit(newState, stateChange); + } + } + + /************************************ + * attach / detach + ************************************/ + + /** + * Attach to this channel. + * This call initiates the attach request, and the response + * is indicated asynchronously in the resulting state change. + * attach() is called implicitly when publishing or subscribing + * on this channel, so it is not usually necessary for a client + * to call attach() explicitly. + * @throws AblyException + */ + public void attach() throws AblyException { + attach(null); + } + + /** + * Attach to this channel. + * This call initiates the attach request, and the response + * is indicated asynchronously in the resulting state change. + * attach() is called implicitly when publishing or subscribing + * on this channel, so it is not usually necessary for a client + * to call attach() explicitly. + * + * @param listener When the channel is attached successfully or the attach fails and + * the ErrorInfo error is passed as an argument to the callback + * @throws AblyException + */ + public void attach(CompletionListener listener) throws AblyException { + this.attach(false, listener); + } + + private void attach(boolean forceReattach, CompletionListener listener) { + clearAttachTimers(); + attachWithTimeout(forceReattach, listener); + } + + private boolean attachResume; + + private void attachImpl(final boolean forceReattach, final CompletionListener listener) throws AblyException { + Log.v(TAG, "attach(); channel = " + name); + if(!forceReattach) { + /* check preconditions */ + switch(state) { + case attaching: + if(listener != null) { + on(new ChannelStateCompletionListener(listener, ChannelState.attached, ChannelState.failed)); + } + return; + case attached: + callCompletionListenerSuccess(listener); + return; + default: + } + } + ConnectionManager connectionManager = ably.connection.connectionManager; + if(!connectionManager.isActive()) { + throw AblyException.fromErrorInfo(connectionManager.getStateErrorInfo()); + } + + /* send attach request and pending state */ + Log.v(TAG, "attach(); channel = " + name + "; sending ATTACH request"); + ProtocolMessage attachMessage = new ProtocolMessage(Action.attach, this.name); + if(this.options != null) { + if(this.options.hasParams()) { + attachMessage.params = CollectionUtils.copy(this.options.params); + } + if(this.options.hasModes()) { + attachMessage.setFlags(options.getModeFlags()); + } + } + if(this.decodeFailureRecoveryInProgress) { + attachMessage.channelSerial = this.lastPayloadProtocolMessageChannelSerial; + } + try { + if (listener != null) { + on(new ChannelStateCompletionListener(listener, ChannelState.attached, ChannelState.failed)); + } + if (this.attachResume) { + attachMessage.setFlag(Flag.attach_resume); + } + + setState(ChannelState.attaching, null); + connectionManager.send(attachMessage, true, null); + } catch(AblyException e) { + throw e; + } + } + + /** + * Detach from this channel. + * This call initiates the detach request, and the response + * is indicated asynchronously in the resulting state change. + * @throws AblyException + */ + public void detach() throws AblyException { + detach(null); + } + + /** + * Detach from this channel. + * This call initiates the detach request, and the response + * is indicated asynchronously in the resulting state change. + * @throws AblyException + */ + public void detach(CompletionListener listener) throws AblyException { + clearAttachTimers(); + detachWithTimeout(listener); + } + + private void detachImpl(CompletionListener listener) throws AblyException { + Log.v(TAG, "detach(); channel = " + name); + /* check preconditions */ + switch(state) { + case initialized: + case detached: { + callCompletionListenerSuccess(listener); + return; + } + case detaching: + if (listener != null) { + on(new ChannelStateCompletionListener(listener, ChannelState.detached, ChannelState.failed)); + } + return; + default: + } + ConnectionManager connectionManager = ably.connection.connectionManager; + if(!connectionManager.isActive()) + throw AblyException.fromErrorInfo(connectionManager.getStateErrorInfo()); + + /* send detach request */ + ProtocolMessage detachMessage = new ProtocolMessage(Action.detach, this.name); + try { + if (listener != null) { + on(new ChannelStateCompletionListener(listener, ChannelState.detached, ChannelState.failed)); + } + + this.attachResume = false; + setState(ChannelState.detaching, null); + connectionManager.send(detachMessage, true, null); + } catch(AblyException e) { + throw e; + } + } + + public void sync() throws AblyException { + Log.v(TAG, "sync(); channel = " + name); + /* check preconditions */ + switch(state) { + case initialized: + case detaching: + case detached: + throw AblyException.fromErrorInfo(new ErrorInfo("Unable to sync to channel; not attached", 40000)); + default: + } + ConnectionManager connectionManager = ably.connection.connectionManager; + if(!connectionManager.isActive()) + throw AblyException.fromErrorInfo(connectionManager.getStateErrorInfo()); + + /* send sync request */ + ProtocolMessage syncMessage = new ProtocolMessage(Action.sync, this.name); + syncMessage.channelSerial = syncChannelSerial; + connectionManager.send(syncMessage, true, null); + } + + /*** + * internal + * + */ + private static void callCompletionListenerSuccess(CompletionListener listener) { + if(listener != null) { + try { + listener.onSuccess(); + } catch(Throwable t) { + Log.e(TAG, "Unexpected exception calling CompletionListener", t); + } + } + } + + private static void callCompletionListenerError(CompletionListener listener, ErrorInfo err) { + if(listener != null) { + try { + listener.onError(err); + } catch(Throwable t) { + Log.e(TAG, "Unexpected exception calling CompletionListener", t); + } + } + } + + private void setAttached(ProtocolMessage message) { + clearAttachTimers(); + boolean resumed = message.hasFlag(Flag.resumed); + Log.v(TAG, "setAttached(); channel = " + name + ", resumed = " + resumed); + properties.attachSerial = message.channelSerial; + params = message.params; + modes = ChannelMode.toSet(message.flags); + if(state == ChannelState.attached) { + Log.v(TAG, String.format("Server initiated attach for channel %s", name)); + /* emit UPDATE event according to RTL12 */ + emitUpdate(null, resumed); + } else { + this.attachResume = true; + setState(ChannelState.attached, message.error, resumed); + sendQueuedMessages(); + presence.setAttached(message.hasFlag(Flag.has_presence)); + } + } + + private void setDetached(ErrorInfo reason) { + clearAttachTimers(); + Log.v(TAG, "setDetached(); channel = " + name); + presence.setDetached(reason); + setState(ChannelState.detached, reason); + failQueuedMessages(reason); + } + + private void setFailed(ErrorInfo reason) { + clearAttachTimers(); + Log.v(TAG, "setFailed(); channel = " + name); + presence.setDetached(reason); + this.attachResume = false; + setState(ChannelState.failed, reason); + failQueuedMessages(reason); + } + + /* Timer for attach operation */ + private Timer attachTimer; + + /* Timer for reattaching if attach failed */ + private Timer reattachTimer; + + /** + * Cancel attach/reattach timers + */ + synchronized private void clearAttachTimers() { + Timer[] timers = new Timer[]{attachTimer, reattachTimer}; + attachTimer = reattachTimer = null; + for (Timer t: timers) { + if (t != null) { + t.cancel(); + t.purge(); + } + } + } + + private void attachWithTimeout(final CompletionListener listener) throws AblyException { + this.attachWithTimeout(false, listener); + } + + /** + * Attach channel, if not attached within timeout set state to suspended and + * set up timer to reattach it later + */ + synchronized private void attachWithTimeout(final boolean forceReattach, final CompletionListener listener) { + Timer currentAttachTimer; + try { + currentAttachTimer = new Timer(); + } catch(Throwable t) { + /* an exception instancing the timer can arise because the runtime is exiting */ + callCompletionListenerError(listener, ErrorInfo.fromThrowable(t)); + return; + } + attachTimer = currentAttachTimer; + + try { + attachImpl(forceReattach, new CompletionListener() { + @Override + public void onSuccess() { + clearAttachTimers(); + callCompletionListenerSuccess(listener); + } + + @Override + public void onError(ErrorInfo reason) { + clearAttachTimers(); + callCompletionListenerError(listener, reason); + } + }); + } catch(AblyException e) { + attachTimer = null; + callCompletionListenerError(listener, e.errorInfo); + } + + if(attachTimer == null) { + /* operation has already succeeded or failed, no need to set the timer */ + return; + } + + final Timer inProgressTimer = currentAttachTimer; + attachTimer.schedule( + new TimerTask() { + @Override + public void run() { + String errorMessage = String.format("Attach timed out for channel %s", name); + Log.v(TAG, errorMessage); + synchronized (ChannelBase.this) { + if(attachTimer != inProgressTimer) { + return; + } + attachTimer = null; + if(state == ChannelState.attaching) { + setSuspended(new ErrorInfo(errorMessage, 91200), true); + reattachAfterTimeout(); + } + } + } + }, Defaults.realtimeRequestTimeout); + } + + /** + * Must be called in suspended state. Wait for timeout specified in clientOptions, and then + * try to attach the channel + */ + synchronized private void reattachAfterTimeout() { + Timer currentReattachTimer; + try { + currentReattachTimer = new Timer(); + } catch(Throwable t) { + /* an exception instancing the timer can arise because the runtime is exiting */ + return; + } + reattachTimer = currentReattachTimer; + + final Timer inProgressTimer = currentReattachTimer; + reattachTimer.schedule(new TimerTask() { + @Override + public void run() { + synchronized (ChannelBase.this) { + if (inProgressTimer != reattachTimer) { + return; + } + reattachTimer = null; + if (state == ChannelState.suspended) { + try { + attachWithTimeout(null); + } catch (AblyException e) { + Log.e(TAG, "Reattach channel failed; channel = " + name, e); + } + } + } + } + }, ably.options.channelRetryTimeout); + } + + /** + * Try to detach the channel. If the server doesn't confirm the detach operation within realtime + * request timeout return channel to previous state + */ + synchronized private void detachWithTimeout(final CompletionListener listener) { + final ChannelState originalState = state; + Timer currentDetachTimer; + try { + currentDetachTimer = new Timer(); + } catch(Throwable t) { + /* an exception instancing the timer can arise because the runtime is exiting */ + callCompletionListenerError(listener, ErrorInfo.fromThrowable(t)); + return; + } + attachTimer = currentDetachTimer; + + try { + detachImpl(new CompletionListener() { + @Override + public void onSuccess() { + clearAttachTimers(); + callCompletionListenerSuccess(listener); + } + + @Override + public void onError(ErrorInfo reason) { + clearAttachTimers(); + callCompletionListenerError(listener, reason); + } + }); + } catch (AblyException e) { + attachTimer = null; + } + + if(attachTimer == null) { + /* operation has already succeeded or failed, no need to set the timer */ + return; + } + + final Timer inProgressTimer = currentDetachTimer; + attachTimer.schedule(new TimerTask() { + @Override + public void run() { + synchronized (ChannelBase.this) { + if (inProgressTimer != attachTimer) { + return; + } + attachTimer = null; + if (state == ChannelState.detaching) { + ErrorInfo reason = new ErrorInfo("Detach operation timed out", 90007); + callCompletionListenerError(listener, reason); + setState(originalState, reason); + } + } + } + }, Defaults.realtimeRequestTimeout); + } + + /* State changes provoked by ConnectionManager state changes. */ + + public void setConnected() { + if(state == ChannelState.attached) { + try { + sync(); + } catch (AblyException e) { + Log.e(TAG, "setConnected(): Unable to sync; channel = " + name, e); + } + } else if (state == ChannelState.suspended) { + /* (RTL3d) If the connection state enters the CONNECTED state, then + * a SUSPENDED channel will initiate an attach operation. If the + * attach operation for the channel times out and the channel + * returns to the SUSPENDED state (see #RTL4f) + */ + try { + attachWithTimeout(null); + } catch (AblyException e) { + Log.e(TAG, "setConnected(): Unable to initiate attach; channel = " + name, e); + } + } + } + + /** If the connection state enters the FAILED state, then an ATTACHING + * or ATTACHED channel state will transition to FAILED and set the + * Channel#errorReason + */ + public void setConnectionFailed(ErrorInfo reason) { + clearAttachTimers(); + if (state == ChannelState.attached || state == ChannelState.attaching) + setFailed(reason); + } + + /** (RTL3b) If the connection state enters the CLOSED state, then an + * ATTACHING or ATTACHED channel state will transition to DETACHED. */ + public void setConnectionClosed(ErrorInfo reason) { + clearAttachTimers(); + if (state == ChannelState.attached || state == ChannelState.attaching) + setDetached(reason); + } + + /** (RTL3c) If the connection state enters the SUSPENDED state, then an + * ATTACHING or ATTACHED channel state will transition to SUSPENDED. + * (RTN15c3) The client library should initiate an attach for channels + * that are in the SUSPENDED state. For all channels in the ATTACHING + * or ATTACHED state, the client library should fail any previously queued + * messages for that channel and initiate a new attach. + * This also gets called when a connection enters CONNECTED but with a + * non-fatal error for a failed reconnect (RTN16e). */ + public synchronized void setSuspended(ErrorInfo reason, boolean notifyStateChange) { + clearAttachTimers(); + if (state == ChannelState.attached || state == ChannelState.attaching) { + Log.v(TAG, "setSuspended(); channel = " + name); + presence.setSuspended(reason); + setState(ChannelState.suspended, reason, false, notifyStateChange); + failQueuedMessages(reason); + } + } + + @Override + protected void apply(ChannelStateListener listener, ChannelEvent event, Object... args) { + try { + listener.onChannelStateChanged((ChannelStateListener.ChannelStateChange)args[0]); + } catch (Throwable t) { + Log.e(TAG, "Unexpected exception calling ChannelStateListener", t); + } + } + + static ErrorInfo REASON_NOT_ATTACHED = new ErrorInfo("Channel not attached", 400, 90001); + + /************************************ + * subscriptions and MessageListener + ************************************/ + + /** + * An interface whereby a client maybe notified of message on a channel. + */ + public interface MessageListener { + void onMessage(Message message); + } + + /** + *

+ * Unsubscribe all subscribed listeners from this channel. + *

+ *

+ * Spec: RTL8a + *

+ */ + public synchronized void unsubscribe() { + Log.v(TAG, "unsubscribe(); channel = " + this.name); + listeners.clear(); + eventListeners.clear(); + } + + /** + * Subscribe for messages on this channel. This implicitly attaches the channel if + * not already attached. + * @param listener: the MessageListener + * @throws AblyException + */ + public synchronized void subscribe(MessageListener listener) throws AblyException { + Log.v(TAG, "subscribe(); channel = " + this.name); + listeners.add(listener); + attach(); + } + + /** + * Unsubscribe a previously subscribed listener from this channel. + * @param listener: the previously subscribed listener. + */ + public synchronized void unsubscribe(MessageListener listener) { + Log.v(TAG, "unsubscribe(); channel = " + this.name); + listeners.remove(listener); + for (MessageMulticaster multicaster: eventListeners.values()) { + multicaster.remove(listener); + } + } + + /** + * Subscribe for messages with a specific event name on this channel. + * This implicitly attaches the channel if not already attached. + * @param name: the event name + * @param listener: the MessageListener + * @throws AblyException + */ + public synchronized void subscribe(String name, MessageListener listener) throws AblyException { + Log.v(TAG, "subscribe(); channel = " + this.name + "; event = " + name); + subscribeImpl(name, listener); + attach(); + } + + /** + * Unsubscribe a previously subscribed event listener from this channel. + * @param name: the event name + * @param listener: the previously subscribed listener. + */ + public synchronized void unsubscribe(String name, MessageListener listener) { + Log.v(TAG, "unsubscribe(); channel = " + this.name + "; event = " + name); + unsubscribeImpl(name, listener); + } + + /** + * Subscribe for messages with an array of event names on this channel. + * This implicitly attaches the channel if not already attached. + * @param names: the event names + * @param listener: the MessageListener + * @throws AblyException + */ + public synchronized void subscribe(String[] names, MessageListener listener) throws AblyException { + Log.v(TAG, "subscribe(); channel = " + this.name + "; (multiple events)"); + for(String name : names) + subscribeImpl(name, listener); + attach(); + } + + /** + * Unsubscribe a previously subscribed event listener from this channel. + * @param names: the event names + * @param listener: the previously subscribed listener. + */ + public synchronized void unsubscribe(String[] names, MessageListener listener) { + Log.v(TAG, "unsubscribe(); channel = " + this.name + "; (multiple events)"); + for(String name : names) + unsubscribeImpl(name, listener); + } + + /*** + * internal + * + */ + private void onMessage(final ProtocolMessage protocolMessage) { + Log.v(TAG, "onMessage(); channel = " + name); + final Message[] messages = protocolMessage.messages; + final Message firstMessage = messages[0]; + final Message lastMessage = messages[messages.length - 1]; + + final DeltaExtras deltaExtras = (null == firstMessage.extras) ? null : firstMessage.extras.getDelta(); + if (null != deltaExtras && !deltaExtras.getFrom().equals(this.lastPayloadMessageId)) { + Log.e(TAG, String.format("Delta message decode failure - previous message not available. Message id = %s, channel = %s", firstMessage.id, name)); + startDecodeFailureRecovery(); + return; + } + + for(int i = 0; i < messages.length; i++) { + final Message msg = messages[i]; + + /* populate fields derived from protocol message */ + if(msg.connectionId == null) msg.connectionId = protocolMessage.connectionId; + if(msg.timestamp == 0) msg.timestamp = protocolMessage.timestamp; + if(msg.id == null) msg.id = protocolMessage.id + ':' + i; + + try { + msg.decode(options, decodingContext); + } catch (MessageDecodeException e) { + if (e.errorInfo.code == 40018) { + Log.e(TAG, String.format("Delta message decode failure - %s. Message id = %s, channel = %s", e.errorInfo.message, msg.id, name)); + startDecodeFailureRecovery(); + + // log messages skipped per RTL16 + for (int j = i + 1; j < messages.length; j++) { + final String jId = messages[j].id; // might be null + final String jIdToLog = (null == jId) ? protocolMessage.id + ':' + j : jId; + Log.v(TAG, String.format("Delta recovery in progress - message skipped. Message id = %s, channel = %s", jIdToLog, name)); + } + + return; + } + else { + Log.e(TAG, String.format("Message decode failure - %s. Message id = %s, channel = %s", e.errorInfo.message, msg.id, name)); + } + } + + /* broadcast */ + final MessageMulticaster listeners = eventListeners.get(msg.name); + if(listeners != null) + listeners.onMessage(msg); + } + + lastPayloadMessageId = lastMessage.id; + lastPayloadProtocolMessageChannelSerial = protocolMessage.channelSerial; + + for (final Message msg : messages) { + this.listeners.onMessage(msg); + } + } + + private void startDecodeFailureRecovery() { + if (this.decodeFailureRecoveryInProgress) { + return; + } + Log.w(TAG, "Starting delta decode failure recovery process"); + this.decodeFailureRecoveryInProgress = true; + this.attach(true, new CompletionListener() { + @Override + public void onSuccess() { + decodeFailureRecoveryInProgress = false; + } + + @Override + public void onError(ErrorInfo reason) { + decodeFailureRecoveryInProgress = false; + } + }); + } + + private void onPresence(ProtocolMessage message, String syncChannelSerial) { + Log.v(TAG, "onPresence(); channel = " + name + "; syncChannelSerial = " + syncChannelSerial); + PresenceMessage[] messages = message.presence; + for(int i = 0; i < messages.length; i++) { + PresenceMessage msg = messages[i]; + try { + msg.decode(options); + } catch (MessageDecodeException e) { + Log.e(TAG, String.format("%s on channel %s", e.errorInfo.message, name)); + } + /* populate fields derived from protocol message */ + if(msg.connectionId == null) msg.connectionId = message.connectionId; + if(msg.timestamp == 0) msg.timestamp = message.timestamp; + if(msg.id == null) msg.id = message.id + ':' + i; + } + presence.setPresence(messages, true, syncChannelSerial); + } + + private void onSync(ProtocolMessage message) { + Log.v(TAG, "onSync(); channel = " + name); + if(message.presence != null) + onPresence(message, (syncChannelSerial = message.channelSerial)); + } + + private MessageMulticaster listeners = new MessageMulticaster(); + private HashMap eventListeners = new HashMap(); + + private static class MessageMulticaster extends io.ably.lib.util.Multicaster implements MessageListener { + @Override + public void onMessage(Message message) { + for(MessageListener member : members) + try { + member.onMessage(message); + } catch (Throwable t) { + Log.e(TAG, "Unexpected exception calling listener", t); + } + } + } + + private void subscribeImpl(String name, MessageListener listener) throws AblyException { + MessageMulticaster listeners = eventListeners.get(name); + if(listeners == null) { + listeners = new MessageMulticaster(); + eventListeners.put(name, listeners); + } + listeners.add(listener); + } + + private void unsubscribeImpl(String name, MessageListener listener) { + MessageMulticaster listeners = eventListeners.get(name); + if(listeners != null) { + listeners.remove(listener); + if(listeners.isEmpty()) + eventListeners.remove(name); + } + } + + /************************************ + * publish and pending messages + ************************************/ + + /** + * Publish a message on this channel. This implicitly attaches the channel if + * not already attached. + * @param name: the event name + * @param data: the message payload + * @throws AblyException + */ + public void publish(String name, Object data) throws AblyException { + publish(name, data, null); + } + + /** + * Publish a message on this channel. This implicitly attaches the channel if + * not already attached. + * @param message: the message + * @throws AblyException + */ + public void publish(Message message) throws AblyException { + publish(message, null); + } + + /** + * Publish an array of messages on this channel. This implicitly attaches the channel if + * not already attached. + * @param messages: the message + * @throws AblyException + */ + public void publish(Message[] messages) throws AblyException { + publish(messages, null); + } + + /** + * Publish a message on this channel. This implicitly attaches the channel if + * not already attached. + * @param name: the event name + * @param data: the message payload. See {@link io.ably.types.Data} for supported datatypes + * @param listener: a listener to be notified of the outcome of this message. + * @throws AblyException + */ + public void publish(String name, Object data, CompletionListener listener) throws AblyException { + Log.v(TAG, "publish(String, Object); channel = " + this.name + "; event = " + name); + publish(new Message[] {new Message(name, data)}, listener); + } + + /** + * Publish a message on this channel. This implicitly attaches the channel if + * not already attached. + * @param message: the message + * @param listener: a listener to be notified of the outcome of this message. + * @throws AblyException + */ + public void publish(Message message, CompletionListener listener) throws AblyException { + Log.v(TAG, "publish(Message); channel = " + this.name + "; event = " + message.name); + publish(new Message[] {message}, listener); + } + + /** + * Publish an array of messages on this channel. This implicitly attaches the channel if + * not already attached. + * @param messages: the message + * @param listener: a listener to be notified of the outcome of this message. + * @throws AblyException + */ + public synchronized void publish(Message[] messages, CompletionListener listener) throws AblyException { + Log.v(TAG, "publish(Message[]); channel = " + this.name); + ConnectionManager connectionManager = ably.connection.connectionManager; + ConnectionManager.State connectionState = connectionManager.getConnectionState(); + boolean queueMessages = ably.options.queueMessages; + if(!connectionManager.isActive() || (connectionState.queueEvents && !queueMessages)) { + throw AblyException.fromErrorInfo(connectionState.defaultErrorInfo); + } + boolean connected = (connectionState.sendEvents); + try { + for(Message message : messages) { + /* RTL6g3: check validity of any clientId; + * RTL6g4: be lenient with a null clientId if we're not connected */ + ably.auth.checkClientId(message, true, connected); + message.encode(options); + } + } catch(AblyException e) { + callCompletionListenerError(listener, e.errorInfo); + return; + } + ProtocolMessage msg = new ProtocolMessage(Action.message, this.name); + msg.messages = messages; + switch(state) { + case failed: + case suspended: + throw AblyException.fromErrorInfo(new ErrorInfo("Unable to publish in failed or suspended state", 400, 40000)); + default: + connectionManager.send(msg, queueMessages, listener); + } + } + + /*** + * internal + * + */ + + private static class FailedMessage { + QueuedMessage msg; + ErrorInfo reason; + FailedMessage(QueuedMessage msg, ErrorInfo reason) { + this.msg = msg; + this.reason = reason; + } + } + + private void sendQueuedMessages() { + Log.v(TAG, "sendQueuedMessages()"); + ArrayList failedMessages = new ArrayList<>(); + synchronized (this) { + boolean queueMessages = ably.options.queueMessages; + ConnectionManager connectionManager = ably.connection.connectionManager; + for (QueuedMessage msg : queuedMessages) + try { + connectionManager.send(msg.msg, queueMessages, msg.listener); + } catch (AblyException e) { + Log.e(TAG, "sendQueuedMessages(): Unexpected exception sending message", e); + if (msg.listener != null) + failedMessages.add(new FailedMessage(msg, e.errorInfo)); + } + queuedMessages.clear(); + } + + /* Call completion callbacks for failed messages without holding the lock */ + for (FailedMessage failed: failedMessages) { + callCompletionListenerError(failed.msg.listener, failed.reason); + } + } + + private void failQueuedMessages(ErrorInfo reason) { + Log.v(TAG, "failQueuedMessages()"); + + ArrayList failedMessages = new ArrayList<>(); + synchronized (this) { + for (QueuedMessage msg: queuedMessages) { + if (msg.listener != null) + failedMessages.add(new FailedMessage(msg, reason)); + } + queuedMessages.clear(); + } + + for(FailedMessage failed : failedMessages) { + callCompletionListenerError(failed.msg.listener, failed.reason); + } + } + + static Param[] replacePlaceholderParams(Channel channel, Param[] placeholderParams) throws AblyException { + if (placeholderParams == null) { + return null; + } + + HashSet params = new HashSet<>(); + + Param param; + for(int i = 0; i < placeholderParams.length; i++) { + param = placeholderParams[i]; + + if(KEY_UNTIL_ATTACH.equals(param.key)) { + if("true".equalsIgnoreCase(param.value)) { + if (channel.state != ChannelState.attached) { + throw AblyException.fromErrorInfo(new ErrorInfo("option untilAttach requires the channel to be attached", 40000, 400)); + } + + params.add(new Param(KEY_FROM_SERIAL, channel.properties.attachSerial)); + } + else if(!"false".equalsIgnoreCase(param.value)) { + throw AblyException.fromErrorInfo(new ErrorInfo("option untilAttach is invalid. \"true\" or \"false\" expected", 40000, 400)); + } + } + else { + /* Add non-placeholder param as is */ + params.add(param); + } + } + + return params.toArray(new Param[params.size()]); + } + + + private static final String KEY_UNTIL_ATTACH = "untilAttach"; + private static final String KEY_FROM_SERIAL = "fromSerial"; + private List queuedMessages; + + /************************************ + * Channel history + ************************************/ + + /** + * Obtain recent history for this channel using the REST API. + * The history provided relqtes to all clients of this application, + * not just this instance. + * @param params: the request params. See the Ably REST API + * documentation for more details. + * @return: an array of Messgaes for this Channel. + * @throws AblyException + */ + public PaginatedResult history(Param[] params) throws AblyException { + return historyImpl(params).sync(); + } + + public void historyAsync(Param[] params, Callback> callback) { + historyImpl(params).async(callback); + } + + private BasePaginatedQuery.ResultRequest historyImpl(Param[] params) { + try { + params = replacePlaceholderParams((Channel) this, params); + } catch (AblyException e) { + return new BasePaginatedQuery.ResultRequest.Failed(e); + } + + HttpCore.BodyHandler bodyHandler = MessageSerializer.getMessageResponseHandler(options); + return new BasePaginatedQuery(ably.http, basePath + "/history", HttpUtils.defaultAcceptHeaders(ably.options.useBinaryProtocol), params, bodyHandler).get(); + } + + /************************************ + * Channel options + ************************************/ + + public void setOptions(ChannelOptions options) throws AblyException { + this.setOptions(options, null); + } + + public void setOptions(ChannelOptions options, CompletionListener listener) throws AblyException { + this.options = options; + if(this.shouldReattachToSetOptions(options)) { + this.attach(true, listener); + } else { + callCompletionListenerSuccess(listener); + } + } + + boolean shouldReattachToSetOptions(ChannelOptions options) { + return + (this.state == ChannelState.attached || this.state == ChannelState.attaching) && + (options.hasModes() || options.hasParams()); + } + + public Map getParams() { + return CollectionUtils.copy(params); + } + + public ChannelMode[] getModes() { + return modes.toArray(new ChannelMode[modes.size()]); + } + + /************************************ + * internal general + * @throws AblyException + ************************************/ + + private class ChannelStateCompletionListener implements ChannelStateListener { + private CompletionListener completionListener; + private final ChannelState successState; + private final ChannelState failureState; + + ChannelStateCompletionListener(CompletionListener completionListener, ChannelState successState, ChannelState failureState) { + this.completionListener = completionListener; + this.successState = successState; + this.failureState = failureState; + } + + @Override + public void onChannelStateChanged(ChannelStateListener.ChannelStateChange stateChange) { + if(stateChange.current.equals(successState)) { + ChannelBase.this.off(this); + completionListener.onSuccess(); + } + else if(stateChange.current.equals(failureState)) { + ChannelBase.this.off(this); + completionListener.onError(reason); + } + } + } + + ChannelBase(AblyRealtime ably, String name, ChannelOptions options) throws AblyException { + Log.v(TAG, "RealtimeChannel(); channel = " + name); + this.ably = ably; + this.name = name; + this.basePath = "/channels/" + HttpUtils.encodeURIComponent(name); + this.setOptions(options); + this.presence = new Presence((Channel) this); + this.attachResume = false; + state = ChannelState.initialized; + queuedMessages = new ArrayList(); + this.decodingContext = new DecodingContext(); + } + + void onChannelMessage(ProtocolMessage msg) { + switch(msg.action) { + case attached: + setAttached(msg); + break; + case detach: + case detached: + ChannelState oldState = state; + switch(oldState) { + case attached: + /* Unexpected detach, reattach when possible */ + setDetached((msg.error != null) ? msg.error : REASON_NOT_ATTACHED); + Log.v(TAG, String.format("Server initiated detach for channel %s; attempting reattach", name)); + try { + attachWithTimeout(null); + } catch (AblyException e) { + /* Send message error */ + Log.e(TAG, "Attempting reattach threw exception", e); + setDetached(e.errorInfo); + } + break; + case attaching: + /* RTL13b says we need to be suspended, but continue to retry */ + Log.v(TAG, String.format("Server initiated detach for channel %s whilst attaching; moving to suspended", name)); + setSuspended(msg.error, true); + reattachAfterTimeout(); + break; + case detaching: + setDetached((msg.error != null) ? msg.error : REASON_NOT_ATTACHED); + break; + case detached: + case suspended: + case failed: + default: + /* do nothing */ + break; + } + break; + case message: + if(state == ChannelState.attached) { + onMessage(msg); + } else { + final String errorMsgPrefix = decodeFailureRecoveryInProgress ? + "Delta recovery in progress - message skipped." : + "Message skipped on a channel that is not ATTACHED."; + + // log messages skipped per RTL17 + for (final Message skippedMessage : msg.messages) { + Log.v(TAG, String.format(errorMsgPrefix + " Message id = %s, channel = %s", skippedMessage.id, name)); + } + } + break; + case presence: + onPresence(msg, null); + break; + case sync: + onSync(msg); + break; + case error: + setFailed(msg.error); + break; + default: + Log.e(TAG, "onChannelMessage(): Unexpected message action (" + msg.action + ")"); + } + } + + /** + * Emits UPDATE event + * @param errorInfo + */ + void emitUpdate(ErrorInfo errorInfo, boolean resumed) { + if(state == ChannelState.attached) + emit(ChannelEvent.update, ChannelStateListener.ChannelStateChange.createUpdateEvent(errorInfo, resumed)); + } + + public void emit(ChannelState state, ChannelStateListener.ChannelStateChange channelStateChange) { + super.emit(state.getChannelEvent(), channelStateChange); + } + + public void on(ChannelState state, ChannelStateListener listener) { + super.on(state.getChannelEvent(), listener); + } + + public void once(ChannelState state, ChannelStateListener listener) { + super.once(state.getChannelEvent(), listener); + } + + private static final String TAG = Channel.class.getName(); + final AblyRealtime ably; + final String basePath; + ChannelOptions options; + String syncChannelSerial; + private Map params; + private Set modes; + private String lastPayloadMessageId; + private String lastPayloadProtocolMessageChannelSerial; + private boolean decodeFailureRecoveryInProgress; + private final DecodingContext decodingContext; } diff --git a/lib/src/main/java/io/ably/lib/realtime/ChannelEvent.java b/lib/src/main/java/io/ably/lib/realtime/ChannelEvent.java index f7ebe1f16..2da8c0745 100644 --- a/lib/src/main/java/io/ably/lib/realtime/ChannelEvent.java +++ b/lib/src/main/java/io/ably/lib/realtime/ChannelEvent.java @@ -4,12 +4,12 @@ * Channel event */ public enum ChannelEvent { - initialized, - attaching, - attached, - detaching, - detached, - failed, - suspended, - update + initialized, + attaching, + attached, + detaching, + detached, + failed, + suspended, + update } diff --git a/lib/src/main/java/io/ably/lib/realtime/ChannelState.java b/lib/src/main/java/io/ably/lib/realtime/ChannelState.java index 2d55e8cea..7cda2f5a5 100644 --- a/lib/src/main/java/io/ably/lib/realtime/ChannelState.java +++ b/lib/src/main/java/io/ably/lib/realtime/ChannelState.java @@ -4,19 +4,19 @@ * Channel states. See Ably Realtime API documentation for more details. */ public enum ChannelState { - initialized(ChannelEvent.initialized), - attaching(ChannelEvent.attaching), - attached(ChannelEvent.attached), - detaching(ChannelEvent.detaching), - detached(ChannelEvent.detached), - failed(ChannelEvent.failed), - suspended(ChannelEvent.suspended); + initialized(ChannelEvent.initialized), + attaching(ChannelEvent.attaching), + attached(ChannelEvent.attached), + detaching(ChannelEvent.detaching), + detached(ChannelEvent.detached), + failed(ChannelEvent.failed), + suspended(ChannelEvent.suspended); - final private ChannelEvent event; - ChannelState(ChannelEvent event) { - this.event = event; - } - public ChannelEvent getChannelEvent() { - return event; - } + final private ChannelEvent event; + ChannelState(ChannelEvent event) { + this.event = event; + } + public ChannelEvent getChannelEvent() { + return event; + } } diff --git a/lib/src/main/java/io/ably/lib/realtime/ChannelStateListener.java b/lib/src/main/java/io/ably/lib/realtime/ChannelStateListener.java index 3ce7befe9..963703ebf 100644 --- a/lib/src/main/java/io/ably/lib/realtime/ChannelStateListener.java +++ b/lib/src/main/java/io/ably/lib/realtime/ChannelStateListener.java @@ -7,70 +7,70 @@ */ public interface ChannelStateListener { - void onChannelStateChanged(ChannelStateChange stateChange); + void onChannelStateChanged(ChannelStateChange stateChange); - /** - * Channel state change. See Ably Realtime API documentation for more details. - */ - class ChannelStateChange { - final public ChannelEvent event; - /* (TH2) The ChannelStateChange object contains the current state in - * attribute current, the previous state in attribute previous. */ - final public ChannelState current; - final public ChannelState previous; - /* (TH3) If the channel state change includes error information, then - * the reason attribute will contain an ErrorInfo object describing the - * reason for the error. */ - final public ErrorInfo reason; - /* (TH4) The ChannelStateChange object contains an attribute resumed which - * in combination with an ATTACHED state, indicates whether the channel - * attach successfully resumed its state following the connection being - * resumed or recovered. If resumed is true, then the attribute indicates - * that the attach within Ably successfully recovered the state for the - * channel, and as such there is no loss of message continuity. In all - * other cases, resumed is false, and may be accompanied with a "channel - * state change error reason". */ - final public boolean resumed; + /** + * Channel state change. See Ably Realtime API documentation for more details. + */ + class ChannelStateChange { + final public ChannelEvent event; + /* (TH2) The ChannelStateChange object contains the current state in + * attribute current, the previous state in attribute previous. */ + final public ChannelState current; + final public ChannelState previous; + /* (TH3) If the channel state change includes error information, then + * the reason attribute will contain an ErrorInfo object describing the + * reason for the error. */ + final public ErrorInfo reason; + /* (TH4) The ChannelStateChange object contains an attribute resumed which + * in combination with an ATTACHED state, indicates whether the channel + * attach successfully resumed its state following the connection being + * resumed or recovered. If resumed is true, then the attribute indicates + * that the attach within Ably successfully recovered the state for the + * channel, and as such there is no loss of message continuity. In all + * other cases, resumed is false, and may be accompanied with a "channel + * state change error reason". */ + final public boolean resumed; - ChannelStateChange(ChannelState current, ChannelState previous, ErrorInfo reason, boolean resumed) { - this.event = current.getChannelEvent(); - this.current = current; - this.previous = previous; - this.reason = reason; - this.resumed = resumed; - } + ChannelStateChange(ChannelState current, ChannelState previous, ErrorInfo reason, boolean resumed) { + this.event = current.getChannelEvent(); + this.current = current; + this.previous = previous; + this.reason = reason; + this.resumed = resumed; + } - private ChannelStateChange(ErrorInfo reason, boolean resumed) { - this.event = ChannelEvent.update; - this.current = this.previous = ChannelState.attached; - this.reason = reason; - this.resumed = resumed; - } + private ChannelStateChange(ErrorInfo reason, boolean resumed) { + this.event = ChannelEvent.update; + this.current = this.previous = ChannelState.attached; + this.reason = reason; + this.resumed = resumed; + } - /* construct UPDATE event */ - static ChannelStateChange createUpdateEvent(ErrorInfo reason, boolean resumed) { - return new ChannelStateChange(reason, resumed); - } - } + /* construct UPDATE event */ + static ChannelStateChange createUpdateEvent(ErrorInfo reason, boolean resumed) { + return new ChannelStateChange(reason, resumed); + } + } - class Multicaster extends io.ably.lib.util.Multicaster implements ChannelStateListener { - @Override - public void onChannelStateChanged(ChannelStateChange stateChange) { - for(ChannelStateListener member : members) - try { - member.onChannelStateChanged(stateChange); - } catch(Throwable t) {} - } - } + class Multicaster extends io.ably.lib.util.Multicaster implements ChannelStateListener { + @Override + public void onChannelStateChanged(ChannelStateChange stateChange) { + for(ChannelStateListener member : members) + try { + member.onChannelStateChanged(stateChange); + } catch(Throwable t) {} + } + } - class Filter implements ChannelStateListener { - @Override - public void onChannelStateChanged(ChannelStateChange stateChange) { - if(stateChange.current == this.state) - listener.onChannelStateChanged(stateChange); - } - Filter(ChannelState state, ChannelStateListener listener) { this.state = state; this.listener = listener; } - ChannelState state; - ChannelStateListener listener; - } + class Filter implements ChannelStateListener { + @Override + public void onChannelStateChanged(ChannelStateChange stateChange) { + if(stateChange.current == this.state) + listener.onChannelStateChanged(stateChange); + } + Filter(ChannelState state, ChannelStateListener listener) { this.state = state; this.listener = listener; } + ChannelState state; + ChannelStateListener listener; + } } diff --git a/lib/src/main/java/io/ably/lib/realtime/CompletionListener.java b/lib/src/main/java/io/ably/lib/realtime/CompletionListener.java index ea2ef1940..c3d213d20 100644 --- a/lib/src/main/java/io/ably/lib/realtime/CompletionListener.java +++ b/lib/src/main/java/io/ably/lib/realtime/CompletionListener.java @@ -9,73 +9,73 @@ * of an asynchronous operation. */ public interface CompletionListener { - /** - * Called when the associated operation completes successfully, - */ - void onSuccess(); + /** + * Called when the associated operation completes successfully, + */ + void onSuccess(); - /** - * Called when the associated operation completes with an error. - * @param reason: information about the error. - */ - void onError(ErrorInfo reason); + /** + * Called when the associated operation completes with an error. + * @param reason: information about the error. + */ + void onError(ErrorInfo reason); - /** - * A Multicaster instance is used in the Ably library to manage a list - * of client listeners against certain operations. - */ - class Multicaster extends io.ably.lib.util.Multicaster implements CompletionListener { - public Multicaster(CompletionListener... members) { super(members); } + /** + * A Multicaster instance is used in the Ably library to manage a list + * of client listeners against certain operations. + */ + class Multicaster extends io.ably.lib.util.Multicaster implements CompletionListener { + public Multicaster(CompletionListener... members) { super(members); } - @Override - public void onSuccess() { - for(CompletionListener member : members) - try { - member.onSuccess(); - } catch(Throwable t) {} - } + @Override + public void onSuccess() { + for(CompletionListener member : members) + try { + member.onSuccess(); + } catch(Throwable t) {} + } - @Override - public void onError(ErrorInfo reason) { - for(CompletionListener member : members) - try { - member.onError(reason); - } catch(Throwable t) {} - } - } + @Override + public void onError(ErrorInfo reason) { + for(CompletionListener member : members) + try { + member.onError(reason); + } catch(Throwable t) {} + } + } - class ToCallback implements Callback { - private CompletionListener listener; - public ToCallback(CompletionListener listener) { - this.listener = listener; - } + class ToCallback implements Callback { + private CompletionListener listener; + public ToCallback(CompletionListener listener) { + this.listener = listener; + } - @Override - public void onSuccess(Void v) { - listener.onSuccess(); - } + @Override + public void onSuccess(Void v) { + listener.onSuccess(); + } - @Override - public void onError(ErrorInfo reason) { - listener.onError(reason); - } - } + @Override + public void onError(ErrorInfo reason) { + listener.onError(reason); + } + } - class FromCallback implements CompletionListener { - private final Callback callback; + class FromCallback implements CompletionListener { + private final Callback callback; - public FromCallback(Callback callback) { - this.callback = callback; - } + public FromCallback(Callback callback) { + this.callback = callback; + } - @Override - public void onSuccess() { - callback.onSuccess(null); - } + @Override + public void onSuccess() { + callback.onSuccess(null); + } - @Override - public void onError(ErrorInfo reason) { - callback.onError(reason); - } - } + @Override + public void onError(ErrorInfo reason) { + callback.onError(reason); + } + } } diff --git a/lib/src/main/java/io/ably/lib/realtime/Connection.java b/lib/src/main/java/io/ably/lib/realtime/Connection.java index 87d8a5e33..03bca1ce7 100644 --- a/lib/src/main/java/io/ably/lib/realtime/Connection.java +++ b/lib/src/main/java/io/ably/lib/realtime/Connection.java @@ -13,111 +13,111 @@ */ public class Connection extends EventEmitter { - /** - * The current state of this Connection. - */ - public ConnectionState state; - - /** - * Error information associated with a connection failure. - */ - public ErrorInfo reason; - - /** - * The assigned connection key. - */ - public String key; - - /** - * RTN16b) Connection#recoveryKey is an attribute composed of the connection key and latest - * serial received on the connection - */ - public String recoveryKey; - - /** - * A public identifier for this connection, used to identify - * this member in presence events and message ids. - */ - public String id; - - /** - * The serial number of the last message to be received on this connection. - */ - public long serial; - - /** - * Causes the library to re-attempt connection, if it was previously explicitly - * closed by the user, or was closed as a result of an unrecoverable error. - */ - public void connect() { - connectionManager.connect(); - } - - /** - * Send a heartbeat message to the Ably service and await a response. - * @param listener: a listener to be notified of the outcome of this message. - */ - public void ping(CompletionListener listener) { - connectionManager.ping(listener); - } - - /** - * Causes the connection to close, entering the closed state, from any state except - * the failed state. Once closed, the library will not attempt to re-establish the - * connection without a call to {@link #connect}. - */ - public void close() { - key = null; - recoveryKey = null; - connectionManager.close(); - } - - /***************** - * internal - *****************/ - - Connection(AblyRealtime ably, ConnectionManager.Channels channels) throws AblyException { - this.ably = ably; - this.state = ConnectionState.initialized; - this.connectionManager = new ConnectionManager(ably, this, channels); - } - - public void onConnectionStateChange(ConnectionStateChange stateChange) { - state = stateChange.current; - reason = stateChange.reason; - emit(state, stateChange); - } - - @Override - protected void apply(ConnectionStateListener listener, ConnectionEvent event, Object... args) { - try { - listener.onConnectionStateChanged((ConnectionStateChange)args[0]); - } catch (Throwable t) { - Log.e(TAG, "Unexpected exception calling ConnectionStateListener", t); - } - } - - public void emitUpdate(ErrorInfo errorInfo) { - if (state == ConnectionState.connected) - emit(ConnectionEvent.update, ConnectionStateListener.ConnectionStateChange.createUpdateEvent(errorInfo)); - } - - @Deprecated - public void emit(ConnectionState state, ConnectionStateChange stateChange) { - super.emit(state.getConnectionEvent(), stateChange); - } - - @Deprecated - public void on(ConnectionState state, ConnectionStateListener listener) { - super.on(state.getConnectionEvent(), listener); - } - - @Deprecated - public void once(ConnectionState state, ConnectionStateListener listener) { - super.once(state.getConnectionEvent(), listener); - } - - private static final String TAG = Connection.class.getName(); - final AblyRealtime ably; - public final ConnectionManager connectionManager; + /** + * The current state of this Connection. + */ + public ConnectionState state; + + /** + * Error information associated with a connection failure. + */ + public ErrorInfo reason; + + /** + * The assigned connection key. + */ + public String key; + + /** + * RTN16b) Connection#recoveryKey is an attribute composed of the connection key and latest + * serial received on the connection + */ + public String recoveryKey; + + /** + * A public identifier for this connection, used to identify + * this member in presence events and message ids. + */ + public String id; + + /** + * The serial number of the last message to be received on this connection. + */ + public long serial; + + /** + * Causes the library to re-attempt connection, if it was previously explicitly + * closed by the user, or was closed as a result of an unrecoverable error. + */ + public void connect() { + connectionManager.connect(); + } + + /** + * Send a heartbeat message to the Ably service and await a response. + * @param listener: a listener to be notified of the outcome of this message. + */ + public void ping(CompletionListener listener) { + connectionManager.ping(listener); + } + + /** + * Causes the connection to close, entering the closed state, from any state except + * the failed state. Once closed, the library will not attempt to re-establish the + * connection without a call to {@link #connect}. + */ + public void close() { + key = null; + recoveryKey = null; + connectionManager.close(); + } + + /***************** + * internal + *****************/ + + Connection(AblyRealtime ably, ConnectionManager.Channels channels) throws AblyException { + this.ably = ably; + this.state = ConnectionState.initialized; + this.connectionManager = new ConnectionManager(ably, this, channels); + } + + public void onConnectionStateChange(ConnectionStateChange stateChange) { + state = stateChange.current; + reason = stateChange.reason; + emit(state, stateChange); + } + + @Override + protected void apply(ConnectionStateListener listener, ConnectionEvent event, Object... args) { + try { + listener.onConnectionStateChanged((ConnectionStateChange)args[0]); + } catch (Throwable t) { + Log.e(TAG, "Unexpected exception calling ConnectionStateListener", t); + } + } + + public void emitUpdate(ErrorInfo errorInfo) { + if (state == ConnectionState.connected) + emit(ConnectionEvent.update, ConnectionStateListener.ConnectionStateChange.createUpdateEvent(errorInfo)); + } + + @Deprecated + public void emit(ConnectionState state, ConnectionStateChange stateChange) { + super.emit(state.getConnectionEvent(), stateChange); + } + + @Deprecated + public void on(ConnectionState state, ConnectionStateListener listener) { + super.on(state.getConnectionEvent(), listener); + } + + @Deprecated + public void once(ConnectionState state, ConnectionStateListener listener) { + super.once(state.getConnectionEvent(), listener); + } + + private static final String TAG = Connection.class.getName(); + final AblyRealtime ably; + public final ConnectionManager connectionManager; } diff --git a/lib/src/main/java/io/ably/lib/realtime/ConnectionEvent.java b/lib/src/main/java/io/ably/lib/realtime/ConnectionEvent.java index 7f1ed216e..c27fcd19a 100644 --- a/lib/src/main/java/io/ably/lib/realtime/ConnectionEvent.java +++ b/lib/src/main/java/io/ably/lib/realtime/ConnectionEvent.java @@ -4,13 +4,13 @@ * Connection event */ public enum ConnectionEvent { - initialized, - connecting, - connected, - disconnected, - suspended, - closing, - closed, - failed, - update + initialized, + connecting, + connected, + disconnected, + suspended, + closing, + closed, + failed, + update } diff --git a/lib/src/main/java/io/ably/lib/realtime/ConnectionState.java b/lib/src/main/java/io/ably/lib/realtime/ConnectionState.java index 50bd79c72..633f4bea5 100644 --- a/lib/src/main/java/io/ably/lib/realtime/ConnectionState.java +++ b/lib/src/main/java/io/ably/lib/realtime/ConnectionState.java @@ -4,20 +4,20 @@ * Connection states. See Ably Realtime API documentation for more details. */ public enum ConnectionState { - initialized(ConnectionEvent.initialized), - connecting(ConnectionEvent.connecting), - connected(ConnectionEvent.connected), - disconnected(ConnectionEvent.disconnected), - suspended(ConnectionEvent.suspended), - closing(ConnectionEvent.closing), - closed(ConnectionEvent.closed), - failed(ConnectionEvent.failed); + initialized(ConnectionEvent.initialized), + connecting(ConnectionEvent.connecting), + connected(ConnectionEvent.connected), + disconnected(ConnectionEvent.disconnected), + suspended(ConnectionEvent.suspended), + closing(ConnectionEvent.closing), + closed(ConnectionEvent.closed), + failed(ConnectionEvent.failed); - final private ConnectionEvent event; - ConnectionState(ConnectionEvent event) { - this.event = event; - } - public ConnectionEvent getConnectionEvent() { - return event; - } + final private ConnectionEvent event; + ConnectionState(ConnectionEvent event) { + this.event = event; + } + public ConnectionEvent getConnectionEvent() { + return event; + } } diff --git a/lib/src/main/java/io/ably/lib/realtime/ConnectionStateListener.java b/lib/src/main/java/io/ably/lib/realtime/ConnectionStateListener.java index b8c167c7a..bcc444b9b 100644 --- a/lib/src/main/java/io/ably/lib/realtime/ConnectionStateListener.java +++ b/lib/src/main/java/io/ably/lib/realtime/ConnectionStateListener.java @@ -4,55 +4,55 @@ public interface ConnectionStateListener { - void onConnectionStateChanged(ConnectionStateListener.ConnectionStateChange state); - - class ConnectionStateChange { - public final ConnectionEvent event; - public final ConnectionState previous; - public final ConnectionState current; - public final long retryIn; - public final ErrorInfo reason; - - public ConnectionStateChange(ConnectionState previous, ConnectionState current, long retryIn, ErrorInfo reason) { - this.event = current.getConnectionEvent(); - this.previous = previous; - this.current = current; - this.retryIn = retryIn; - this.reason = reason; - } - - /* private constructor for UPDATE event */ - private ConnectionStateChange(ErrorInfo reason) { - this.event = ConnectionEvent.update; - this.current = this.previous = ConnectionState.connected; - this.retryIn = 0; - this.reason = reason; - } - - /* construct UPDATE event */ - public static ConnectionStateChange createUpdateEvent(ErrorInfo reason) { - return new ConnectionStateChange(reason); - } - } - - class Multicaster extends io.ably.lib.util.Multicaster implements ConnectionStateListener { - @Override - public void onConnectionStateChanged(ConnectionStateChange state) { - for(ConnectionStateListener member : members) - try { - member.onConnectionStateChanged(state); - } catch(Throwable t) {} - } - } - - class Filter implements ConnectionStateListener { - @Override - public void onConnectionStateChanged(ConnectionStateChange change) { - if(change.current == state) - listener.onConnectionStateChanged(change); - } - Filter(ConnectionState state, ConnectionStateListener listener) { this.state = state; this.listener = listener; } - ConnectionState state; - ConnectionStateListener listener; - } + void onConnectionStateChanged(ConnectionStateListener.ConnectionStateChange state); + + class ConnectionStateChange { + public final ConnectionEvent event; + public final ConnectionState previous; + public final ConnectionState current; + public final long retryIn; + public final ErrorInfo reason; + + public ConnectionStateChange(ConnectionState previous, ConnectionState current, long retryIn, ErrorInfo reason) { + this.event = current.getConnectionEvent(); + this.previous = previous; + this.current = current; + this.retryIn = retryIn; + this.reason = reason; + } + + /* private constructor for UPDATE event */ + private ConnectionStateChange(ErrorInfo reason) { + this.event = ConnectionEvent.update; + this.current = this.previous = ConnectionState.connected; + this.retryIn = 0; + this.reason = reason; + } + + /* construct UPDATE event */ + public static ConnectionStateChange createUpdateEvent(ErrorInfo reason) { + return new ConnectionStateChange(reason); + } + } + + class Multicaster extends io.ably.lib.util.Multicaster implements ConnectionStateListener { + @Override + public void onConnectionStateChanged(ConnectionStateChange state) { + for(ConnectionStateListener member : members) + try { + member.onConnectionStateChanged(state); + } catch(Throwable t) {} + } + } + + class Filter implements ConnectionStateListener { + @Override + public void onConnectionStateChanged(ConnectionStateChange change) { + if(change.current == state) + listener.onConnectionStateChanged(change); + } + Filter(ConnectionState state, ConnectionStateListener listener) { this.state = state; this.listener = listener; } + ConnectionState state; + ConnectionStateListener listener; + } } diff --git a/lib/src/main/java/io/ably/lib/realtime/Presence.java b/lib/src/main/java/io/ably/lib/realtime/Presence.java index adc8af1e5..d9ef91462 100644 --- a/lib/src/main/java/io/ably/lib/realtime/Presence.java +++ b/lib/src/main/java/io/ably/lib/realtime/Presence.java @@ -24,1020 +24,1020 @@ */ public class Presence { - /************************************ - * subscriptions and PresenceListener - ************************************/ - - /** - * String parameter names for get() call with Param... as an argument - */ - public final static String GET_WAITFORSYNC = "waitForSync"; - public final static String GET_CLIENTID = "clientId"; - public final static String GET_CONNECTIONID = "connectionId"; - - /** - * Get the presence state for this channel. Take Param[] array as an argument. - * Implicitly attaches the channel. However, if the channel is in or moves to the FAILED - * state before the operation succeeds, it will result in an error - * @param params - * @return - * @throws AblyException - * @throws InterruptedException - */ - public synchronized PresenceMessage[] get(Param... params) throws AblyException { - if (channel.state == ChannelState.failed) { - throw AblyException.fromErrorInfo(new ErrorInfo("channel operation failed (invalid channel state)", 90001)); - } - - channel.attach(); - try { - Collection values = presence.get(params); - return values.toArray(new PresenceMessage[values.size()]); - } catch (InterruptedException e) { - Log.v(TAG, String.format("Channel %s: get() operation interrupted", channel.name)); - throw AblyException.fromThrowable(e); - } - } - - /** - * Get the presence state for this Channel, optionally waiting for sync to complete. - * Implicitly attaches the Channel. However, if the channel is in or moves to the FAILED - * state before the operation succeeds, it will result in an error - * @return: the current present members. - * @throws AblyException - */ - public synchronized PresenceMessage[] get(boolean wait) throws AblyException { - return get(new Param(GET_WAITFORSYNC, String.valueOf(wait))); - } - - /** - * Get the presence state for a given clientId. Implicitly attaches the - * Channel. However, if the channel is in or moves to the FAILED - * state before the operation succeeds, it will result in an error - * @param wait - * @return - * @throws InterruptedException - * @throws AblyException - */ - public synchronized PresenceMessage[] get(String clientId, boolean wait) throws AblyException { - return get(new Param(GET_WAITFORSYNC, String.valueOf(wait)), new Param(GET_CLIENTID, clientId)); - } - - /** - * An interface allowing a listener to be notified of arrival of a presence message. - */ - public interface PresenceListener { - void onPresenceMessage(PresenceMessage message); - } - - /** - * Subscribe to presence events on the associated Channel. This implicitly - * attaches the Channel if it is not already attached. - * @param listener: the listener to me notified on arrival of presence messages. - * @param completionListener listener to be called on success/failure - * @throws AblyException - */ - public void subscribe(PresenceListener listener, CompletionListener completionListener) throws AblyException { - implicitAttachOnSubscribe(completionListener); - listeners.add(listener); - } - - /** - * Same as above without completion listener - */ - public void subscribe(PresenceListener listener) throws AblyException { - subscribe(listener, null); - } - - /** - * Unsubscribe a previously subscribed presence listener for this channel. - * @param listener: the previously subscribed listener. - */ - public void unsubscribe(PresenceListener listener) { - listeners.remove(listener); - for (Multicaster multicaster: eventListeners.values()) { - multicaster.remove(listener); - } - } - - /** - * Subscribe to presence events with a specific action on the associated Channel. - * This implicitly attaches the Channel if it is not already attached. - * - * @param action to be observed - * @param listener - * @param completionListener listener to be called on success/failure - * @throws AblyException - */ - public void subscribe(PresenceMessage.Action action, PresenceListener listener, CompletionListener completionListener) throws AblyException { - implicitAttachOnSubscribe(completionListener); - subscribeImpl(action, listener); - } - - /** - * Same as above without completion listener - */ - public void subscribe(PresenceMessage.Action action, PresenceListener listener) throws AblyException { - subscribe(action, listener, null); - } - - /** - * Unsubscribe a previously subscribed presence listener for this channel from specific action. - * - * @param action - * @param listener - */ - public void unsubscribe(PresenceMessage.Action action, PresenceListener listener) { - unsubscribeImpl(action, listener); - } - - /** - * Subscribe to presence events with specific actions on the associated Channel. - * This implicitly attaches the Channel if it is not already attached. - * - * @param actions to be observed - * @param listener - * @param completionListener listener to be called on success/failure - * @throws AblyException - */ - public void subscribe(EnumSet actions, PresenceListener listener, CompletionListener completionListener) throws AblyException { - implicitAttachOnSubscribe(completionListener); - for (PresenceMessage.Action action : actions) { - subscribeImpl(action, listener); - } - } - - /** - * Same as above without completion listener - */ - public void subscribe(EnumSet actions, PresenceListener listener) throws AblyException { - subscribe(actions, listener, null); - } - - /** - * Unsubscribe a previously subscribed presence listener for this channel from specific actions. - * - * @param actions - * @param listener - */ - public void unsubscribe(EnumSet actions, PresenceListener listener) { - for (PresenceMessage.Action action : actions) { - unsubscribeImpl(action, listener); - } - } - - /** - * Unsubscribe all subscribed presence lisceners for this channel. - */ - public void unsubscribe() { - listeners.clear(); - eventListeners.clear(); - } - - - /*** - * internal - * - */ - - /** - * Implicitly attach channel on subscribe. Throw exception if channel is in failed state - * @param completionListener - * @throws AblyException - */ - private void implicitAttachOnSubscribe(CompletionListener completionListener) throws AblyException { - if (channel.state == ChannelState.failed) { - String errorString = String.format("Channel %s: subscribe in FAILED channel state", channel.name); - Log.v(TAG, errorString); - ErrorInfo errorInfo = new ErrorInfo(errorString, 90001); - throw AblyException.fromErrorInfo(errorInfo); - } - channel.attach(completionListener); - } - - /* End sync and emit leave messages for residual members */ - private void endSyncAndEmitLeaves() { - currentSyncChannelSerial = null; - List residualMembers = presence.endSync(); - for (PresenceMessage member: residualMembers) { - /* - * RTP19: ... The PresenceMessage published should contain the original attributes of the presence - * member with the action set to LEAVE, PresenceMessage#id set to null, and the timestamp set - * to the current time ... - */ - member.action = PresenceMessage.Action.leave; - member.id = null; - member.timestamp = System.currentTimeMillis(); - } - broadcastPresence(residualMembers.toArray(new PresenceMessage[residualMembers.size()])); - - /** - * (RTP5c2) If a SYNC is initiated as part of the attach, then once the SYNC is complete, - * all members not present in the PresenceMap but present in the internal PresenceMap must - * be re-entered automatically by the client using the clientId and data attributes from - * each. The members re-entered automatically must be removed from the internal PresenceMap - * ensuring that members present on the channel are constructed from presence events sent - * from Ably since the channel became ATTACHED - */ - if (syncAsResultOfAttach) { - syncAsResultOfAttach = false; - for (PresenceMessage item: internalPresence.values()) { - if (presence.put(item)) { - /* Message is new to presence map, send it */ - final String clientId = item.clientId; - try { - PresenceMessage itemToSend = (PresenceMessage)item.clone(); - itemToSend.action = PresenceMessage.Action.enter; - updatePresence(itemToSend, new CompletionListener() { - @Override - public void onSuccess() { - } - - @Override - public void onError(ErrorInfo reason) { - /* - * (RTP5c3) If any of the automatic ENTER presence messages published - * in RTP5c2 fail, then an UPDATE event should be emitted on the channel - * with resumed set to true and reason set to an ErrorInfo object with error - * code value 91004 and the error message string containing the message - * received from Ably (if applicable), the code received from Ably - * (if applicable) and the explicit or implicit client_id of the PresenceMessage - */ - String errorString = String.format("Cannot automatically re-enter %s on channel %s (%s)", - clientId, channel.name, reason.message); - Log.e(TAG, errorString); - channel.emitUpdate(new ErrorInfo(errorString, 91004), true); - } - }); - } catch(AblyException e) { - String errorString = String.format("Cannot automatically re-enter %s on channel %s (%s)", - clientId, channel.name, e.errorInfo.message); - Log.e(TAG, errorString); - channel.emitUpdate(new ErrorInfo(errorString, 91004), true); - } - } - } - internalPresence.clear(); - } - } - - void setPresence(PresenceMessage[] messages, boolean broadcast, String syncChannelSerial) { - Log.v(TAG, "setPresence(); channel = " + channel.name + "; broadcast = " + broadcast + "; syncChannelSerial = " + syncChannelSerial); - String syncCursor = null; - if(syncChannelSerial != null) { - int colonPos = syncChannelSerial.indexOf(':'); - String serial = colonPos >= 0 ? syncChannelSerial.substring(0, colonPos) : syncChannelSerial; - /* Discard incomplete sync if serial has changed */ - if (presence.syncInProgress && currentSyncChannelSerial != null && !currentSyncChannelSerial.equals(serial)) - endSyncAndEmitLeaves(); - syncCursor = syncChannelSerial.substring(colonPos); - if(syncCursor.length() > 1) { - presence.startSync(); - currentSyncChannelSerial = serial; - } - } - for(PresenceMessage update : messages) { - boolean updateInternalPresence = update.connectionId.equals(channel.ably.connection.id); - boolean broadcastThisUpdate = broadcast; - PresenceMessage originalUpdate = update; - - switch(update.action) { - case enter: - case update: - update = (PresenceMessage)update.clone(); - update.action = PresenceMessage.Action.present; - case present: - broadcastThisUpdate &= presence.put(update); - if(updateInternalPresence) - internalPresence.put(update); - break; - case leave: - broadcastThisUpdate &= presence.remove(update); - if(updateInternalPresence) - internalPresence.remove(update); - break; - case absent: - } - - /* - * RTP2g: Any incoming presence message that passes the newness check should be emitted on the - * Presence object, with an event name set to its original action. - */ - if (broadcastThisUpdate) - broadcastPresence(new PresenceMessage[]{originalUpdate}); - } - - /* if this is the last message in a sequence of sync updates, end the sync */ - if(syncChannelSerial == null || syncCursor.length() <= 1) { - endSyncAndEmitLeaves(); - } - } - - private void broadcastPresence(PresenceMessage[] messages) { - for(PresenceMessage message : messages) { - listeners.onPresenceMessage(message); - - Multicaster eventListener = eventListeners.get(message.action); - if(eventListener != null) - eventListener.onPresenceMessage(message); - } - } - - private final Multicaster listeners = new Multicaster(); - private final EnumMap eventListeners = new EnumMap<>(PresenceMessage.Action.class); - - private static class Multicaster extends io.ably.lib.util.Multicaster implements PresenceListener { - @Override - public void onPresenceMessage(PresenceMessage message) { - for(PresenceListener member : members) - try { - member.onPresenceMessage(message); - } catch(Throwable t) {} - } - } - - private void subscribeImpl(PresenceMessage.Action action, PresenceListener listener) { - Multicaster listeners = eventListeners.get(action); - if(listeners == null) { - listeners = new Multicaster(); - eventListeners.put(action, listeners); - } - listeners.add(listener); - } - - private void unsubscribeImpl(PresenceMessage.Action action, PresenceListener listener) { - Multicaster listeners = eventListeners.get(action); - if(listeners != null) { - listeners.remove(listener); - if(listeners.isEmpty()) { - eventListeners.remove(action); - } - } - } - - - /************************************ - * enter/leave and pending messages - ************************************/ - - /** - * Enter this client into this channel. This client will be added to the presence set - * and presence subscribers will see an enter message for this client. - * @param data: optional data (eg a status message) for this member. - * See {@link io.ably.types.Data} for the supported data types. - * @param listener: a listener to be notified on completion of the operation. - * @throws AblyException - */ - public void enter(Object data, CompletionListener listener) throws AblyException { - Log.v(TAG, "enter(); channel = " + channel.name); - updatePresence(new PresenceMessage(PresenceMessage.Action.enter, null, data), listener); - } - - /** - * Update the presence data for this client. If the client is not already a member of - * the presence set it will be added, and presence subscribers will see an enter or - * update message for this client. - * @param data: optional data (eg a status message) for this member. - * See {@link io.ably.types.Data} for the supported data types. - * @param listener: a listener to be notified on completion of the operation. - * @throws AblyException - */ - public void update(Object data, CompletionListener listener) throws AblyException { - Log.v(TAG, "update(); channel = " + channel.name); - updatePresence(new PresenceMessage(PresenceMessage.Action.update, null, data), listener); - } - - /** - * Leave this client from this channel. This client will be removed from the presence - * set and presence subscribers will see a leave message for this client. - * @param data: optional data (eg a status message) for this member. - * See {@link io.ably.types.Data} for the supported data types. - * @param listener: a listener to be notified on completion of the operation. - * @throws AblyException - */ - public void leave(Object data, CompletionListener listener) throws AblyException { - Log.v(TAG, "leave(); channel = " + channel.name); - updatePresence(new PresenceMessage(PresenceMessage.Action.leave, null, data), listener); - } - - /** - * Leave this client from this channel. This client will be removed from the presence - * set and presence subscribers will see a leave message for this client. - * @param listener: a listener to be notified on completion of the operation. - * @throws AblyException - */ - public void leave(CompletionListener listener) throws AblyException { - leave(null, listener); - } - - /** - * Enter a specified client into this channel. The given clientId will be added to - * the presence set and presence subscribers will see a corresponding presence message - * with an empty data payload. - * This method is provided to support connections (eg connections from application - * server instances) that act on behalf of multiple clientIds. In order to be able to - * enter the channel with this method, the client library must have been instanced - * either with a key, or with a token bound to the wildcard clientId. - * @param clientId: the id of the client. - */ - public void enterClient(String clientId) throws AblyException { - enterClient(clientId, null); - } - - /** - * Enter a specified client into this channel. The given client will be added to the - * presence set and presence subscribers will see a corresponding presence message. - * This method is provided to support connections (eg connections from application - * server instances) that act on behalf of multiple clientIds. In order to be able to - * enter the channel with this method, the client library must have been instanced - * either with a key, or with a token bound to the wildcard clientId. - * @param clientId: the id of the client. - * @param data: optional data (eg a status message) for this member. - * @throws AblyException - */ - public void enterClient(String clientId, Object data) throws AblyException { - enterClient(clientId, data, null); - } - - /** - * Enter a specified client into this channel. The given client will be added to the - * presence set and presence subscribers will see a corresponding presence message. - * This method is provided to support connections (eg connections from application - * server instances) that act on behalf of multiple clientIds. In order to be able to - * enter the channel with this method, the client library must have been instanced - * either with a key, or with a token bound to the wildcard clientId. - * @param clientId: the id of the client. - * @param data: optional data (eg a status message) for this member. - * @param listener: a listener to be notified on completion of the operation. - * @throws AblyException - */ - public void enterClient(String clientId, Object data, CompletionListener listener) throws AblyException { - if(clientId == null) { - String errorMessage = String.format("Channel %s: unable to enter presence channel (null clientId specified)", channel.name); - Log.v(TAG, errorMessage); - if(listener != null) { - listener.onError(new ErrorInfo(errorMessage, 40000)); - return; - } - } - Log.v(TAG, "enterClient(); channel = " + channel.name + "; clientId = " + clientId); - updatePresence(new PresenceMessage(PresenceMessage.Action.enter, clientId, data), listener); - } - - /** - * Update the presence data for a specified client into this channel. - * If the client is not already a member of the presence set it will be added, - * and presence subscribers will see a corresponding presence message - * with an empty data payload. As for #enterClient above, the connection - * must be authenticated in a way that enables it to represent an arbitrary clientId. - * @param clientId: the id of the client. - * @throws AblyException - */ - public void updateClient(String clientId) throws AblyException { - updateClient(clientId, null); - } - - /** - * Update the presence data for a specified client into this channel. - * If the client is not already a member of the presence set it will be added, and - * presence subscribers will see an enter or update message for this client. - * As for #enterClient above, the connection must be authenticated in a way that - * enables it to represent an arbitrary clientId. - * @param clientId: the id of the client. - * @param data: optional data (eg a status message) for this member. - * @throws AblyException - */ - public void updateClient(String clientId, Object data) throws AblyException { - updateClient(clientId, data, null); - } - - /** - * Update the presence data for a specified client into this channel. - * If the client is not already a member of the presence set it will be added, and - * presence subscribers will see an enter or update message for this client. - * As for #enterClient above, the connection must be authenticated in a way that - * enables it to represent an arbitrary clientId. - * @param clientId: the id of the client. - * @param data: optional data (eg a status message) for this member. - * @param listener: a listener to be notified on completion of the operation. - * @throws AblyException - */ - public void updateClient(String clientId, Object data, CompletionListener listener) throws AblyException { - if(clientId == null) { - String errorMessage = String.format("Channel %s: unable to update presence channel (null clientId specified)", channel.name); - Log.v(TAG, errorMessage); - if(listener != null) { - listener.onError(new ErrorInfo(errorMessage, 40000)); - return; - } - } - Log.v(TAG, "updateClient(); channel = " + channel.name + "; clientId = " + clientId); - updatePresence(new PresenceMessage(PresenceMessage.Action.update, clientId, data), listener); - } - - /** - * Leave a given client from this channel. This client will be removed from the - * presence set and presence subscribers will see a corresponding presence message - * with an empty data payload. - * @param clientId: the id of the client. - * @throws AblyException - */ - public void leaveClient(String clientId) throws AblyException { - leaveClient(clientId, null); - } - - /** - * Leave a given client from this channel. This client will be removed from the - * presence set and presence subscribers will see a leave message for this client. - * @param clientId: the id of the client. - * @param data: optional data (eg a status message) for this member. - * @throws AblyException - */ - public void leaveClient(String clientId, Object data) throws AblyException { - leaveClient(clientId, data, null); - } - - /** - * Leave a given client from this channel. This client will be removed from the - * presence set and presence subscribers will see a leave message for this client. - * @param clientId: the id of the client. - * @param data: optional data (eg a status message) for this member. - * @param listener: a listener to be notified on completion of the operation. - * @throws AblyException - */ - public void leaveClient(String clientId, Object data, CompletionListener listener) throws AblyException { - if(clientId == null) { - String errorMessage = String.format("Channel %s: unable to leave presence channel (null clientId specified)", channel.name); - Log.v(TAG, errorMessage); - if(listener != null) { - listener.onError(new ErrorInfo(errorMessage, 40000)); - return; - } - } - Log.v(TAG, "leaveClient(); channel = " + channel.name + "; clientId = " + clientId); - updatePresence(new PresenceMessage(PresenceMessage.Action.leave, clientId, data), listener); - } - - /** - * Update the presence for this channel with a given PresenceMessage update. - * The connection must be authenticated in a way that enables it to represent - * the clientId in the message. - * @param msg: the presence message - * @param listener: a listener to be notified on completion of the operation. - * @throws AblyException - */ - public void updatePresence(PresenceMessage msg, CompletionListener listener) throws AblyException { - Log.v(TAG, "update(); channel = " + channel.name); - - AblyRealtime ably = channel.ably; - boolean connected = (ably.connection.state == ConnectionState.connected); - String clientId; - try { - clientId = ably.auth.checkClientId(msg, false, connected); - } catch(AblyException e) { - if(listener != null) { - listener.onError(e.errorInfo); - } - return; - } - - msg.encode(null); - synchronized(channel) { - switch(channel.state) { - case initialized: - channel.attach(); - case attaching: - QueuedPresence queued = new QueuedPresence(msg, listener); - pendingPresence.put(clientId, queued); - break; - case attached: - ProtocolMessage message = new ProtocolMessage(ProtocolMessage.Action.presence, channel.name); - message.presence = new PresenceMessage[] { msg }; - ConnectionManager connectionManager = ably.connection.connectionManager; - connectionManager.send(message, ably.options.queueMessages, listener); - break; - default: - throw AblyException.fromErrorInfo(new ErrorInfo("Unable to enter presence channel in detached or failed state", 400, 91001)); - } - } - } - - /************************************ - * history - ************************************/ - - /** - * Obtain recent history for this channel using the REST API. - * The history provided relates to all clients of this application, - * not just this instance. - * @param params: the request params. See the Ably REST API - * documentation for more details. - * @return: an array of Messgaes for this Channel. - * @throws AblyException - */ - public PaginatedResult history(Param[] params) throws AblyException { - return historyImpl(params).sync(); - } - - public void historyAsync(Param[] params, Callback> callback) { - historyImpl(params).async(callback); - } - - private BasePaginatedQuery.ResultRequest historyImpl(Param[] params) { - try { - params = Channel.replacePlaceholderParams(channel, params); - } catch (AblyException e) { - return new BasePaginatedQuery.ResultRequest.Failed(e); - } - - AblyRealtime ably = channel.ably; - HttpCore.BodyHandler bodyHandler = PresenceSerializer.getPresenceResponseHandler(channel.options); - return new BasePaginatedQuery(ably.http, channel.basePath + "/presence/history", HttpUtils.defaultAcceptHeaders(ably.options.useBinaryProtocol), params, bodyHandler).get(); - } - - /** - * internal - * - */ - private static class QueuedPresence { - public PresenceMessage msg; - public CompletionListener listener; - QueuedPresence(PresenceMessage msg, CompletionListener listener) { this.msg = msg; this.listener = listener; } - } - - private final Map pendingPresence = new HashMap(); - - private void sendQueuedMessages() { - Log.v(TAG, "sendQueuedMessages()"); - AblyRealtime ably = channel.ably; - boolean queueMessages = ably.options.queueMessages; - ConnectionManager connectionManager = ably.connection.connectionManager; - int count = pendingPresence.size(); - if(count == 0) - return; - - ProtocolMessage message = new ProtocolMessage(ProtocolMessage.Action.presence, channel.name); - Iterator allQueued = pendingPresence.values().iterator(); - PresenceMessage[] presenceMessages = message.presence = new PresenceMessage[count]; - CompletionListener listener; - - if(count == 1) { - QueuedPresence queued = allQueued.next(); - presenceMessages[0] = queued.msg; - listener = queued.listener; - } else { - int idx = 0; - CompletionListener.Multicaster mListener = new CompletionListener.Multicaster(); - while(allQueued.hasNext()) { - QueuedPresence queued = allQueued.next(); - presenceMessages[idx++] = queued.msg; - if(queued.listener != null) - mListener.add(queued.listener); - } - listener = mListener.isEmpty() ? null : mListener; - } - pendingPresence.clear(); - try { - connectionManager.send(message, queueMessages, listener); - } catch(AblyException e) { - Log.e(TAG, "sendQueuedMessages(): Unexpected exception sending message", e); - if(listener != null) - listener.onError(e.errorInfo); - } - } - - private void failQueuedMessages(ErrorInfo reason) { - Log.v(TAG, "failQueuedMessages()"); - for(QueuedPresence msg : pendingPresence.values()) - if(msg.listener != null) - try { - msg.listener.onError(reason); - } catch(Throwable t) { - Log.e(TAG, "failQueuedMessages(): Unexpected exception calling listener", t); - } - pendingPresence.clear(); - } - - - /************************************ - * attach / detach - ************************************/ - - void setAttached(boolean hasPresence) { - /* Start sync, if hasPresence is not set end sync immediately dropping all the current presence members */ - presence.startSync(); - syncAsResultOfAttach = true; - if (!hasPresence) { - /* - * RTP19a If the PresenceMap has existing members when an ATTACHED message is received without a - * HAS_PRESENCE flag, the client library should emit a LEAVE event for each existing member ... - */ - endSyncAndEmitLeaves(); - } - sendQueuedMessages(); - } - - void setDetached(ErrorInfo reason) { - /* Interrupt get() call if needed */ - synchronized (presence) { - presence.notifyAll(); - } - - /** - * (RTP5a) If the channel enters the DETACHED or FAILED state then all queued presence - * messages will fail immediately, and the PresenceMap and internal PresenceMap is cleared. - * The latter ensures members are not automatically re-entered if the Channel later becomes attached - */ - failQueuedMessages(reason); - presence.clear(); - internalPresence.clear(); - } - - void setSuspended(ErrorInfo reason) { - /* Interrupt get() call if needed */ - synchronized (presence) { - presence.notifyAll(); - } - - /* - * (RTP5f) If the channel enters the SUSPENDED state then all queued presence messages will fail - * immediately, and the PresenceMap is maintained - */ - failQueuedMessages(reason); - } - - /** - * A class encapsulating a map of the members of this presence channel, - * indexed by a String key that is a combination of connectionId and clientId. - * This map synchronises the membership of the presence set by handling - * sync messages from the service. Since sync messages can be out-of-order - - * eg an enter sync event being received after that member has in fact left - - * this map keeps "witness" entries, with absent Action, to remember the - * fact that a leave event has been seen for a member. These entries are - * cleared once the last set of updates of a sync sequence have been received. - * - */ - private class PresenceMap { - - /** - * Wait for sync to be complete. If we are in attaching state wait for initial sync to - * complete as well. Return false if wait was interrupted because channel transitioned to - * state other than attached or attaching - */ - synchronized void waitForSync() throws AblyException, InterruptedException { - boolean syncIsComplete = false; /* temporary variable to avoid potential race conditions */ - while((channel.state == ChannelState.attached || channel.state == ChannelState.attaching) && - /* = (and not ==) is intentional */ - !(syncIsComplete = (!syncInProgress && syncComplete))) - wait(); - - /* invalid channel state */ - int errorCode; - String errorMessage; - - if (channel.state == ChannelState.suspended) { - /* (RTP11d) If the Channel is in the SUSPENDED state then the get function will by default, - * or if waitForSync is set to true, result in an error with code 91005 and a message stating - * that the presence state is out of sync due to the channel being in a SUSPENDED state */ - errorCode = 91005; - errorMessage = String.format("Channel %s: presence state is out of sync due to the channel being in a SUSPENDED state", channel.name); - } else if(syncIsComplete) { - return; - } else { - errorCode = 90001; - errorMessage = String.format("Channel %s: cannot get presence state because channel is in invalid state", channel.name); - } - Log.v(TAG, errorMessage); - throw AblyException.fromErrorInfo(new ErrorInfo(errorMessage, errorCode)); - } - - synchronized Collection get(Param[] params) throws AblyException, InterruptedException { - boolean waitForSync = true; - String clientId = null; - String connectionId = null; - - for (Param param: params) { - switch (param.key) { - case GET_WAITFORSYNC: - waitForSync = Boolean.valueOf(param.value); - break; - case GET_CLIENTID: - clientId = param.value; - break; - case GET_CONNECTIONID: - connectionId = param.value; - break; - } - } - - HashSet result = new HashSet<>(); - if (waitForSync) - waitForSync(); - - for (Map.Entry entry: members.entrySet()) { - PresenceMessage member = entry.getValue(); - if ((clientId == null || member.clientId.equals(clientId)) && - (connectionId == null || member.connectionId.equals(connectionId))) - result.add(member); - } - - return result; - } - - /** - * Add or update the presence state for a member - * @param item - * @return true if the given message represents a change; - * false if the message is already superseded - */ - synchronized boolean put(PresenceMessage item) { - String key = item.memberKey(); - /* we've seen this member, so do not remove it at the end of sync */ - if(residualMembers != null) - residualMembers.remove(key); - - /* check if there is a newer existing member (or absent witness) */ - if (hasNewerItem(key, item)) - return false; - - members.put(key, item); - return true; - } - - /** - * Determine if there is a newer item already in the map - * @param key key used to search the item in the map - * @param item new presence message to be added - * @return true if there is a newer item - */ - synchronized boolean hasNewerItem(String key, PresenceMessage item) { - PresenceMessage existingItem = members.get(key); - if(existingItem == null) - return false; - - /* - * (RTP2b1) If either presence message has a connectionId which is not an initial substring - * of its id, compare them by timestamp numerically. (This will be the case when one of them - * is a 'synthesized leave' event sent by realtime to indicate a connection disconnected - * unexpectedly 15s ago. Such messages will have an id that does not correspond to its - * connectionId, as it wasn't actually published by that connection - */ - if(item.connectionId != null && existingItem.connectionId != null && - (!item.id.startsWith(item.connectionId) || !existingItem.id.startsWith(existingItem.connectionId))) - return existingItem.timestamp >= item.timestamp; - - /* - * (RTP2b2) Else split the id of both presence messages (which will be of the form - * connid:msgSerial:index, e.g. aaaaaa:0:0) on the separator :, and parse the latter two as - * integers. Compare them first by msgSerial numerically, then (if @msgSerial@s are equal) by - * index numerically, larger being newer in both cases - */ - String[] itemComponents = item.id.split(":", 3); - String[] existingItemComponents = existingItem.id.split(":", 3); - - if(itemComponents.length < 3 || existingItemComponents.length < 3) - return false; - - try { - long messageSerial = Long.valueOf(itemComponents[1]); - long messageIndex = Long.valueOf(itemComponents[2]); - long existingMessageSerial = Long.valueOf(existingItemComponents[1]); - long existingMessageIndex = Long.valueOf(existingItemComponents[2]); - - return existingMessageSerial > messageSerial || - (existingMessageSerial == messageSerial && existingMessageIndex >= messageIndex); - } - catch(NumberFormatException e) { - return false; - } - } - - /** - * Get all members based on the current state (even if sync is in progress) - * @return - */ - synchronized Collection values() { - try { return values(false); } catch (InterruptedException|AblyException e) { return null; } - } - - /** - * Get all members, optionally waiting if a sync is in progress. - * @param wait - * @return - * @throws InterruptedException - */ - synchronized Collection values(boolean wait) throws AblyException, InterruptedException { - Set result = new HashSet(); - if(wait) - waitForSync(); - result.addAll(members.values()); - for(Iterator it = result.iterator(); it.hasNext();) { - PresenceMessage entry = it.next(); - if(entry.action == PresenceMessage.Action.absent) { - it.remove(); - } - } - return result; - } - - /** - * Remove a member. - * @param item - * @return - */ - synchronized boolean remove(PresenceMessage item) { - String key = item.memberKey(); - if (hasNewerItem(key, item)) - return false; - PresenceMessage existingItem = members.remove(key); - if(existingItem != null && existingItem.action == PresenceMessage.Action.absent) - return false; - return true; - } - - /** - * Start a sync sequence. - * Note that this is called each time a sync message is received that is not - * the last. - */ - synchronized void startSync() { - Log.v(TAG, "startSync(); channel = " + channel.name + "; syncInProgress = " + syncInProgress); - /* we might be called multiple times while a sync is in progress */ - if(!syncInProgress) { - residualMembers = new HashSet(members.keySet()); - syncInProgress = true; - } - } - - /** - * Finish a sync sequence. Returns "residual" items that were removed as a part of a sync - */ - synchronized List endSync() { - Log.v(TAG, "endSync(); channel = " + channel.name + "; syncInProgress = " + syncInProgress); - ArrayList removedEntries = new ArrayList<>(); - if(syncInProgress) { - /* we can now strip out the absent members, as we have - * received all of the out-of-order sync messages */ - for(Iterator> it = members.entrySet().iterator(); it.hasNext();) { - Map.Entry entry = it.next(); - if(entry.getValue().action == PresenceMessage.Action.absent) { - it.remove(); - } - } - /* any members that were present at the start of the sync, - * and have not been seen in sync, can be removed */ - for(String itemKey: residualMembers) { - /* clone presence message as it still can be in the internal presence map */ - removedEntries.add((PresenceMessage)members.get(itemKey).clone()); - members.remove(itemKey); - } - residualMembers = null; - - /* finish, notifying any waiters */ - syncInProgress = false; - } - syncComplete = true; - notifyAll(); - return removedEntries; - } - - /** - * Clear all entries - */ - synchronized void clear() { - members.clear(); - if(residualMembers != null) - residualMembers.clear(); - } - - private boolean syncInProgress; - private Collection residualMembers; - private final HashMap members = new HashMap(); - } - - private final PresenceMap presence = new PresenceMap(); - private final PresenceMap internalPresence = new PresenceMap(); - - /************************************ - * general - ************************************/ - - Presence(Channel channel) { - this.channel = channel; - } - - private static final String TAG = Channel.class.getName(); - - private final Channel channel; - - /* channel serial if sync is in progress */ - private String currentSyncChannelSerial; - /* Sync in progress is a result of attach operation */ - private boolean syncAsResultOfAttach; - - /** - * (RTP13) Presence#syncComplete returns true if the initial SYNC operation has completed for - * the members present on the channel - */ - public boolean syncComplete; + /************************************ + * subscriptions and PresenceListener + ************************************/ + + /** + * String parameter names for get() call with Param... as an argument + */ + public final static String GET_WAITFORSYNC = "waitForSync"; + public final static String GET_CLIENTID = "clientId"; + public final static String GET_CONNECTIONID = "connectionId"; + + /** + * Get the presence state for this channel. Take Param[] array as an argument. + * Implicitly attaches the channel. However, if the channel is in or moves to the FAILED + * state before the operation succeeds, it will result in an error + * @param params + * @return + * @throws AblyException + * @throws InterruptedException + */ + public synchronized PresenceMessage[] get(Param... params) throws AblyException { + if (channel.state == ChannelState.failed) { + throw AblyException.fromErrorInfo(new ErrorInfo("channel operation failed (invalid channel state)", 90001)); + } + + channel.attach(); + try { + Collection values = presence.get(params); + return values.toArray(new PresenceMessage[values.size()]); + } catch (InterruptedException e) { + Log.v(TAG, String.format("Channel %s: get() operation interrupted", channel.name)); + throw AblyException.fromThrowable(e); + } + } + + /** + * Get the presence state for this Channel, optionally waiting for sync to complete. + * Implicitly attaches the Channel. However, if the channel is in or moves to the FAILED + * state before the operation succeeds, it will result in an error + * @return: the current present members. + * @throws AblyException + */ + public synchronized PresenceMessage[] get(boolean wait) throws AblyException { + return get(new Param(GET_WAITFORSYNC, String.valueOf(wait))); + } + + /** + * Get the presence state for a given clientId. Implicitly attaches the + * Channel. However, if the channel is in or moves to the FAILED + * state before the operation succeeds, it will result in an error + * @param wait + * @return + * @throws InterruptedException + * @throws AblyException + */ + public synchronized PresenceMessage[] get(String clientId, boolean wait) throws AblyException { + return get(new Param(GET_WAITFORSYNC, String.valueOf(wait)), new Param(GET_CLIENTID, clientId)); + } + + /** + * An interface allowing a listener to be notified of arrival of a presence message. + */ + public interface PresenceListener { + void onPresenceMessage(PresenceMessage message); + } + + /** + * Subscribe to presence events on the associated Channel. This implicitly + * attaches the Channel if it is not already attached. + * @param listener: the listener to me notified on arrival of presence messages. + * @param completionListener listener to be called on success/failure + * @throws AblyException + */ + public void subscribe(PresenceListener listener, CompletionListener completionListener) throws AblyException { + implicitAttachOnSubscribe(completionListener); + listeners.add(listener); + } + + /** + * Same as above without completion listener + */ + public void subscribe(PresenceListener listener) throws AblyException { + subscribe(listener, null); + } + + /** + * Unsubscribe a previously subscribed presence listener for this channel. + * @param listener: the previously subscribed listener. + */ + public void unsubscribe(PresenceListener listener) { + listeners.remove(listener); + for (Multicaster multicaster: eventListeners.values()) { + multicaster.remove(listener); + } + } + + /** + * Subscribe to presence events with a specific action on the associated Channel. + * This implicitly attaches the Channel if it is not already attached. + * + * @param action to be observed + * @param listener + * @param completionListener listener to be called on success/failure + * @throws AblyException + */ + public void subscribe(PresenceMessage.Action action, PresenceListener listener, CompletionListener completionListener) throws AblyException { + implicitAttachOnSubscribe(completionListener); + subscribeImpl(action, listener); + } + + /** + * Same as above without completion listener + */ + public void subscribe(PresenceMessage.Action action, PresenceListener listener) throws AblyException { + subscribe(action, listener, null); + } + + /** + * Unsubscribe a previously subscribed presence listener for this channel from specific action. + * + * @param action + * @param listener + */ + public void unsubscribe(PresenceMessage.Action action, PresenceListener listener) { + unsubscribeImpl(action, listener); + } + + /** + * Subscribe to presence events with specific actions on the associated Channel. + * This implicitly attaches the Channel if it is not already attached. + * + * @param actions to be observed + * @param listener + * @param completionListener listener to be called on success/failure + * @throws AblyException + */ + public void subscribe(EnumSet actions, PresenceListener listener, CompletionListener completionListener) throws AblyException { + implicitAttachOnSubscribe(completionListener); + for (PresenceMessage.Action action : actions) { + subscribeImpl(action, listener); + } + } + + /** + * Same as above without completion listener + */ + public void subscribe(EnumSet actions, PresenceListener listener) throws AblyException { + subscribe(actions, listener, null); + } + + /** + * Unsubscribe a previously subscribed presence listener for this channel from specific actions. + * + * @param actions + * @param listener + */ + public void unsubscribe(EnumSet actions, PresenceListener listener) { + for (PresenceMessage.Action action : actions) { + unsubscribeImpl(action, listener); + } + } + + /** + * Unsubscribe all subscribed presence lisceners for this channel. + */ + public void unsubscribe() { + listeners.clear(); + eventListeners.clear(); + } + + + /*** + * internal + * + */ + + /** + * Implicitly attach channel on subscribe. Throw exception if channel is in failed state + * @param completionListener + * @throws AblyException + */ + private void implicitAttachOnSubscribe(CompletionListener completionListener) throws AblyException { + if (channel.state == ChannelState.failed) { + String errorString = String.format("Channel %s: subscribe in FAILED channel state", channel.name); + Log.v(TAG, errorString); + ErrorInfo errorInfo = new ErrorInfo(errorString, 90001); + throw AblyException.fromErrorInfo(errorInfo); + } + channel.attach(completionListener); + } + + /* End sync and emit leave messages for residual members */ + private void endSyncAndEmitLeaves() { + currentSyncChannelSerial = null; + List residualMembers = presence.endSync(); + for (PresenceMessage member: residualMembers) { + /* + * RTP19: ... The PresenceMessage published should contain the original attributes of the presence + * member with the action set to LEAVE, PresenceMessage#id set to null, and the timestamp set + * to the current time ... + */ + member.action = PresenceMessage.Action.leave; + member.id = null; + member.timestamp = System.currentTimeMillis(); + } + broadcastPresence(residualMembers.toArray(new PresenceMessage[residualMembers.size()])); + + /** + * (RTP5c2) If a SYNC is initiated as part of the attach, then once the SYNC is complete, + * all members not present in the PresenceMap but present in the internal PresenceMap must + * be re-entered automatically by the client using the clientId and data attributes from + * each. The members re-entered automatically must be removed from the internal PresenceMap + * ensuring that members present on the channel are constructed from presence events sent + * from Ably since the channel became ATTACHED + */ + if (syncAsResultOfAttach) { + syncAsResultOfAttach = false; + for (PresenceMessage item: internalPresence.values()) { + if (presence.put(item)) { + /* Message is new to presence map, send it */ + final String clientId = item.clientId; + try { + PresenceMessage itemToSend = (PresenceMessage)item.clone(); + itemToSend.action = PresenceMessage.Action.enter; + updatePresence(itemToSend, new CompletionListener() { + @Override + public void onSuccess() { + } + + @Override + public void onError(ErrorInfo reason) { + /* + * (RTP5c3) If any of the automatic ENTER presence messages published + * in RTP5c2 fail, then an UPDATE event should be emitted on the channel + * with resumed set to true and reason set to an ErrorInfo object with error + * code value 91004 and the error message string containing the message + * received from Ably (if applicable), the code received from Ably + * (if applicable) and the explicit or implicit client_id of the PresenceMessage + */ + String errorString = String.format("Cannot automatically re-enter %s on channel %s (%s)", + clientId, channel.name, reason.message); + Log.e(TAG, errorString); + channel.emitUpdate(new ErrorInfo(errorString, 91004), true); + } + }); + } catch(AblyException e) { + String errorString = String.format("Cannot automatically re-enter %s on channel %s (%s)", + clientId, channel.name, e.errorInfo.message); + Log.e(TAG, errorString); + channel.emitUpdate(new ErrorInfo(errorString, 91004), true); + } + } + } + internalPresence.clear(); + } + } + + void setPresence(PresenceMessage[] messages, boolean broadcast, String syncChannelSerial) { + Log.v(TAG, "setPresence(); channel = " + channel.name + "; broadcast = " + broadcast + "; syncChannelSerial = " + syncChannelSerial); + String syncCursor = null; + if(syncChannelSerial != null) { + int colonPos = syncChannelSerial.indexOf(':'); + String serial = colonPos >= 0 ? syncChannelSerial.substring(0, colonPos) : syncChannelSerial; + /* Discard incomplete sync if serial has changed */ + if (presence.syncInProgress && currentSyncChannelSerial != null && !currentSyncChannelSerial.equals(serial)) + endSyncAndEmitLeaves(); + syncCursor = syncChannelSerial.substring(colonPos); + if(syncCursor.length() > 1) { + presence.startSync(); + currentSyncChannelSerial = serial; + } + } + for(PresenceMessage update : messages) { + boolean updateInternalPresence = update.connectionId.equals(channel.ably.connection.id); + boolean broadcastThisUpdate = broadcast; + PresenceMessage originalUpdate = update; + + switch(update.action) { + case enter: + case update: + update = (PresenceMessage)update.clone(); + update.action = PresenceMessage.Action.present; + case present: + broadcastThisUpdate &= presence.put(update); + if(updateInternalPresence) + internalPresence.put(update); + break; + case leave: + broadcastThisUpdate &= presence.remove(update); + if(updateInternalPresence) + internalPresence.remove(update); + break; + case absent: + } + + /* + * RTP2g: Any incoming presence message that passes the newness check should be emitted on the + * Presence object, with an event name set to its original action. + */ + if (broadcastThisUpdate) + broadcastPresence(new PresenceMessage[]{originalUpdate}); + } + + /* if this is the last message in a sequence of sync updates, end the sync */ + if(syncChannelSerial == null || syncCursor.length() <= 1) { + endSyncAndEmitLeaves(); + } + } + + private void broadcastPresence(PresenceMessage[] messages) { + for(PresenceMessage message : messages) { + listeners.onPresenceMessage(message); + + Multicaster eventListener = eventListeners.get(message.action); + if(eventListener != null) + eventListener.onPresenceMessage(message); + } + } + + private final Multicaster listeners = new Multicaster(); + private final EnumMap eventListeners = new EnumMap<>(PresenceMessage.Action.class); + + private static class Multicaster extends io.ably.lib.util.Multicaster implements PresenceListener { + @Override + public void onPresenceMessage(PresenceMessage message) { + for(PresenceListener member : members) + try { + member.onPresenceMessage(message); + } catch(Throwable t) {} + } + } + + private void subscribeImpl(PresenceMessage.Action action, PresenceListener listener) { + Multicaster listeners = eventListeners.get(action); + if(listeners == null) { + listeners = new Multicaster(); + eventListeners.put(action, listeners); + } + listeners.add(listener); + } + + private void unsubscribeImpl(PresenceMessage.Action action, PresenceListener listener) { + Multicaster listeners = eventListeners.get(action); + if(listeners != null) { + listeners.remove(listener); + if(listeners.isEmpty()) { + eventListeners.remove(action); + } + } + } + + + /************************************ + * enter/leave and pending messages + ************************************/ + + /** + * Enter this client into this channel. This client will be added to the presence set + * and presence subscribers will see an enter message for this client. + * @param data: optional data (eg a status message) for this member. + * See {@link io.ably.types.Data} for the supported data types. + * @param listener: a listener to be notified on completion of the operation. + * @throws AblyException + */ + public void enter(Object data, CompletionListener listener) throws AblyException { + Log.v(TAG, "enter(); channel = " + channel.name); + updatePresence(new PresenceMessage(PresenceMessage.Action.enter, null, data), listener); + } + + /** + * Update the presence data for this client. If the client is not already a member of + * the presence set it will be added, and presence subscribers will see an enter or + * update message for this client. + * @param data: optional data (eg a status message) for this member. + * See {@link io.ably.types.Data} for the supported data types. + * @param listener: a listener to be notified on completion of the operation. + * @throws AblyException + */ + public void update(Object data, CompletionListener listener) throws AblyException { + Log.v(TAG, "update(); channel = " + channel.name); + updatePresence(new PresenceMessage(PresenceMessage.Action.update, null, data), listener); + } + + /** + * Leave this client from this channel. This client will be removed from the presence + * set and presence subscribers will see a leave message for this client. + * @param data: optional data (eg a status message) for this member. + * See {@link io.ably.types.Data} for the supported data types. + * @param listener: a listener to be notified on completion of the operation. + * @throws AblyException + */ + public void leave(Object data, CompletionListener listener) throws AblyException { + Log.v(TAG, "leave(); channel = " + channel.name); + updatePresence(new PresenceMessage(PresenceMessage.Action.leave, null, data), listener); + } + + /** + * Leave this client from this channel. This client will be removed from the presence + * set and presence subscribers will see a leave message for this client. + * @param listener: a listener to be notified on completion of the operation. + * @throws AblyException + */ + public void leave(CompletionListener listener) throws AblyException { + leave(null, listener); + } + + /** + * Enter a specified client into this channel. The given clientId will be added to + * the presence set and presence subscribers will see a corresponding presence message + * with an empty data payload. + * This method is provided to support connections (eg connections from application + * server instances) that act on behalf of multiple clientIds. In order to be able to + * enter the channel with this method, the client library must have been instanced + * either with a key, or with a token bound to the wildcard clientId. + * @param clientId: the id of the client. + */ + public void enterClient(String clientId) throws AblyException { + enterClient(clientId, null); + } + + /** + * Enter a specified client into this channel. The given client will be added to the + * presence set and presence subscribers will see a corresponding presence message. + * This method is provided to support connections (eg connections from application + * server instances) that act on behalf of multiple clientIds. In order to be able to + * enter the channel with this method, the client library must have been instanced + * either with a key, or with a token bound to the wildcard clientId. + * @param clientId: the id of the client. + * @param data: optional data (eg a status message) for this member. + * @throws AblyException + */ + public void enterClient(String clientId, Object data) throws AblyException { + enterClient(clientId, data, null); + } + + /** + * Enter a specified client into this channel. The given client will be added to the + * presence set and presence subscribers will see a corresponding presence message. + * This method is provided to support connections (eg connections from application + * server instances) that act on behalf of multiple clientIds. In order to be able to + * enter the channel with this method, the client library must have been instanced + * either with a key, or with a token bound to the wildcard clientId. + * @param clientId: the id of the client. + * @param data: optional data (eg a status message) for this member. + * @param listener: a listener to be notified on completion of the operation. + * @throws AblyException + */ + public void enterClient(String clientId, Object data, CompletionListener listener) throws AblyException { + if(clientId == null) { + String errorMessage = String.format("Channel %s: unable to enter presence channel (null clientId specified)", channel.name); + Log.v(TAG, errorMessage); + if(listener != null) { + listener.onError(new ErrorInfo(errorMessage, 40000)); + return; + } + } + Log.v(TAG, "enterClient(); channel = " + channel.name + "; clientId = " + clientId); + updatePresence(new PresenceMessage(PresenceMessage.Action.enter, clientId, data), listener); + } + + /** + * Update the presence data for a specified client into this channel. + * If the client is not already a member of the presence set it will be added, + * and presence subscribers will see a corresponding presence message + * with an empty data payload. As for #enterClient above, the connection + * must be authenticated in a way that enables it to represent an arbitrary clientId. + * @param clientId: the id of the client. + * @throws AblyException + */ + public void updateClient(String clientId) throws AblyException { + updateClient(clientId, null); + } + + /** + * Update the presence data for a specified client into this channel. + * If the client is not already a member of the presence set it will be added, and + * presence subscribers will see an enter or update message for this client. + * As for #enterClient above, the connection must be authenticated in a way that + * enables it to represent an arbitrary clientId. + * @param clientId: the id of the client. + * @param data: optional data (eg a status message) for this member. + * @throws AblyException + */ + public void updateClient(String clientId, Object data) throws AblyException { + updateClient(clientId, data, null); + } + + /** + * Update the presence data for a specified client into this channel. + * If the client is not already a member of the presence set it will be added, and + * presence subscribers will see an enter or update message for this client. + * As for #enterClient above, the connection must be authenticated in a way that + * enables it to represent an arbitrary clientId. + * @param clientId: the id of the client. + * @param data: optional data (eg a status message) for this member. + * @param listener: a listener to be notified on completion of the operation. + * @throws AblyException + */ + public void updateClient(String clientId, Object data, CompletionListener listener) throws AblyException { + if(clientId == null) { + String errorMessage = String.format("Channel %s: unable to update presence channel (null clientId specified)", channel.name); + Log.v(TAG, errorMessage); + if(listener != null) { + listener.onError(new ErrorInfo(errorMessage, 40000)); + return; + } + } + Log.v(TAG, "updateClient(); channel = " + channel.name + "; clientId = " + clientId); + updatePresence(new PresenceMessage(PresenceMessage.Action.update, clientId, data), listener); + } + + /** + * Leave a given client from this channel. This client will be removed from the + * presence set and presence subscribers will see a corresponding presence message + * with an empty data payload. + * @param clientId: the id of the client. + * @throws AblyException + */ + public void leaveClient(String clientId) throws AblyException { + leaveClient(clientId, null); + } + + /** + * Leave a given client from this channel. This client will be removed from the + * presence set and presence subscribers will see a leave message for this client. + * @param clientId: the id of the client. + * @param data: optional data (eg a status message) for this member. + * @throws AblyException + */ + public void leaveClient(String clientId, Object data) throws AblyException { + leaveClient(clientId, data, null); + } + + /** + * Leave a given client from this channel. This client will be removed from the + * presence set and presence subscribers will see a leave message for this client. + * @param clientId: the id of the client. + * @param data: optional data (eg a status message) for this member. + * @param listener: a listener to be notified on completion of the operation. + * @throws AblyException + */ + public void leaveClient(String clientId, Object data, CompletionListener listener) throws AblyException { + if(clientId == null) { + String errorMessage = String.format("Channel %s: unable to leave presence channel (null clientId specified)", channel.name); + Log.v(TAG, errorMessage); + if(listener != null) { + listener.onError(new ErrorInfo(errorMessage, 40000)); + return; + } + } + Log.v(TAG, "leaveClient(); channel = " + channel.name + "; clientId = " + clientId); + updatePresence(new PresenceMessage(PresenceMessage.Action.leave, clientId, data), listener); + } + + /** + * Update the presence for this channel with a given PresenceMessage update. + * The connection must be authenticated in a way that enables it to represent + * the clientId in the message. + * @param msg: the presence message + * @param listener: a listener to be notified on completion of the operation. + * @throws AblyException + */ + public void updatePresence(PresenceMessage msg, CompletionListener listener) throws AblyException { + Log.v(TAG, "update(); channel = " + channel.name); + + AblyRealtime ably = channel.ably; + boolean connected = (ably.connection.state == ConnectionState.connected); + String clientId; + try { + clientId = ably.auth.checkClientId(msg, false, connected); + } catch(AblyException e) { + if(listener != null) { + listener.onError(e.errorInfo); + } + return; + } + + msg.encode(null); + synchronized(channel) { + switch(channel.state) { + case initialized: + channel.attach(); + case attaching: + QueuedPresence queued = new QueuedPresence(msg, listener); + pendingPresence.put(clientId, queued); + break; + case attached: + ProtocolMessage message = new ProtocolMessage(ProtocolMessage.Action.presence, channel.name); + message.presence = new PresenceMessage[] { msg }; + ConnectionManager connectionManager = ably.connection.connectionManager; + connectionManager.send(message, ably.options.queueMessages, listener); + break; + default: + throw AblyException.fromErrorInfo(new ErrorInfo("Unable to enter presence channel in detached or failed state", 400, 91001)); + } + } + } + + /************************************ + * history + ************************************/ + + /** + * Obtain recent history for this channel using the REST API. + * The history provided relates to all clients of this application, + * not just this instance. + * @param params: the request params. See the Ably REST API + * documentation for more details. + * @return: an array of Messgaes for this Channel. + * @throws AblyException + */ + public PaginatedResult history(Param[] params) throws AblyException { + return historyImpl(params).sync(); + } + + public void historyAsync(Param[] params, Callback> callback) { + historyImpl(params).async(callback); + } + + private BasePaginatedQuery.ResultRequest historyImpl(Param[] params) { + try { + params = Channel.replacePlaceholderParams(channel, params); + } catch (AblyException e) { + return new BasePaginatedQuery.ResultRequest.Failed(e); + } + + AblyRealtime ably = channel.ably; + HttpCore.BodyHandler bodyHandler = PresenceSerializer.getPresenceResponseHandler(channel.options); + return new BasePaginatedQuery(ably.http, channel.basePath + "/presence/history", HttpUtils.defaultAcceptHeaders(ably.options.useBinaryProtocol), params, bodyHandler).get(); + } + + /** + * internal + * + */ + private static class QueuedPresence { + public PresenceMessage msg; + public CompletionListener listener; + QueuedPresence(PresenceMessage msg, CompletionListener listener) { this.msg = msg; this.listener = listener; } + } + + private final Map pendingPresence = new HashMap(); + + private void sendQueuedMessages() { + Log.v(TAG, "sendQueuedMessages()"); + AblyRealtime ably = channel.ably; + boolean queueMessages = ably.options.queueMessages; + ConnectionManager connectionManager = ably.connection.connectionManager; + int count = pendingPresence.size(); + if(count == 0) + return; + + ProtocolMessage message = new ProtocolMessage(ProtocolMessage.Action.presence, channel.name); + Iterator allQueued = pendingPresence.values().iterator(); + PresenceMessage[] presenceMessages = message.presence = new PresenceMessage[count]; + CompletionListener listener; + + if(count == 1) { + QueuedPresence queued = allQueued.next(); + presenceMessages[0] = queued.msg; + listener = queued.listener; + } else { + int idx = 0; + CompletionListener.Multicaster mListener = new CompletionListener.Multicaster(); + while(allQueued.hasNext()) { + QueuedPresence queued = allQueued.next(); + presenceMessages[idx++] = queued.msg; + if(queued.listener != null) + mListener.add(queued.listener); + } + listener = mListener.isEmpty() ? null : mListener; + } + pendingPresence.clear(); + try { + connectionManager.send(message, queueMessages, listener); + } catch(AblyException e) { + Log.e(TAG, "sendQueuedMessages(): Unexpected exception sending message", e); + if(listener != null) + listener.onError(e.errorInfo); + } + } + + private void failQueuedMessages(ErrorInfo reason) { + Log.v(TAG, "failQueuedMessages()"); + for(QueuedPresence msg : pendingPresence.values()) + if(msg.listener != null) + try { + msg.listener.onError(reason); + } catch(Throwable t) { + Log.e(TAG, "failQueuedMessages(): Unexpected exception calling listener", t); + } + pendingPresence.clear(); + } + + + /************************************ + * attach / detach + ************************************/ + + void setAttached(boolean hasPresence) { + /* Start sync, if hasPresence is not set end sync immediately dropping all the current presence members */ + presence.startSync(); + syncAsResultOfAttach = true; + if (!hasPresence) { + /* + * RTP19a If the PresenceMap has existing members when an ATTACHED message is received without a + * HAS_PRESENCE flag, the client library should emit a LEAVE event for each existing member ... + */ + endSyncAndEmitLeaves(); + } + sendQueuedMessages(); + } + + void setDetached(ErrorInfo reason) { + /* Interrupt get() call if needed */ + synchronized (presence) { + presence.notifyAll(); + } + + /** + * (RTP5a) If the channel enters the DETACHED or FAILED state then all queued presence + * messages will fail immediately, and the PresenceMap and internal PresenceMap is cleared. + * The latter ensures members are not automatically re-entered if the Channel later becomes attached + */ + failQueuedMessages(reason); + presence.clear(); + internalPresence.clear(); + } + + void setSuspended(ErrorInfo reason) { + /* Interrupt get() call if needed */ + synchronized (presence) { + presence.notifyAll(); + } + + /* + * (RTP5f) If the channel enters the SUSPENDED state then all queued presence messages will fail + * immediately, and the PresenceMap is maintained + */ + failQueuedMessages(reason); + } + + /** + * A class encapsulating a map of the members of this presence channel, + * indexed by a String key that is a combination of connectionId and clientId. + * This map synchronises the membership of the presence set by handling + * sync messages from the service. Since sync messages can be out-of-order - + * eg an enter sync event being received after that member has in fact left - + * this map keeps "witness" entries, with absent Action, to remember the + * fact that a leave event has been seen for a member. These entries are + * cleared once the last set of updates of a sync sequence have been received. + * + */ + private class PresenceMap { + + /** + * Wait for sync to be complete. If we are in attaching state wait for initial sync to + * complete as well. Return false if wait was interrupted because channel transitioned to + * state other than attached or attaching + */ + synchronized void waitForSync() throws AblyException, InterruptedException { + boolean syncIsComplete = false; /* temporary variable to avoid potential race conditions */ + while((channel.state == ChannelState.attached || channel.state == ChannelState.attaching) && + /* = (and not ==) is intentional */ + !(syncIsComplete = (!syncInProgress && syncComplete))) + wait(); + + /* invalid channel state */ + int errorCode; + String errorMessage; + + if (channel.state == ChannelState.suspended) { + /* (RTP11d) If the Channel is in the SUSPENDED state then the get function will by default, + * or if waitForSync is set to true, result in an error with code 91005 and a message stating + * that the presence state is out of sync due to the channel being in a SUSPENDED state */ + errorCode = 91005; + errorMessage = String.format("Channel %s: presence state is out of sync due to the channel being in a SUSPENDED state", channel.name); + } else if(syncIsComplete) { + return; + } else { + errorCode = 90001; + errorMessage = String.format("Channel %s: cannot get presence state because channel is in invalid state", channel.name); + } + Log.v(TAG, errorMessage); + throw AblyException.fromErrorInfo(new ErrorInfo(errorMessage, errorCode)); + } + + synchronized Collection get(Param[] params) throws AblyException, InterruptedException { + boolean waitForSync = true; + String clientId = null; + String connectionId = null; + + for (Param param: params) { + switch (param.key) { + case GET_WAITFORSYNC: + waitForSync = Boolean.valueOf(param.value); + break; + case GET_CLIENTID: + clientId = param.value; + break; + case GET_CONNECTIONID: + connectionId = param.value; + break; + } + } + + HashSet result = new HashSet<>(); + if (waitForSync) + waitForSync(); + + for (Map.Entry entry: members.entrySet()) { + PresenceMessage member = entry.getValue(); + if ((clientId == null || member.clientId.equals(clientId)) && + (connectionId == null || member.connectionId.equals(connectionId))) + result.add(member); + } + + return result; + } + + /** + * Add or update the presence state for a member + * @param item + * @return true if the given message represents a change; + * false if the message is already superseded + */ + synchronized boolean put(PresenceMessage item) { + String key = item.memberKey(); + /* we've seen this member, so do not remove it at the end of sync */ + if(residualMembers != null) + residualMembers.remove(key); + + /* check if there is a newer existing member (or absent witness) */ + if (hasNewerItem(key, item)) + return false; + + members.put(key, item); + return true; + } + + /** + * Determine if there is a newer item already in the map + * @param key key used to search the item in the map + * @param item new presence message to be added + * @return true if there is a newer item + */ + synchronized boolean hasNewerItem(String key, PresenceMessage item) { + PresenceMessage existingItem = members.get(key); + if(existingItem == null) + return false; + + /* + * (RTP2b1) If either presence message has a connectionId which is not an initial substring + * of its id, compare them by timestamp numerically. (This will be the case when one of them + * is a 'synthesized leave' event sent by realtime to indicate a connection disconnected + * unexpectedly 15s ago. Such messages will have an id that does not correspond to its + * connectionId, as it wasn't actually published by that connection + */ + if(item.connectionId != null && existingItem.connectionId != null && + (!item.id.startsWith(item.connectionId) || !existingItem.id.startsWith(existingItem.connectionId))) + return existingItem.timestamp >= item.timestamp; + + /* + * (RTP2b2) Else split the id of both presence messages (which will be of the form + * connid:msgSerial:index, e.g. aaaaaa:0:0) on the separator :, and parse the latter two as + * integers. Compare them first by msgSerial numerically, then (if @msgSerial@s are equal) by + * index numerically, larger being newer in both cases + */ + String[] itemComponents = item.id.split(":", 3); + String[] existingItemComponents = existingItem.id.split(":", 3); + + if(itemComponents.length < 3 || existingItemComponents.length < 3) + return false; + + try { + long messageSerial = Long.valueOf(itemComponents[1]); + long messageIndex = Long.valueOf(itemComponents[2]); + long existingMessageSerial = Long.valueOf(existingItemComponents[1]); + long existingMessageIndex = Long.valueOf(existingItemComponents[2]); + + return existingMessageSerial > messageSerial || + (existingMessageSerial == messageSerial && existingMessageIndex >= messageIndex); + } + catch(NumberFormatException e) { + return false; + } + } + + /** + * Get all members based on the current state (even if sync is in progress) + * @return + */ + synchronized Collection values() { + try { return values(false); } catch (InterruptedException|AblyException e) { return null; } + } + + /** + * Get all members, optionally waiting if a sync is in progress. + * @param wait + * @return + * @throws InterruptedException + */ + synchronized Collection values(boolean wait) throws AblyException, InterruptedException { + Set result = new HashSet(); + if(wait) + waitForSync(); + result.addAll(members.values()); + for(Iterator it = result.iterator(); it.hasNext();) { + PresenceMessage entry = it.next(); + if(entry.action == PresenceMessage.Action.absent) { + it.remove(); + } + } + return result; + } + + /** + * Remove a member. + * @param item + * @return + */ + synchronized boolean remove(PresenceMessage item) { + String key = item.memberKey(); + if (hasNewerItem(key, item)) + return false; + PresenceMessage existingItem = members.remove(key); + if(existingItem != null && existingItem.action == PresenceMessage.Action.absent) + return false; + return true; + } + + /** + * Start a sync sequence. + * Note that this is called each time a sync message is received that is not + * the last. + */ + synchronized void startSync() { + Log.v(TAG, "startSync(); channel = " + channel.name + "; syncInProgress = " + syncInProgress); + /* we might be called multiple times while a sync is in progress */ + if(!syncInProgress) { + residualMembers = new HashSet(members.keySet()); + syncInProgress = true; + } + } + + /** + * Finish a sync sequence. Returns "residual" items that were removed as a part of a sync + */ + synchronized List endSync() { + Log.v(TAG, "endSync(); channel = " + channel.name + "; syncInProgress = " + syncInProgress); + ArrayList removedEntries = new ArrayList<>(); + if(syncInProgress) { + /* we can now strip out the absent members, as we have + * received all of the out-of-order sync messages */ + for(Iterator> it = members.entrySet().iterator(); it.hasNext();) { + Map.Entry entry = it.next(); + if(entry.getValue().action == PresenceMessage.Action.absent) { + it.remove(); + } + } + /* any members that were present at the start of the sync, + * and have not been seen in sync, can be removed */ + for(String itemKey: residualMembers) { + /* clone presence message as it still can be in the internal presence map */ + removedEntries.add((PresenceMessage)members.get(itemKey).clone()); + members.remove(itemKey); + } + residualMembers = null; + + /* finish, notifying any waiters */ + syncInProgress = false; + } + syncComplete = true; + notifyAll(); + return removedEntries; + } + + /** + * Clear all entries + */ + synchronized void clear() { + members.clear(); + if(residualMembers != null) + residualMembers.clear(); + } + + private boolean syncInProgress; + private Collection residualMembers; + private final HashMap members = new HashMap(); + } + + private final PresenceMap presence = new PresenceMap(); + private final PresenceMap internalPresence = new PresenceMap(); + + /************************************ + * general + ************************************/ + + Presence(Channel channel) { + this.channel = channel; + } + + private static final String TAG = Channel.class.getName(); + + private final Channel channel; + + /* channel serial if sync is in progress */ + private String currentSyncChannelSerial; + /* Sync in progress is a result of attach operation */ + private boolean syncAsResultOfAttach; + + /** + * (RTP13) Presence#syncComplete returns true if the initial SYNC operation has completed for + * the members present on the channel + */ + public boolean syncComplete; } 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 142f6f0ba..8341d6079 100644 --- a/lib/src/main/java/io/ably/lib/rest/AblyBase.java +++ b/lib/src/main/java/io/ably/lib/rest/AblyBase.java @@ -29,273 +29,273 @@ */ public abstract class AblyBase { - public final ClientOptions options; - public final Http http; - public final HttpCore httpCore; - - public final Auth auth; - public final Channels channels; - public final Platform platform; - public final Push push; - - /** - * Instance the Ably library using a key only. - * This is simply a convenience constructor for the - * simplest case of instancing the library with a key - * for basic authentication and no other options. - * @param key; String key (obtained from application dashboard) - * @throws AblyException - */ - public AblyBase(String key) throws AblyException { - this(new ClientOptions(key)); - } - - /** - * Instance the Ably library with the given options. - * @param options: see {@link io.ably.lib.types.ClientOptions} for options - * @throws AblyException - */ - public AblyBase(ClientOptions options) throws AblyException { - /* normalise options */ - if(options == null) { - String msg = "no options provided"; - Log.e(getClass().getName(), msg); - throw AblyException.fromErrorInfo(new ErrorInfo(msg, 400, 40000)); - } - this.options = options; - - /* process options */ - Log.setLevel(options.logLevel); - Log.setHandler(options.logHandler); - Log.i(getClass().getName(), "started"); - - auth = new Auth(this, options); - httpCore = new HttpCore(options, auth); - http = new Http(new AsyncHttpScheduler(httpCore, options), new SyncHttpScheduler(httpCore)); - - channels = new InternalChannels(); - - platform = new Platform(); - push = new Push(this); - } - - /** - * A collection of Channels associated with an Ably instance. - */ - public interface Channels extends ReadOnlyMap { - Channel get(String channelName); - Channel get(String channelName, ChannelOptions channelOptions) throws AblyException; - void release(String channelName); - int size(); - Iterable values(); - } - - private class InternalChannels extends InternalMap implements Channels { - InternalChannels() { - super(new HashMap()); - } - - @Override - public Channel get(String channelName) { - try { - return get(channelName, null); - } catch (AblyException e) { return null; } - } - - @Override - public Channel get(String channelName, ChannelOptions channelOptions) throws AblyException { - Channel channel = map.get(channelName); - if (channel != null) { - if (channelOptions != null) - channel.options = channelOptions; - return channel; - } - - channel = new Channel(AblyBase.this, channelName, channelOptions); - map.put(channelName, channel); - return channel; - } - - @Override - public void release(String channelName) { - map.remove(channelName); - } - } - - /** - * Obtain the time from the Ably service. - * This may be required on clients that do not have access - * to a sufficiently well maintained time source, to provide - * timestamps for use in token requests - * @return time in millis since the epoch - * @throws AblyException - */ - public long time() throws AblyException { - return timeImpl().sync().longValue(); - } - - /** - * Asynchronously obtain the time from the Ably service. - * This may be required on clients that do not have access - * to a sufficiently well maintained time source, to provide - * timestamps for use in token requests - * @param callback - */ - public void timeAsync(Callback callback) { - timeImpl().async(callback); - } - - private Http.Request timeImpl() { - return http.request(new Http.Execute() { - @Override - public void execute(HttpScheduler http, Callback callback) throws AblyException { - http.get("/time", HttpUtils.defaultAcceptHeaders(false), null, new HttpCore.ResponseHandler() { - @Override - public Long handleResponse(HttpCore.Response response, ErrorInfo error) throws AblyException { - if(error != null) { - throw AblyException.fromErrorInfo(error); - } - return Serialisation.gson.fromJson(new String(response.body), Long[].class)[0]; - } - }, false, callback); - } - }); - } - - /** - * Request usage statistics for this application. Returned stats - * are application-wide and not just relating to this instance. - * @param params query options: see Ably REST API documentation - * for available options - * @return a PaginatedResult of Stats records for the requested params - * @throws AblyException - */ - public PaginatedResult stats(Param[] params) throws AblyException { - return new PaginatedQuery(http, "/stats", HttpUtils.defaultAcceptHeaders(false), params, StatsReader.statsResponseHandler).get(); - } - - /** - * Asynchronously obtain usage statistics for this application using the REST API. - * @param params: the request params. See the Ably REST API - * @param callback - * @return - */ - public void statsAsync(Param[] params, Callback> callback) { - (new AsyncPaginatedQuery(http, "/stats", HttpUtils.defaultAcceptHeaders(false), params, StatsReader.statsResponseHandler)).get(callback); - } - - /** - * Make a generic HTTP request against an endpoint representing a collection - * of some type; this is to provide a forward compatibility path for new APIs. - * @param method: the HTTP method to use (see constants in io.ably.lib.httpCore.HttpCore) - * @param path: the path component of the resource URI - * @param params (optional; may be null): any parameters to send with the request; see API-specific documentation - * @param body (optional; may be null): an instance of RequestBody; either a JSONRequestBody or ByteArrayRequestBody - * @param headers (optional; may be null): any additional headers to send; see API-specific documentation - * @return a page of results, each represented as a JsonElement - * @throws AblyException if it was not possible to complete the request, or an error response was received - */ - public HttpPaginatedResponse request(String method, String path, Param[] params, HttpCore.RequestBody body, Param[] headers) throws AblyException { - headers = HttpUtils.mergeHeaders(HttpUtils.defaultAcceptHeaders(false), headers); - return new HttpPaginatedQuery(http, method, path, headers, params, body).exec(); - } - - /** - * Make an async generic HTTP request against an endpoint representing a collection - * of some type; this is to provide a forward compatibility path for new APIs. - * @param method: the HTTP method to use (see constants in io.ably.lib.httpCore.HttpCore) - * @param path: the path component of the resource URI - * @param params (optional; may be null): any parameters to send with the request; see API-specific documentation - * @param body (optional; may be null): an instance of RequestBody; either a JSONRequestBody or ByteArrayRequestBody - * @param headers (optional; may be null): any additional headers to send; see API-specific documentation - * @param callback: called with the asynchronous result - */ - public void requestAsync(String method, String path, Param[] params, HttpCore.RequestBody body, Param[] headers, final AsyncHttpPaginatedResponse.Callback callback) { - headers = HttpUtils.mergeHeaders(HttpUtils.defaultAcceptHeaders(false), headers); - (new AsyncHttpPaginatedQuery(http, method, path, headers, params, body)).exec(callback); - } - - /** - * Publish a messages on one or more channels. When there are - * messages to be sent on multiple channels simultaneously, - * it is more efficient to use this method to publish them in - * a single request, as compared with publishing via multiple - * independent requests. - * @throws AblyException - */ - @Experimental - public PublishResponse[] publishBatch(Message.Batch[] pubSpecs, ChannelOptions channelOptions) throws AblyException { - return publishBatchImpl(pubSpecs, channelOptions).sync(); - } - - @Experimental - public void publishBatchAsync(Message.Batch[] pubSpecs, ChannelOptions channelOptions, final Callback callback) throws AblyException { - publishBatchImpl(pubSpecs, channelOptions).async(callback); - } - - private Http.Request publishBatchImpl(final Message.Batch[] pubSpecs, ChannelOptions channelOptions) throws AblyException { - boolean hasClientSuppliedId = false; - for(Message.Batch spec : pubSpecs) { - for(Message message : spec.messages) { - /* handle message ids */ - /* RSL1k2 */ - hasClientSuppliedId |= (message.id != null); - /* RTL6g3 */ - auth.checkClientId(message, true, false); - message.encode(channelOptions); - } - if(!hasClientSuppliedId && options.idempotentRestPublishing) { - /* RSL1k1: populate the message id with a library-generated id */ - String messageId = Crypto.getRandomMessageId(); - for (int i = 0; i < spec.messages.length; i++) { - spec.messages[i].id = messageId + ':' + i; - } - } - } - return http.request(new Http.Execute() { - @Override - public void execute(HttpScheduler http, final Callback callback) throws AblyException { - HttpCore.RequestBody requestBody = options.useBinaryProtocol ? MessageSerializer.asMsgpackRequest(pubSpecs) : MessageSerializer.asJSONRequest(pubSpecs); - http.post("/messages", HttpUtils.defaultAcceptHeaders(options.useBinaryProtocol), null, requestBody, new HttpCore.ResponseHandler() { - @Override - public PublishResponse[] handleResponse(HttpCore.Response response, ErrorInfo error) throws AblyException { - if(error != null && error.code != 40020) { - throw AblyException.fromErrorInfo(error); - } - return PublishResponse.getBulkPublishResponseHandler(response.statusCode).handleResponseBody(response.contentType, response.body); - } - }, true, callback); - } - }); - } - - /** - * Authentication token has changed. waitForResult is true if there is a need to - * wait for server response to auth request - */ - - /** - * Override this method in AblyRealtime and pass updated token to ConnectionManager - * @param token new token - * @param waitForResponse wait for server response before returning from method - * @throws AblyException - */ - protected void onAuthUpdated(String token, boolean waitForResponse) throws AblyException { - /* Default is to do nothing. Overridden by subclass. */ - } - - /** - * Authentication error occurred - */ - protected void onAuthError(ErrorInfo errorInfo) { - /* Default is to do nothing. Overridden by subclass. */ - } - - /** - * clientId set by late initialisation - */ - protected void onClientIdSet(String clientId) { - /* Default is to do nothing. Overridden by subclass. */ - } + public final ClientOptions options; + public final Http http; + public final HttpCore httpCore; + + public final Auth auth; + public final Channels channels; + public final Platform platform; + public final Push push; + + /** + * Instance the Ably library using a key only. + * This is simply a convenience constructor for the + * simplest case of instancing the library with a key + * for basic authentication and no other options. + * @param key; String key (obtained from application dashboard) + * @throws AblyException + */ + public AblyBase(String key) throws AblyException { + this(new ClientOptions(key)); + } + + /** + * Instance the Ably library with the given options. + * @param options: see {@link io.ably.lib.types.ClientOptions} for options + * @throws AblyException + */ + public AblyBase(ClientOptions options) throws AblyException { + /* normalise options */ + if(options == null) { + String msg = "no options provided"; + Log.e(getClass().getName(), msg); + throw AblyException.fromErrorInfo(new ErrorInfo(msg, 400, 40000)); + } + this.options = options; + + /* process options */ + Log.setLevel(options.logLevel); + Log.setHandler(options.logHandler); + Log.i(getClass().getName(), "started"); + + auth = new Auth(this, options); + httpCore = new HttpCore(options, auth); + http = new Http(new AsyncHttpScheduler(httpCore, options), new SyncHttpScheduler(httpCore)); + + channels = new InternalChannels(); + + platform = new Platform(); + push = new Push(this); + } + + /** + * A collection of Channels associated with an Ably instance. + */ + public interface Channels extends ReadOnlyMap { + Channel get(String channelName); + Channel get(String channelName, ChannelOptions channelOptions) throws AblyException; + void release(String channelName); + int size(); + Iterable values(); + } + + private class InternalChannels extends InternalMap implements Channels { + InternalChannels() { + super(new HashMap()); + } + + @Override + public Channel get(String channelName) { + try { + return get(channelName, null); + } catch (AblyException e) { return null; } + } + + @Override + public Channel get(String channelName, ChannelOptions channelOptions) throws AblyException { + Channel channel = map.get(channelName); + if (channel != null) { + if (channelOptions != null) + channel.options = channelOptions; + return channel; + } + + channel = new Channel(AblyBase.this, channelName, channelOptions); + map.put(channelName, channel); + return channel; + } + + @Override + public void release(String channelName) { + map.remove(channelName); + } + } + + /** + * Obtain the time from the Ably service. + * This may be required on clients that do not have access + * to a sufficiently well maintained time source, to provide + * timestamps for use in token requests + * @return time in millis since the epoch + * @throws AblyException + */ + public long time() throws AblyException { + return timeImpl().sync().longValue(); + } + + /** + * Asynchronously obtain the time from the Ably service. + * This may be required on clients that do not have access + * to a sufficiently well maintained time source, to provide + * timestamps for use in token requests + * @param callback + */ + public void timeAsync(Callback callback) { + timeImpl().async(callback); + } + + private Http.Request timeImpl() { + return http.request(new Http.Execute() { + @Override + public void execute(HttpScheduler http, Callback callback) throws AblyException { + http.get("/time", HttpUtils.defaultAcceptHeaders(false), null, new HttpCore.ResponseHandler() { + @Override + public Long handleResponse(HttpCore.Response response, ErrorInfo error) throws AblyException { + if(error != null) { + throw AblyException.fromErrorInfo(error); + } + return Serialisation.gson.fromJson(new String(response.body), Long[].class)[0]; + } + }, false, callback); + } + }); + } + + /** + * Request usage statistics for this application. Returned stats + * are application-wide and not just relating to this instance. + * @param params query options: see Ably REST API documentation + * for available options + * @return a PaginatedResult of Stats records for the requested params + * @throws AblyException + */ + public PaginatedResult stats(Param[] params) throws AblyException { + return new PaginatedQuery(http, "/stats", HttpUtils.defaultAcceptHeaders(false), params, StatsReader.statsResponseHandler).get(); + } + + /** + * Asynchronously obtain usage statistics for this application using the REST API. + * @param params: the request params. See the Ably REST API + * @param callback + * @return + */ + public void statsAsync(Param[] params, Callback> callback) { + (new AsyncPaginatedQuery(http, "/stats", HttpUtils.defaultAcceptHeaders(false), params, StatsReader.statsResponseHandler)).get(callback); + } + + /** + * Make a generic HTTP request against an endpoint representing a collection + * of some type; this is to provide a forward compatibility path for new APIs. + * @param method: the HTTP method to use (see constants in io.ably.lib.httpCore.HttpCore) + * @param path: the path component of the resource URI + * @param params (optional; may be null): any parameters to send with the request; see API-specific documentation + * @param body (optional; may be null): an instance of RequestBody; either a JSONRequestBody or ByteArrayRequestBody + * @param headers (optional; may be null): any additional headers to send; see API-specific documentation + * @return a page of results, each represented as a JsonElement + * @throws AblyException if it was not possible to complete the request, or an error response was received + */ + public HttpPaginatedResponse request(String method, String path, Param[] params, HttpCore.RequestBody body, Param[] headers) throws AblyException { + headers = HttpUtils.mergeHeaders(HttpUtils.defaultAcceptHeaders(false), headers); + return new HttpPaginatedQuery(http, method, path, headers, params, body).exec(); + } + + /** + * Make an async generic HTTP request against an endpoint representing a collection + * of some type; this is to provide a forward compatibility path for new APIs. + * @param method: the HTTP method to use (see constants in io.ably.lib.httpCore.HttpCore) + * @param path: the path component of the resource URI + * @param params (optional; may be null): any parameters to send with the request; see API-specific documentation + * @param body (optional; may be null): an instance of RequestBody; either a JSONRequestBody or ByteArrayRequestBody + * @param headers (optional; may be null): any additional headers to send; see API-specific documentation + * @param callback: called with the asynchronous result + */ + public void requestAsync(String method, String path, Param[] params, HttpCore.RequestBody body, Param[] headers, final AsyncHttpPaginatedResponse.Callback callback) { + headers = HttpUtils.mergeHeaders(HttpUtils.defaultAcceptHeaders(false), headers); + (new AsyncHttpPaginatedQuery(http, method, path, headers, params, body)).exec(callback); + } + + /** + * Publish a messages on one or more channels. When there are + * messages to be sent on multiple channels simultaneously, + * it is more efficient to use this method to publish them in + * a single request, as compared with publishing via multiple + * independent requests. + * @throws AblyException + */ + @Experimental + public PublishResponse[] publishBatch(Message.Batch[] pubSpecs, ChannelOptions channelOptions) throws AblyException { + return publishBatchImpl(pubSpecs, channelOptions).sync(); + } + + @Experimental + public void publishBatchAsync(Message.Batch[] pubSpecs, ChannelOptions channelOptions, final Callback callback) throws AblyException { + publishBatchImpl(pubSpecs, channelOptions).async(callback); + } + + private Http.Request publishBatchImpl(final Message.Batch[] pubSpecs, ChannelOptions channelOptions) throws AblyException { + boolean hasClientSuppliedId = false; + for(Message.Batch spec : pubSpecs) { + for(Message message : spec.messages) { + /* handle message ids */ + /* RSL1k2 */ + hasClientSuppliedId |= (message.id != null); + /* RTL6g3 */ + auth.checkClientId(message, true, false); + message.encode(channelOptions); + } + if(!hasClientSuppliedId && options.idempotentRestPublishing) { + /* RSL1k1: populate the message id with a library-generated id */ + String messageId = Crypto.getRandomMessageId(); + for (int i = 0; i < spec.messages.length; i++) { + spec.messages[i].id = messageId + ':' + i; + } + } + } + return http.request(new Http.Execute() { + @Override + public void execute(HttpScheduler http, final Callback callback) throws AblyException { + HttpCore.RequestBody requestBody = options.useBinaryProtocol ? MessageSerializer.asMsgpackRequest(pubSpecs) : MessageSerializer.asJSONRequest(pubSpecs); + http.post("/messages", HttpUtils.defaultAcceptHeaders(options.useBinaryProtocol), null, requestBody, new HttpCore.ResponseHandler() { + @Override + public PublishResponse[] handleResponse(HttpCore.Response response, ErrorInfo error) throws AblyException { + if(error != null && error.code != 40020) { + throw AblyException.fromErrorInfo(error); + } + return PublishResponse.getBulkPublishResponseHandler(response.statusCode).handleResponseBody(response.contentType, response.body); + } + }, true, callback); + } + }); + } + + /** + * Authentication token has changed. waitForResult is true if there is a need to + * wait for server response to auth request + */ + + /** + * Override this method in AblyRealtime and pass updated token to ConnectionManager + * @param token new token + * @param waitForResponse wait for server response before returning from method + * @throws AblyException + */ + protected void onAuthUpdated(String token, boolean waitForResponse) throws AblyException { + /* Default is to do nothing. Overridden by subclass. */ + } + + /** + * Authentication error occurred + */ + protected void onAuthError(ErrorInfo errorInfo) { + /* Default is to do nothing. Overridden by subclass. */ + } + + /** + * clientId set by late initialisation + */ + protected void onClientIdSet(String clientId) { + /* Default is to do nothing. Overridden by subclass. */ + } } diff --git a/lib/src/main/java/io/ably/lib/rest/Auth.java b/lib/src/main/java/io/ably/lib/rest/Auth.java index 040f923d3..5a11b13fa 100644 --- a/lib/src/main/java/io/ably/lib/rest/Auth.java +++ b/lib/src/main/java/io/ably/lib/rest/Auth.java @@ -38,1082 +38,1082 @@ */ public class Auth { - /** - * Authentication methods - */ - public enum AuthMethod { - basic, - token; - } - - /** - * Authentication options when instancing the Ably library - */ - public static class AuthOptions { - - /** - * A callback to call to obtain a signed TokenRequest, - * TokenDetails or a token string. This enables a client - * to obtain token requests or tokens from another entity, - * so tokens can be renewed without the client requiring a - * key - */ - public TokenCallback authCallback; - - /** - * A URL to query to obtain a signed TokenRequest, - * TokenDetails or a token string. This enables a client - * to obtain token request or token from another entity, - * so tokens can be renewed without the client requiring - * a key - */ - public String authUrl; - - /** - * TO3j7: authMethod: The HTTP verb to be used when a request - * is made by the library to the authUrl. Defaults to GET, - * supports GET and POST - */ - public String authMethod; - - /** - * Full Ably key string as obtained from dashboard. - */ - public String key; - - /** - * An authentication token issued for this application - * against a specific key and {@link TokenParams} - */ - public String token; - - /** - * An authentication token issued for this application - * against a specific key and {@link TokenParams} - */ - public TokenDetails tokenDetails; - - /** - * Headers to be included in any request made by the library - * to the authURL. - */ - public Param[] authHeaders; - - /** - * Query params to be included in any request made by the library - * to the authURL. - */ - public Param[] authParams; - - /** - * This may be set in instances that the library is to sign - * token requests based on a given key. If true, the library - * will query the Ably system for the current time instead of - * relying on a locally-available time of day. - */ - public boolean queryTime; - - /** - * TO3j4: Use token authorization even if no clientId - */ - public boolean useTokenAuth; - - /** - * Default constructor - */ - public AuthOptions() {} - - /** - * Convenience constructor, to create an AuthOptions based - * on the key string obtained from the application dashboard. - * @param key: the full key string as obtained from the dashboard - * @throws AblyException - */ - public AuthOptions(String key) throws AblyException { - if (key == null) { - throw AblyException.fromErrorInfo(new ErrorInfo("key string cannot be null", 40000, 400)); - } - if(key.indexOf(':') > -1) - this.key = key; - else - this.token = key; - } - - /** - * Stores the AuthOptions arguments as defaults for subsequent authorizations - * with the exception of the attributes {@link AuthOptions#timestamp()} and - * {@link AuthOptions#queryTime} - *

- * Spec: RSA10g - *

- */ - private AuthOptions storedValues() { - AuthOptions result = new AuthOptions(); - result.key = this.key; - result.authUrl = this.authUrl; - result.authMethod = this.authMethod; - result.authParams = this.authParams; - result.authHeaders = this.authHeaders; - result.token = this.token; - result.tokenDetails = this.tokenDetails; - result.authCallback = this.authCallback; - return result; - } - - /** - * Create a new copy of object - * - * @return copied object - */ - private AuthOptions copy() { - AuthOptions result = new AuthOptions(); - result.key = this.key; - result.authUrl = this.authUrl; - result.authMethod = this.authMethod; - result.authParams = this.authParams; - result.authHeaders = this.authHeaders; - result.token = this.token; - result.tokenDetails = this.tokenDetails; - result.authCallback = this.authCallback; - result.queryTime = this.queryTime; - return result; - } - } - - /** - * A class providing details of a token and its associated metadata, - * provided when the system successfully requests a token from the system. - * - */ - public static class TokenDetails { - - /** - * The token itself - */ - public String token; - - /** - * The time (in millis since the epoch) at which this token expires. - */ - public long expires; - - /** - * The time (in millis since the epoch) at which this token was issued. - */ - public long issued; - - /** - * The capability associated with this token. See the Ably Authentication - * documentation for details. - */ - public String capability; - - /** - * The clientId, if any, bound to this token. If a clientId is included, - * then the token authenticates its bearer as that clientId, and the - * token may only be used to perform operations on behalf of that clientId. - */ - public String clientId; - - public TokenDetails() {} - public TokenDetails(String token) { this.token = token; } - - /** - * Convert a JSON response body to a TokenDetails. - * Deprecated: use fromJson() instead - * @param json - * @return - */ - @Deprecated - public static TokenDetails fromJSON(JsonObject json) { - return Serialisation.gson.fromJson(json, TokenDetails.class); - } - - /** - * Convert a JSON element response body to a TokenDetails. - * Spec: TD7 - * @param json - * @return - */ - public static TokenDetails fromJson(String json) { - return Serialisation.gson.fromJson(json, TokenDetails.class); - } - - /** - * Convert a JSON element response body to a TokenDetails. - * @param json - * @return - */ - public static TokenDetails fromJsonElement(JsonObject json) { - return Serialisation.gson.fromJson(json, TokenDetails.class); - } - - /** - * Convert a TokenDetails into a JSON object. - */ - public JsonObject asJsonElement() { - return (JsonObject)Serialisation.gson.toJsonTree(this); - } - - /** - * Convert a TokenDetails into a JSON string. - */ - public String asJson() { - return asJsonElement().toString(); - } - - /** - * Check equality of a TokenDetails - * @param obj - */ - @Override - public boolean equals(Object obj) { - TokenDetails details = (TokenDetails)obj; - return equalNullableStrings(this.token, details.token) & - equalNullableStrings(this.capability, details.capability) & - equalNullableStrings(this.clientId, details.clientId) & - (this.issued == details.issued) & - (this.expires == details.expires); - } + /** + * Authentication methods + */ + public enum AuthMethod { + basic, + token; + } + + /** + * Authentication options when instancing the Ably library + */ + public static class AuthOptions { + + /** + * A callback to call to obtain a signed TokenRequest, + * TokenDetails or a token string. This enables a client + * to obtain token requests or tokens from another entity, + * so tokens can be renewed without the client requiring a + * key + */ + public TokenCallback authCallback; + + /** + * A URL to query to obtain a signed TokenRequest, + * TokenDetails or a token string. This enables a client + * to obtain token request or token from another entity, + * so tokens can be renewed without the client requiring + * a key + */ + public String authUrl; + + /** + * TO3j7: authMethod: The HTTP verb to be used when a request + * is made by the library to the authUrl. Defaults to GET, + * supports GET and POST + */ + public String authMethod; + + /** + * Full Ably key string as obtained from dashboard. + */ + public String key; + + /** + * An authentication token issued for this application + * against a specific key and {@link TokenParams} + */ + public String token; + + /** + * An authentication token issued for this application + * against a specific key and {@link TokenParams} + */ + public TokenDetails tokenDetails; + + /** + * Headers to be included in any request made by the library + * to the authURL. + */ + public Param[] authHeaders; + + /** + * Query params to be included in any request made by the library + * to the authURL. + */ + public Param[] authParams; + + /** + * This may be set in instances that the library is to sign + * token requests based on a given key. If true, the library + * will query the Ably system for the current time instead of + * relying on a locally-available time of day. + */ + public boolean queryTime; + + /** + * TO3j4: Use token authorization even if no clientId + */ + public boolean useTokenAuth; + + /** + * Default constructor + */ + public AuthOptions() {} + + /** + * Convenience constructor, to create an AuthOptions based + * on the key string obtained from the application dashboard. + * @param key: the full key string as obtained from the dashboard + * @throws AblyException + */ + public AuthOptions(String key) throws AblyException { + if (key == null) { + throw AblyException.fromErrorInfo(new ErrorInfo("key string cannot be null", 40000, 400)); + } + if(key.indexOf(':') > -1) + this.key = key; + else + this.token = key; + } + + /** + * Stores the AuthOptions arguments as defaults for subsequent authorizations + * with the exception of the attributes {@link AuthOptions#timestamp()} and + * {@link AuthOptions#queryTime} + *

+ * Spec: RSA10g + *

+ */ + private AuthOptions storedValues() { + AuthOptions result = new AuthOptions(); + result.key = this.key; + result.authUrl = this.authUrl; + result.authMethod = this.authMethod; + result.authParams = this.authParams; + result.authHeaders = this.authHeaders; + result.token = this.token; + result.tokenDetails = this.tokenDetails; + result.authCallback = this.authCallback; + return result; + } + + /** + * Create a new copy of object + * + * @return copied object + */ + private AuthOptions copy() { + AuthOptions result = new AuthOptions(); + result.key = this.key; + result.authUrl = this.authUrl; + result.authMethod = this.authMethod; + result.authParams = this.authParams; + result.authHeaders = this.authHeaders; + result.token = this.token; + result.tokenDetails = this.tokenDetails; + result.authCallback = this.authCallback; + result.queryTime = this.queryTime; + return result; + } + } + + /** + * A class providing details of a token and its associated metadata, + * provided when the system successfully requests a token from the system. + * + */ + public static class TokenDetails { + + /** + * The token itself + */ + public String token; + + /** + * The time (in millis since the epoch) at which this token expires. + */ + public long expires; + + /** + * The time (in millis since the epoch) at which this token was issued. + */ + public long issued; + + /** + * The capability associated with this token. See the Ably Authentication + * documentation for details. + */ + public String capability; + + /** + * The clientId, if any, bound to this token. If a clientId is included, + * then the token authenticates its bearer as that clientId, and the + * token may only be used to perform operations on behalf of that clientId. + */ + public String clientId; + + public TokenDetails() {} + public TokenDetails(String token) { this.token = token; } + + /** + * Convert a JSON response body to a TokenDetails. + * Deprecated: use fromJson() instead + * @param json + * @return + */ + @Deprecated + public static TokenDetails fromJSON(JsonObject json) { + return Serialisation.gson.fromJson(json, TokenDetails.class); + } + + /** + * Convert a JSON element response body to a TokenDetails. + * Spec: TD7 + * @param json + * @return + */ + public static TokenDetails fromJson(String json) { + return Serialisation.gson.fromJson(json, TokenDetails.class); + } + + /** + * Convert a JSON element response body to a TokenDetails. + * @param json + * @return + */ + public static TokenDetails fromJsonElement(JsonObject json) { + return Serialisation.gson.fromJson(json, TokenDetails.class); + } + + /** + * Convert a TokenDetails into a JSON object. + */ + public JsonObject asJsonElement() { + return (JsonObject)Serialisation.gson.toJsonTree(this); + } + + /** + * Convert a TokenDetails into a JSON string. + */ + public String asJson() { + return asJsonElement().toString(); + } + + /** + * Check equality of a TokenDetails + * @param obj + */ + @Override + public boolean equals(Object obj) { + TokenDetails details = (TokenDetails)obj; + return equalNullableStrings(this.token, details.token) & + equalNullableStrings(this.capability, details.capability) & + equalNullableStrings(this.clientId, details.clientId) & + (this.issued == details.issued) & + (this.expires == details.expires); + } } - /** - * A class providing parameters of a token request. - */ - public static class TokenParams { - - /** - * Requested time to live for the token. If the token request - * is successful, the TTL of the returned token will be less - * than or equal to this value depending on application settings - * and the attributes of the issuing key. - * - * 0 means Ably will set it to the default value. - */ - public long ttl; - - /** - * Capability of the token. If the token request is successful, - * the capability of the returned token will be the intersection of - * this capability with the capability of the issuing key. - */ - public String capability; - - /** - * A clientId to associate with this token. The generated token - * may be used to authenticate as this clientId. - */ - public String clientId; - - /** - * The timestamp (in millis since the epoch) of this request. - * Timestamps, in conjunction with the nonce, are used to prevent - * token requests from being replayed. - */ - public long timestamp; - - /** - * Internal; convert a TokenParams to a collection of Params - * @return - */ - public Map asMap() { - Map params = new HashMap(); - if(ttl > 0) params.put("ttl", new Param("ttl", String.valueOf(ttl))); - if(capability != null) params.put("capability", new Param("capability", capability)); - if(clientId != null) params.put("clientId", new Param("clientId", clientId)); - if(timestamp > 0) params.put("timestamp", new Param("timestamp", String.valueOf(timestamp))); - return params; - } - - /** - * Check equality of a TokenParams - * @param obj - */ - @Override - public boolean equals(Object obj) { - TokenParams params = (TokenParams)obj; - return (this.ttl == params.ttl) & - equalNullableStrings(this.capability, params.capability) & - equalNullableStrings(this.clientId, params.clientId) & - (this.timestamp == params.timestamp); - } - - /** - * Stores the TokenParams arguments as defaults for subsequent authorizations - * with the exception of the attributes {@link TokenParams#timestamp} - *

- * Spec: RSA10g - *

- */ - private TokenParams storedValues() { - TokenParams result = new TokenParams(); - result.ttl = this.ttl; - result.capability = this.capability; - result.clientId = this.clientId; - return result; - } - - /** - * Create a new copy of object - * - * @return copied object - */ - private TokenParams copy() { - TokenParams result = new TokenParams(); - result.ttl = this.ttl; - result.capability = this.capability; - result.clientId = this.clientId; - result.timestamp = this.timestamp; - return result; - } - } - - /** - * A class providing parameters of a token request. - */ - public static class TokenRequest extends TokenParams { - - public TokenRequest() {} - - public TokenRequest(TokenParams params) { - this.ttl = params.ttl; - this.capability = params.capability; - this.clientId = params.clientId; - this.timestamp = params.timestamp; - } - - /** - * The keyName of the key against which this request is made. - */ - public String keyName; - - /** - * An opaque nonce string of at least 16 characters to ensure - * uniqueness of this request. Any subsequent request using the - * same nonce will be rejected. - */ - public String nonce; - - /** - * The Message Authentication Code for this request. See the Ably - * Authentication documentation for more details. - */ - public String mac; - - /** - * Convert a JSON serialisation to a TokenParams. - * Deprecated: use fromJson() instead - * @param json - * @return - */ - @Deprecated - public static TokenRequest fromJSON(JsonObject json) { - return Serialisation.gson.fromJson(json, TokenRequest.class); - } - - /** - * Convert a parsed JSON response body to a TokenParams. - * @param json - * @return - */ - public static TokenRequest fromJsonElement(JsonObject json) { - return Serialisation.gson.fromJson(json, TokenRequest.class); - } - - /** - * Convert a string JSON response body to a TokenParams. - * Spec: TE6 - * @param json - * @return - */ - public static TokenRequest fromJson(String json) { - return Serialisation.gson.fromJson(json, TokenRequest.class); - } - - /** - * Convert a TokenParams into a JSON object. - */ - public JsonObject asJsonElement() { - JsonObject o = (JsonObject)Serialisation.gson.toJsonTree(this); - if (this.ttl == 0) { - o.remove("ttl"); - } - if (this.capability != null && this.capability.isEmpty()) { - o.remove("capability"); - } - return o; - } - - /** - * Convert a TokenParams into a JSON string. - */ - public String asJson() { - return asJsonElement().toString(); - } - - /** - * Check equality of a TokenRequest - * @param obj - */ - @Override - public boolean equals(Object obj) { - TokenRequest request = (TokenRequest)obj; - return super.equals(obj) & - equalNullableStrings(this.keyName, request.keyName) & - equalNullableStrings(this.nonce, request.nonce) & - equalNullableStrings(this.mac, request.mac); - } - } - - /** - * An interface implemented by a callback that provides either tokens, - * or signed token requests, in response to a request with given token params. - */ - public interface TokenCallback { - Object getTokenRequest(TokenParams params) throws AblyException; - } - - /** - * The clientId for this library instance - * Spec RSA7b - */ - public String clientId; - - /** - * Ensure valid auth credentials are present. This may rely in an already-known - * and valid token, and will obtain a new token if necessary or explicitly - * requested. - * Authorization will use the parameters supplied on construction except - * where overridden with the options supplied in the call. - * - * @param params - * an object containing the request params: - * - key: (optional) the key to use; if not specified, the key - * passed in constructing the Rest interface may be used - * - * - ttl: (optional) the requested life of any new token in ms. If none - * is specified a default of 1 hour is provided. The maximum lifetime - * is 24hours; any request exceeeding that lifetime will be rejected - * with an error. - * - * - capability: (optional) the capability to associate with the access token. - * If none is specified, a token will be requested with all of the - * capabilities of the specified key. - * - * - clientId: (optional) a client Id to associate with the token - * - * - timestamp: (optional) the time in ms since the epoch. If none is specified, - * the system will be queried for a time value to use. - * - * - queryTime (optional) boolean indicating that the Ably system should be - * queried for the current time when none is specified explicitly. - * - * @param options - */ - public TokenDetails authorize(TokenParams params, AuthOptions options) throws AblyException { - /* Spec: RSA10g */ - if (options != null) - this.authOptions = options.storedValues(); - if (params != null) - this.tokenParams = params.storedValues(); - - /* Spec: RSA10j */ - options = (options == null) ? this.authOptions : options.copy(); - params = (params == null) ? this.tokenParams : params.copy(); - - /* RSA10e (as clarified in PR https://github.com/ably/docs/pull/186 ) - * Use supplied token or tokenDetails if any. */ - if (authOptions.token != null) { - authOptions.tokenDetails = new TokenDetails(authOptions.token); - } - TokenDetails tokenDetails; - if(authOptions.tokenDetails != null) { - tokenDetails = authOptions.tokenDetails; - setTokenDetails(tokenDetails); - } else { - try { - tokenDetails = assertValidToken(params, options, true); - } catch (AblyException e) { - /* Give AblyRealtime a chance to update its state and emit an event according to RSA4c */ - ably.onAuthError(e.errorInfo); - throw e; - } - } - ably.onAuthUpdated(tokenDetails.token, true); - return tokenDetails; - } - - /** - * Alias of authorize() (0.9 RSA10l) - */ - @Deprecated - public TokenDetails authorise(TokenParams params, AuthOptions options) throws AblyException { - Log.w(TAG, "authorise() is deprecated and will be removed in 1.0. Please use authorize() instead"); - return authorize(params, options); - } - - /** - * Make a token request. This will make a token request now, even if the library already - * has a valid token. It would typically be used to issue tokens for use by other clients. - * @param params : see {@link #authorize} for params - * @param tokenOptions : see {@link #authorize} for options - * @return: the TokenDetails - * @throws AblyException - */ - public TokenDetails requestToken(TokenParams params, AuthOptions tokenOptions) throws AblyException { - /* Spec: RSA8e */ - tokenOptions = (tokenOptions == null) ? this.authOptions : tokenOptions.copy(); - params = (params == null) ? this.tokenParams : params.copy(); - - /* Spec: RSA7d */ - if(params.clientId == null) { - params.clientId = ably.options.clientId; - } - params.capability = Capability.c14n(params.capability); - - /* get the signed token request */ - TokenRequest signedTokenRequest; - if(tokenOptions.authCallback != null) { - Log.i("Auth.requestToken()", "using token auth with auth_callback"); - try { - /* the callback can return either a signed token request, or a TokenDetails */ - Object authCallbackResponse = tokenOptions.authCallback.getTokenRequest(params); - if(authCallbackResponse instanceof String) - return new TokenDetails((String)authCallbackResponse); - if(authCallbackResponse instanceof TokenDetails) - return (TokenDetails)authCallbackResponse; - if(authCallbackResponse instanceof TokenRequest) - signedTokenRequest = (TokenRequest)authCallbackResponse; - else - throw AblyException.fromErrorInfo(new ErrorInfo("Invalid authCallback response", 400, 40000)); - } catch(AblyException e) { - throw AblyException.fromErrorInfo(e, new ErrorInfo("authCallback failed with an exception", 401, 80019)); - } - } else if(tokenOptions.authUrl != null) { - Log.i("Auth.requestToken()", "using token auth with auth_url"); - - /* the auth request can return either a signed token request as a TokenParams, or a TokenDetails */ - Object authUrlResponse = null; - try { - HttpCore.ResponseHandler responseHandler = new HttpCore.ResponseHandler() { - @Override - public Object handleResponse(HttpCore.Response response, ErrorInfo error) throws AblyException { - if(error != null) { - throw AblyException.fromErrorInfo(error); - } - try { - String contentType = response.contentType; - byte[] body = response.body; - if(body == null || body.length == 0) { - return null; - } - if(contentType != null) { - if(contentType.startsWith("text/plain") || contentType.startsWith("application/jwt")) { - /* assumed to be token string */ - String token = new String(body); - return new TokenDetails(token); - } - if(!contentType.startsWith("application/json")) { - throw AblyException.fromErrorInfo(new ErrorInfo("Unacceptable content type from auth callback", 406, 40170)); - } - } - /* if not explicitly indicated, we will just assume it's JSON */ - JsonElement json = Serialisation.gsonParser.parse(new String(body)); - if(!(json instanceof JsonObject)) { - throw AblyException.fromErrorInfo(new ErrorInfo("Unexpected response type from auth callback", 406, 40170)); - } - JsonObject jsonObject = (JsonObject)json; - if(jsonObject.has("issued")) { - /* we assume this is a token details */ - return TokenDetails.fromJsonElement(jsonObject); - } else { - /* otherwise it's a signed token request */ - return TokenRequest.fromJsonElement(jsonObject); - } - } catch(JsonParseException e) { - throw AblyException.fromErrorInfo(new ErrorInfo("Unable to parse response from auth callback", 406, 40170)); - } - } - }; - - /* append all relevant params to token params */ - Map urlParams = null; - URL authUrl = HttpUtils.parseUrl(authOptions.authUrl); - String queryString = authUrl.getQuery(); - if(queryString != null && !queryString.isEmpty()) { - urlParams = HttpUtils.decodeParams(queryString); - } - Map tokenParams = params.asMap(); - if(tokenOptions.authParams != null) { - for(Param p : tokenOptions.authParams) { - /* (RSA8c2) TokenParams take precedence over any configured - * authParams when a name conflict occurs */ - if(!tokenParams.containsKey(p.key)) { - tokenParams.put(p.key, p); - } - } - } - if (HttpConstants.Methods.POST.equals(tokenOptions.authMethod)) { - authUrlResponse = HttpHelpers.postUri(ably.httpCore, tokenOptions.authUrl, tokenOptions.authHeaders, HttpUtils.flattenParams(urlParams), HttpUtils.flattenParams(tokenParams), responseHandler); - } else { - Map requestParams = (urlParams != null) ? HttpUtils.mergeParams(urlParams, tokenParams) : tokenParams; - authUrlResponse = HttpHelpers.getUri(ably.httpCore, tokenOptions.authUrl, tokenOptions.authHeaders, HttpUtils.flattenParams(requestParams), responseHandler); - } - } catch(AblyException e) { - throw AblyException.fromErrorInfo(e, new ErrorInfo("authUrl failed with an exception", 401, 80019)); - } - if(authUrlResponse == null) { - throw AblyException.fromErrorInfo(null, new ErrorInfo("Empty response received from authUrl", 401, 80019)); - } - if(authUrlResponse instanceof TokenDetails) { - /* we're done */ - return (TokenDetails)authUrlResponse; - } - /* otherwise it's a signed token request */ - signedTokenRequest = (TokenRequest)authUrlResponse; - } else if(tokenOptions.key != null) { - Log.i("Auth.requestToken()", "using token auth with client-side signing"); - signedTokenRequest = createTokenRequest(params, tokenOptions); - } else { - throw AblyException.fromErrorInfo(new ErrorInfo("Auth.requestToken(): options must include valid authentication parameters", 400, 40106)); - } - - String tokenPath = "/keys/" + signedTokenRequest.keyName + "/requestToken"; - return HttpHelpers.postSync(ably.http, tokenPath, null, null, new HttpUtils.JsonRequestBody(signedTokenRequest.asJsonElement().toString()), new HttpCore.ResponseHandler() { - @Override - public TokenDetails handleResponse(HttpCore.Response response, ErrorInfo error) throws AblyException { - if(error != null) { - throw AblyException.fromErrorInfo(error); - } - try { - String jsonText = new String(response.body); - JsonObject json = (JsonObject)Serialisation.gsonParser.parse(jsonText); - return TokenDetails.fromJsonElement(json); - } catch(JsonParseException e) { - throw AblyException.fromThrowable(e); - } - } - }, false); - } - - /** - * Create a signed token request based on known credentials - * and the given token params. This would typically be used if creating - * signed requests for submission by another client. - * @param params : see {@link #authorize} for params - * @param options : see {@link #authorize} for options - * @return: the params augmented with the mac. - * @throws AblyException - */ - public TokenRequest createTokenRequest(TokenParams params, AuthOptions options) throws AblyException { - /* Spec: RSA9h */ - options = (options == null) ? this.authOptions : options.copy(); - params = (params == null) ? this.tokenParams : params.copy(); - - if(params.capability != null) - params.capability = Capability.c14n(params.capability); - TokenRequest request = new TokenRequest(params); - - String key = options.key; - if(key == null) - throw AblyException.fromErrorInfo(new ErrorInfo("No key specified", 401, 40101)); - - String[] keyParts = key.split(":"); - if(keyParts.length != 2) - throw AblyException.fromErrorInfo(new ErrorInfo("Invalid key specified", 401, 40101)); - - String keyName = keyParts[0], keySecret = keyParts[1]; - if(request.keyName == null) - request.keyName = keyName; - else if(!request.keyName.equals(keyName)) - throw AblyException.fromErrorInfo(new ErrorInfo("Incompatible keys specified", 401, 40102)); - - /* expires */ - String ttlText = (request.ttl == 0) ? "" : String.valueOf(request.ttl); - - /* capability */ - String capabilityText = (request.capability == null) ? "" : request.capability; - - /* clientId */ - if (request.clientId == null) request.clientId = ably.options.clientId; - String clientIdText = (request.clientId == null) ? "" : request.clientId; - - /* timestamp */ - if(request.timestamp == 0) { - if(options.queryTime) { - long oldNanoTimeDelta = nanoTimeDelta; - long currentNanoTimeDelta = System.currentTimeMillis() - System.nanoTime()/(1000*1000); - - if (timeDelta != Long.MAX_VALUE) { - /* system time changed by more than 500ms since last time? */ - if(Math.abs(oldNanoTimeDelta - currentNanoTimeDelta) > 500) - timeDelta = Long.MAX_VALUE; - } - - if (timeDelta != Long.MAX_VALUE) { - request.timestamp = timestamp() + timeDelta; - nanoTimeDelta = currentNanoTimeDelta; - } else { - request.timestamp = ably.time(); - timeDelta = request.timestamp - timestamp(); - } - } - else { - request.timestamp = timestamp(); - } - } - - /* nonce */ - request.nonce = random(); - - String signText - = request.keyName + '\n' - + ttlText + '\n' - + capabilityText + '\n' - + clientIdText + '\n' - + request.timestamp + '\n' - + request.nonce + '\n'; - - request.mac = hmac(signText, keySecret); - - Log.i("Auth.getTokenRequest()", "generated signed request"); - return request; - } - - /** - * Get the authentication method for this library instance. - * @return - */ - public AuthMethod getAuthMethod() { - return method; - } - - /** - * Get the credentials for HTTP basic auth, if available. - * @return - */ - public String getBasicCredentials() { - return (method == AuthMethod.basic) ? basicCredentials : null; - } - - /** - * Get query params representing the current authentication method and credentials. - * @return - * @throws AblyException - */ - public Param[] getAuthParams() throws AblyException { - Param[] params = null; - switch(method) { - case basic: - params = new Param[]{new Param("key", authOptions.key) }; - break; - case token: - assertValidToken(); - params = new Param[]{new Param("accessToken", getTokenDetails().token) }; - break; - } - return params; - } - - /** - * Get (a copy of) auth options currently set in this Auth. - */ - public AuthOptions getAuthOptions() { - return authOptions.copy(); - } - - /** - * Renew auth credentials. - * Will obtain a new token, even if we already have an apparently valid one. - * Authorization will use the parameters supplied on construction. - */ - public TokenDetails renew() throws AblyException { - TokenDetails tokenDetails = assertValidToken(this.tokenParams, this.authOptions, true); - ably.onAuthUpdated(tokenDetails.token, false); - return tokenDetails; - } - - public void onAuthError(ErrorInfo err) { - /* we're only interested in token expiry errors */ - if(err.code >= 40140 && err.code < 40150) - clearTokenDetails(); - } - - public static long timestamp() { return System.currentTimeMillis(); } - - /******************** - * internal - ********************/ - - /** - * Private constructor. - * @param ably - * @param options - * @throws AblyException - */ - Auth(AblyBase ably, ClientOptions options) throws AblyException { - this.ably = ably; - authOptions = options; - tokenParams = options.defaultTokenParams != null ? - options.defaultTokenParams : new TokenParams(); - - /* set clientId (spec Rsa7b1) */ - if(options.clientId != null) { - if(options.clientId.equals(WILDCARD_CLIENTID)) { - /* RSA7c */ - throw AblyException.fromErrorInfo(new ErrorInfo("Disallowed wildcard clientId in ClientOptions", 400, 40000)); - } - /* RSC17 */ - setClientId(options.clientId); - /* RSA7a4 */ - tokenParams.clientId = options.clientId; - } - - /* decide default auth method (spec: RSA4) */ - if(authOptions.key != null) { - if(options.clientId == null && - !options.useTokenAuth && - options.token == null && - options.tokenDetails == null && - options.authCallback == null && - options.authUrl == null) { - /* we have the key and do not need to authenticate the client, - * so default to using basic auth */ - Log.i("Auth()", "anonymous, using basic auth"); - this.method = AuthMethod.basic; - basicCredentials = authOptions.key; - setClientId(WILDCARD_CLIENTID); - return; - } - } - /* using token auth, but decide the method */ - this.method = AuthMethod.token; - if(authOptions.token != null) { - setTokenDetails(authOptions.token); - } - else if(authOptions.tokenDetails != null) { - setTokenDetails(authOptions.tokenDetails); - } - - if(authOptions.authCallback != null) { - Log.i("Auth()", "using token auth with authCallback"); - } else if(authOptions.authUrl != null) { - /* verify configured URL parses */ - HttpUtils.parseUrl(authOptions.authUrl); - Log.i("Auth()", "using token auth with authUrl"); - } else if(authOptions.key != null) { - Log.i("Auth()", "using token auth with client-side signing"); - } else if(tokenDetails != null) { - Log.i("Auth()", "using token auth with supplied token only"); - } else { - /* no means to authenticate (Spec: RSA14) */ - Log.e("Auth()", "no authentication parameters supplied"); - throw AblyException.fromErrorInfo(new ErrorInfo("No authentication parameters supplied", 400, 40000)); - } - } - - public TokenDetails getTokenDetails() { - Log.i("TokenAuth.getTokenDetails()", ""); - return tokenDetails; - } - - public String getEncodedToken() { - Log.i("TokenAuth.getEncodedToken()", ""); - return encodedToken; - } - - private void setTokenDetails(String token) throws AblyException { - Log.i("TokenAuth.setTokenDetails()", ""); - this.tokenDetails = new TokenDetails(token); - this.encodedToken = Base64Coder.encodeString(token).replace("=", ""); - } - - private void setTokenDetails(TokenDetails tokenDetails) throws AblyException { - Log.i("TokenAuth.setTokenDetails()", ""); - setClientId(tokenDetails.clientId); - this.tokenDetails = tokenDetails; - this.encodedToken = Base64Coder.encodeString(tokenDetails.token).replace("=", ""); - } - - private void clearTokenDetails() { - Log.i("TokenAuth.clearTokenDetails()", ""); - this.tokenDetails = null; - this.encodedToken = null; - this.authHeader = null; - } - - public TokenDetails assertValidToken() throws AblyException { - return assertValidToken(tokenParams, authOptions, false); - } - - private TokenDetails assertValidToken(TokenParams params, AuthOptions options, boolean force) throws AblyException { - Log.i("Auth.assertValidToken()", ""); - if(tokenDetails != null) { - if(!force && (tokenDetails.expires == 0 || tokenValid(tokenDetails))) { - Log.i("Auth.assertValidToken()", "using cached token; expires = " + tokenDetails.expires); - return tokenDetails; - } else { - /* expired, so remove */ - Log.i("Auth.assertValidToken()", "deleting expired token"); - clearTokenDetails(); - } - } - Log.i("Auth.assertValidToken()", "requesting new token"); - setTokenDetails(requestToken(params, options)); - return tokenDetails; - } - - private boolean tokenValid(TokenDetails tokenDetails) { - /* RSA4b1: only perform a local check for token validity if we have time sync with the server */ - return (timeDelta == Long.MAX_VALUE) || (tokenDetails.expires > serverTimestamp()); - } - - /** - * Get the Authorization header, forcing the creation of a new token if requested - * @param forceRenew - * @return - * @throws AblyException - */ - public void assertAuthorizationHeader(boolean forceRenew) throws AblyException { - if(authHeader != null && !forceRenew) { - return; - } - if(getAuthMethod() == AuthMethod.basic) { - authHeader = "Basic " + Base64Coder.encodeString(getBasicCredentials()); - } else { - if (forceRenew) { - renew(); - } else { - assertValidToken(); - } - authHeader = "Bearer " + getEncodedToken(); - } - } - - public String getAuthorizationHeader() { - return authHeader; - } - - private static String random() { return String.format("%016d", (long)(Math.random() * 1E16)); } - - private static boolean equalNullableStrings(String one, String two) { - return (one == null) ? (two == null) : one.equals(two); - } - - private static String hmac(String text, String key) { - try { - Mac mac = Mac.getInstance("HmacSHA256"); - mac.init(new SecretKeySpec(key.getBytes(Charset.forName("UTF-8")), "HmacSHA256")); - return new String(Base64Coder.encode(mac.doFinal(text.getBytes(Charset.forName("UTF-8"))))); - } catch (GeneralSecurityException e) { Log.e("Auth.hmac", "Unexpected exception", e); return null; } - } - - /** - * Set the clientId, after first initialisation in the construction of the library - * therefore an existing null value is significant - it means that ClientOptions.clientId - * was null - * @param clientId - * @throws AblyException - */ - public void setClientId(String clientId) throws AblyException { - if(clientId == null) { - /* do nothing - we received a token without a clientId */ - return; - } - - if(this.clientId == null) { - /* RSA12a, RSA12b, RSA7b2, RSA7b3, RSA7b4: the given clientId is now our clientId */ - this.clientId = clientId; - this.ably.onClientIdSet(clientId); - return; - } - /* now this.clientId != null */ - if(this.clientId.equals(clientId)) { - /* this includes the wildcard case RSA7b4 */ - return; - } - if(WILDCARD_CLIENTID.equals(clientId)) { - /* this signifies that the credentials permit the use of any specific clientId */ - return; - } - throw AblyException.fromErrorInfo(new ErrorInfo("Unable to set different clientId from that given in options", 401, 40101)); - } - - /** - * Verify that a message, possibly containing a clientId, - * is compatible with Auth.clientId if it is set - * @param msg - * @param allowNullClientId: true if it is ok for there to be no resolved clientId - * @param connected: true if connected; if false it is ok for the library to be unidentified - * @return the resolved clientId - * @throws AblyException - */ - public String checkClientId(BaseMessage msg, boolean allowNullClientId, boolean connected) throws AblyException { - /* Check that the message doesn't contain the disallowed wildcard clientId - * RTL6g3 */ - String msgClientId = msg.clientId; - if(WILDCARD_CLIENTID.equals(msgClientId)) { - throw AblyException.fromErrorInfo(new ErrorInfo("Invalid wildcard clientId specified in message", 400, 40000)); - } - - /* Check that any clientId given in the message is compatible with the library clientId */ - boolean undeterminedClientId = (clientId == null && !connected); - if(msgClientId != null) { - if(msgClientId.equals(clientId) || WILDCARD_CLIENTID.equals(clientId) || undeterminedClientId) { - /* RTL6g4: be lenient checking against a null clientId if we're not connected */ - return msgClientId; - } - throw AblyException.fromErrorInfo(new ErrorInfo("Incompatible clientId specified in message", 400, 40012)); - } - - if(clientId == null || clientId.equals(WILDCARD_CLIENTID)) { - if(allowNullClientId || undeterminedClientId) { - /* the message is sent with no clientId */ - return null; - } - /* this case only applies to presence, when allowNullClientId=false */ - throw AblyException.fromErrorInfo(new ErrorInfo("Invalid attempt to enter with no clientId", 400, 91000)); - } - - /* the message is sent with no explicit clientId, but implicitly has the library clientId */ - return clientId; - } - - /** - * Using time delta obtained before guess current server time - */ - public long serverTimestamp() { - long clientTime = timestamp(); - long delta = timeDelta; - return delta != Long.MAX_VALUE ? clientTime + timeDelta : clientTime; - } - - private static final String TAG = Auth.class.getName(); - private final AblyBase ably; - private final AuthMethod method; - private AuthOptions authOptions; - private TokenParams tokenParams; - private String basicCredentials; - private TokenDetails tokenDetails; - private String encodedToken; - private String authHeader; - - /** - * Time delta is server time minus client time, in milliseconds, MAX_VALUE if not obtained yet - */ - private long timeDelta = Long.MAX_VALUE; - /** - * Time delta between System.nanoTime() and System.currentTimeMillis. If it changes significantly it - * suggests device time/date has changed - */ - private long nanoTimeDelta = System.currentTimeMillis() - System.nanoTime()/(1000*1000); - - public static final String WILDCARD_CLIENTID = "*"; - /** - * For testing purposes we need method to clear cached timeDelta - */ - public void clearCachedServerTime() { - timeDelta = Long.MAX_VALUE; - } + /** + * A class providing parameters of a token request. + */ + public static class TokenParams { + + /** + * Requested time to live for the token. If the token request + * is successful, the TTL of the returned token will be less + * than or equal to this value depending on application settings + * and the attributes of the issuing key. + * + * 0 means Ably will set it to the default value. + */ + public long ttl; + + /** + * Capability of the token. If the token request is successful, + * the capability of the returned token will be the intersection of + * this capability with the capability of the issuing key. + */ + public String capability; + + /** + * A clientId to associate with this token. The generated token + * may be used to authenticate as this clientId. + */ + public String clientId; + + /** + * The timestamp (in millis since the epoch) of this request. + * Timestamps, in conjunction with the nonce, are used to prevent + * token requests from being replayed. + */ + public long timestamp; + + /** + * Internal; convert a TokenParams to a collection of Params + * @return + */ + public Map asMap() { + Map params = new HashMap(); + if(ttl > 0) params.put("ttl", new Param("ttl", String.valueOf(ttl))); + if(capability != null) params.put("capability", new Param("capability", capability)); + if(clientId != null) params.put("clientId", new Param("clientId", clientId)); + if(timestamp > 0) params.put("timestamp", new Param("timestamp", String.valueOf(timestamp))); + return params; + } + + /** + * Check equality of a TokenParams + * @param obj + */ + @Override + public boolean equals(Object obj) { + TokenParams params = (TokenParams)obj; + return (this.ttl == params.ttl) & + equalNullableStrings(this.capability, params.capability) & + equalNullableStrings(this.clientId, params.clientId) & + (this.timestamp == params.timestamp); + } + + /** + * Stores the TokenParams arguments as defaults for subsequent authorizations + * with the exception of the attributes {@link TokenParams#timestamp} + *

+ * Spec: RSA10g + *

+ */ + private TokenParams storedValues() { + TokenParams result = new TokenParams(); + result.ttl = this.ttl; + result.capability = this.capability; + result.clientId = this.clientId; + return result; + } + + /** + * Create a new copy of object + * + * @return copied object + */ + private TokenParams copy() { + TokenParams result = new TokenParams(); + result.ttl = this.ttl; + result.capability = this.capability; + result.clientId = this.clientId; + result.timestamp = this.timestamp; + return result; + } + } + + /** + * A class providing parameters of a token request. + */ + public static class TokenRequest extends TokenParams { + + public TokenRequest() {} + + public TokenRequest(TokenParams params) { + this.ttl = params.ttl; + this.capability = params.capability; + this.clientId = params.clientId; + this.timestamp = params.timestamp; + } + + /** + * The keyName of the key against which this request is made. + */ + public String keyName; + + /** + * An opaque nonce string of at least 16 characters to ensure + * uniqueness of this request. Any subsequent request using the + * same nonce will be rejected. + */ + public String nonce; + + /** + * The Message Authentication Code for this request. See the Ably + * Authentication documentation for more details. + */ + public String mac; + + /** + * Convert a JSON serialisation to a TokenParams. + * Deprecated: use fromJson() instead + * @param json + * @return + */ + @Deprecated + public static TokenRequest fromJSON(JsonObject json) { + return Serialisation.gson.fromJson(json, TokenRequest.class); + } + + /** + * Convert a parsed JSON response body to a TokenParams. + * @param json + * @return + */ + public static TokenRequest fromJsonElement(JsonObject json) { + return Serialisation.gson.fromJson(json, TokenRequest.class); + } + + /** + * Convert a string JSON response body to a TokenParams. + * Spec: TE6 + * @param json + * @return + */ + public static TokenRequest fromJson(String json) { + return Serialisation.gson.fromJson(json, TokenRequest.class); + } + + /** + * Convert a TokenParams into a JSON object. + */ + public JsonObject asJsonElement() { + JsonObject o = (JsonObject)Serialisation.gson.toJsonTree(this); + if (this.ttl == 0) { + o.remove("ttl"); + } + if (this.capability != null && this.capability.isEmpty()) { + o.remove("capability"); + } + return o; + } + + /** + * Convert a TokenParams into a JSON string. + */ + public String asJson() { + return asJsonElement().toString(); + } + + /** + * Check equality of a TokenRequest + * @param obj + */ + @Override + public boolean equals(Object obj) { + TokenRequest request = (TokenRequest)obj; + return super.equals(obj) & + equalNullableStrings(this.keyName, request.keyName) & + equalNullableStrings(this.nonce, request.nonce) & + equalNullableStrings(this.mac, request.mac); + } + } + + /** + * An interface implemented by a callback that provides either tokens, + * or signed token requests, in response to a request with given token params. + */ + public interface TokenCallback { + Object getTokenRequest(TokenParams params) throws AblyException; + } + + /** + * The clientId for this library instance + * Spec RSA7b + */ + public String clientId; + + /** + * Ensure valid auth credentials are present. This may rely in an already-known + * and valid token, and will obtain a new token if necessary or explicitly + * requested. + * Authorization will use the parameters supplied on construction except + * where overridden with the options supplied in the call. + * + * @param params + * an object containing the request params: + * - key: (optional) the key to use; if not specified, the key + * passed in constructing the Rest interface may be used + * + * - ttl: (optional) the requested life of any new token in ms. If none + * is specified a default of 1 hour is provided. The maximum lifetime + * is 24hours; any request exceeeding that lifetime will be rejected + * with an error. + * + * - capability: (optional) the capability to associate with the access token. + * If none is specified, a token will be requested with all of the + * capabilities of the specified key. + * + * - clientId: (optional) a client Id to associate with the token + * + * - timestamp: (optional) the time in ms since the epoch. If none is specified, + * the system will be queried for a time value to use. + * + * - queryTime (optional) boolean indicating that the Ably system should be + * queried for the current time when none is specified explicitly. + * + * @param options + */ + public TokenDetails authorize(TokenParams params, AuthOptions options) throws AblyException { + /* Spec: RSA10g */ + if (options != null) + this.authOptions = options.storedValues(); + if (params != null) + this.tokenParams = params.storedValues(); + + /* Spec: RSA10j */ + options = (options == null) ? this.authOptions : options.copy(); + params = (params == null) ? this.tokenParams : params.copy(); + + /* RSA10e (as clarified in PR https://github.com/ably/docs/pull/186 ) + * Use supplied token or tokenDetails if any. */ + if (authOptions.token != null) { + authOptions.tokenDetails = new TokenDetails(authOptions.token); + } + TokenDetails tokenDetails; + if(authOptions.tokenDetails != null) { + tokenDetails = authOptions.tokenDetails; + setTokenDetails(tokenDetails); + } else { + try { + tokenDetails = assertValidToken(params, options, true); + } catch (AblyException e) { + /* Give AblyRealtime a chance to update its state and emit an event according to RSA4c */ + ably.onAuthError(e.errorInfo); + throw e; + } + } + ably.onAuthUpdated(tokenDetails.token, true); + return tokenDetails; + } + + /** + * Alias of authorize() (0.9 RSA10l) + */ + @Deprecated + public TokenDetails authorise(TokenParams params, AuthOptions options) throws AblyException { + Log.w(TAG, "authorise() is deprecated and will be removed in 1.0. Please use authorize() instead"); + return authorize(params, options); + } + + /** + * Make a token request. This will make a token request now, even if the library already + * has a valid token. It would typically be used to issue tokens for use by other clients. + * @param params : see {@link #authorize} for params + * @param tokenOptions : see {@link #authorize} for options + * @return: the TokenDetails + * @throws AblyException + */ + public TokenDetails requestToken(TokenParams params, AuthOptions tokenOptions) throws AblyException { + /* Spec: RSA8e */ + tokenOptions = (tokenOptions == null) ? this.authOptions : tokenOptions.copy(); + params = (params == null) ? this.tokenParams : params.copy(); + + /* Spec: RSA7d */ + if(params.clientId == null) { + params.clientId = ably.options.clientId; + } + params.capability = Capability.c14n(params.capability); + + /* get the signed token request */ + TokenRequest signedTokenRequest; + if(tokenOptions.authCallback != null) { + Log.i("Auth.requestToken()", "using token auth with auth_callback"); + try { + /* the callback can return either a signed token request, or a TokenDetails */ + Object authCallbackResponse = tokenOptions.authCallback.getTokenRequest(params); + if(authCallbackResponse instanceof String) + return new TokenDetails((String)authCallbackResponse); + if(authCallbackResponse instanceof TokenDetails) + return (TokenDetails)authCallbackResponse; + if(authCallbackResponse instanceof TokenRequest) + signedTokenRequest = (TokenRequest)authCallbackResponse; + else + throw AblyException.fromErrorInfo(new ErrorInfo("Invalid authCallback response", 400, 40000)); + } catch(AblyException e) { + throw AblyException.fromErrorInfo(e, new ErrorInfo("authCallback failed with an exception", 401, 80019)); + } + } else if(tokenOptions.authUrl != null) { + Log.i("Auth.requestToken()", "using token auth with auth_url"); + + /* the auth request can return either a signed token request as a TokenParams, or a TokenDetails */ + Object authUrlResponse = null; + try { + HttpCore.ResponseHandler responseHandler = new HttpCore.ResponseHandler() { + @Override + public Object handleResponse(HttpCore.Response response, ErrorInfo error) throws AblyException { + if(error != null) { + throw AblyException.fromErrorInfo(error); + } + try { + String contentType = response.contentType; + byte[] body = response.body; + if(body == null || body.length == 0) { + return null; + } + if(contentType != null) { + if(contentType.startsWith("text/plain") || contentType.startsWith("application/jwt")) { + /* assumed to be token string */ + String token = new String(body); + return new TokenDetails(token); + } + if(!contentType.startsWith("application/json")) { + throw AblyException.fromErrorInfo(new ErrorInfo("Unacceptable content type from auth callback", 406, 40170)); + } + } + /* if not explicitly indicated, we will just assume it's JSON */ + JsonElement json = Serialisation.gsonParser.parse(new String(body)); + if(!(json instanceof JsonObject)) { + throw AblyException.fromErrorInfo(new ErrorInfo("Unexpected response type from auth callback", 406, 40170)); + } + JsonObject jsonObject = (JsonObject)json; + if(jsonObject.has("issued")) { + /* we assume this is a token details */ + return TokenDetails.fromJsonElement(jsonObject); + } else { + /* otherwise it's a signed token request */ + return TokenRequest.fromJsonElement(jsonObject); + } + } catch(JsonParseException e) { + throw AblyException.fromErrorInfo(new ErrorInfo("Unable to parse response from auth callback", 406, 40170)); + } + } + }; + + /* append all relevant params to token params */ + Map urlParams = null; + URL authUrl = HttpUtils.parseUrl(authOptions.authUrl); + String queryString = authUrl.getQuery(); + if(queryString != null && !queryString.isEmpty()) { + urlParams = HttpUtils.decodeParams(queryString); + } + Map tokenParams = params.asMap(); + if(tokenOptions.authParams != null) { + for(Param p : tokenOptions.authParams) { + /* (RSA8c2) TokenParams take precedence over any configured + * authParams when a name conflict occurs */ + if(!tokenParams.containsKey(p.key)) { + tokenParams.put(p.key, p); + } + } + } + if (HttpConstants.Methods.POST.equals(tokenOptions.authMethod)) { + authUrlResponse = HttpHelpers.postUri(ably.httpCore, tokenOptions.authUrl, tokenOptions.authHeaders, HttpUtils.flattenParams(urlParams), HttpUtils.flattenParams(tokenParams), responseHandler); + } else { + Map requestParams = (urlParams != null) ? HttpUtils.mergeParams(urlParams, tokenParams) : tokenParams; + authUrlResponse = HttpHelpers.getUri(ably.httpCore, tokenOptions.authUrl, tokenOptions.authHeaders, HttpUtils.flattenParams(requestParams), responseHandler); + } + } catch(AblyException e) { + throw AblyException.fromErrorInfo(e, new ErrorInfo("authUrl failed with an exception", 401, 80019)); + } + if(authUrlResponse == null) { + throw AblyException.fromErrorInfo(null, new ErrorInfo("Empty response received from authUrl", 401, 80019)); + } + if(authUrlResponse instanceof TokenDetails) { + /* we're done */ + return (TokenDetails)authUrlResponse; + } + /* otherwise it's a signed token request */ + signedTokenRequest = (TokenRequest)authUrlResponse; + } else if(tokenOptions.key != null) { + Log.i("Auth.requestToken()", "using token auth with client-side signing"); + signedTokenRequest = createTokenRequest(params, tokenOptions); + } else { + throw AblyException.fromErrorInfo(new ErrorInfo("Auth.requestToken(): options must include valid authentication parameters", 400, 40106)); + } + + String tokenPath = "/keys/" + signedTokenRequest.keyName + "/requestToken"; + return HttpHelpers.postSync(ably.http, tokenPath, null, null, new HttpUtils.JsonRequestBody(signedTokenRequest.asJsonElement().toString()), new HttpCore.ResponseHandler() { + @Override + public TokenDetails handleResponse(HttpCore.Response response, ErrorInfo error) throws AblyException { + if(error != null) { + throw AblyException.fromErrorInfo(error); + } + try { + String jsonText = new String(response.body); + JsonObject json = (JsonObject)Serialisation.gsonParser.parse(jsonText); + return TokenDetails.fromJsonElement(json); + } catch(JsonParseException e) { + throw AblyException.fromThrowable(e); + } + } + }, false); + } + + /** + * Create a signed token request based on known credentials + * and the given token params. This would typically be used if creating + * signed requests for submission by another client. + * @param params : see {@link #authorize} for params + * @param options : see {@link #authorize} for options + * @return: the params augmented with the mac. + * @throws AblyException + */ + public TokenRequest createTokenRequest(TokenParams params, AuthOptions options) throws AblyException { + /* Spec: RSA9h */ + options = (options == null) ? this.authOptions : options.copy(); + params = (params == null) ? this.tokenParams : params.copy(); + + if(params.capability != null) + params.capability = Capability.c14n(params.capability); + TokenRequest request = new TokenRequest(params); + + String key = options.key; + if(key == null) + throw AblyException.fromErrorInfo(new ErrorInfo("No key specified", 401, 40101)); + + String[] keyParts = key.split(":"); + if(keyParts.length != 2) + throw AblyException.fromErrorInfo(new ErrorInfo("Invalid key specified", 401, 40101)); + + String keyName = keyParts[0], keySecret = keyParts[1]; + if(request.keyName == null) + request.keyName = keyName; + else if(!request.keyName.equals(keyName)) + throw AblyException.fromErrorInfo(new ErrorInfo("Incompatible keys specified", 401, 40102)); + + /* expires */ + String ttlText = (request.ttl == 0) ? "" : String.valueOf(request.ttl); + + /* capability */ + String capabilityText = (request.capability == null) ? "" : request.capability; + + /* clientId */ + if (request.clientId == null) request.clientId = ably.options.clientId; + String clientIdText = (request.clientId == null) ? "" : request.clientId; + + /* timestamp */ + if(request.timestamp == 0) { + if(options.queryTime) { + long oldNanoTimeDelta = nanoTimeDelta; + long currentNanoTimeDelta = System.currentTimeMillis() - System.nanoTime()/(1000*1000); + + if (timeDelta != Long.MAX_VALUE) { + /* system time changed by more than 500ms since last time? */ + if(Math.abs(oldNanoTimeDelta - currentNanoTimeDelta) > 500) + timeDelta = Long.MAX_VALUE; + } + + if (timeDelta != Long.MAX_VALUE) { + request.timestamp = timestamp() + timeDelta; + nanoTimeDelta = currentNanoTimeDelta; + } else { + request.timestamp = ably.time(); + timeDelta = request.timestamp - timestamp(); + } + } + else { + request.timestamp = timestamp(); + } + } + + /* nonce */ + request.nonce = random(); + + String signText + = request.keyName + '\n' + + ttlText + '\n' + + capabilityText + '\n' + + clientIdText + '\n' + + request.timestamp + '\n' + + request.nonce + '\n'; + + request.mac = hmac(signText, keySecret); + + Log.i("Auth.getTokenRequest()", "generated signed request"); + return request; + } + + /** + * Get the authentication method for this library instance. + * @return + */ + public AuthMethod getAuthMethod() { + return method; + } + + /** + * Get the credentials for HTTP basic auth, if available. + * @return + */ + public String getBasicCredentials() { + return (method == AuthMethod.basic) ? basicCredentials : null; + } + + /** + * Get query params representing the current authentication method and credentials. + * @return + * @throws AblyException + */ + public Param[] getAuthParams() throws AblyException { + Param[] params = null; + switch(method) { + case basic: + params = new Param[]{new Param("key", authOptions.key) }; + break; + case token: + assertValidToken(); + params = new Param[]{new Param("accessToken", getTokenDetails().token) }; + break; + } + return params; + } + + /** + * Get (a copy of) auth options currently set in this Auth. + */ + public AuthOptions getAuthOptions() { + return authOptions.copy(); + } + + /** + * Renew auth credentials. + * Will obtain a new token, even if we already have an apparently valid one. + * Authorization will use the parameters supplied on construction. + */ + public TokenDetails renew() throws AblyException { + TokenDetails tokenDetails = assertValidToken(this.tokenParams, this.authOptions, true); + ably.onAuthUpdated(tokenDetails.token, false); + return tokenDetails; + } + + public void onAuthError(ErrorInfo err) { + /* we're only interested in token expiry errors */ + if(err.code >= 40140 && err.code < 40150) + clearTokenDetails(); + } + + public static long timestamp() { return System.currentTimeMillis(); } + + /******************** + * internal + ********************/ + + /** + * Private constructor. + * @param ably + * @param options + * @throws AblyException + */ + Auth(AblyBase ably, ClientOptions options) throws AblyException { + this.ably = ably; + authOptions = options; + tokenParams = options.defaultTokenParams != null ? + options.defaultTokenParams : new TokenParams(); + + /* set clientId (spec Rsa7b1) */ + if(options.clientId != null) { + if(options.clientId.equals(WILDCARD_CLIENTID)) { + /* RSA7c */ + throw AblyException.fromErrorInfo(new ErrorInfo("Disallowed wildcard clientId in ClientOptions", 400, 40000)); + } + /* RSC17 */ + setClientId(options.clientId); + /* RSA7a4 */ + tokenParams.clientId = options.clientId; + } + + /* decide default auth method (spec: RSA4) */ + if(authOptions.key != null) { + if(options.clientId == null && + !options.useTokenAuth && + options.token == null && + options.tokenDetails == null && + options.authCallback == null && + options.authUrl == null) { + /* we have the key and do not need to authenticate the client, + * so default to using basic auth */ + Log.i("Auth()", "anonymous, using basic auth"); + this.method = AuthMethod.basic; + basicCredentials = authOptions.key; + setClientId(WILDCARD_CLIENTID); + return; + } + } + /* using token auth, but decide the method */ + this.method = AuthMethod.token; + if(authOptions.token != null) { + setTokenDetails(authOptions.token); + } + else if(authOptions.tokenDetails != null) { + setTokenDetails(authOptions.tokenDetails); + } + + if(authOptions.authCallback != null) { + Log.i("Auth()", "using token auth with authCallback"); + } else if(authOptions.authUrl != null) { + /* verify configured URL parses */ + HttpUtils.parseUrl(authOptions.authUrl); + Log.i("Auth()", "using token auth with authUrl"); + } else if(authOptions.key != null) { + Log.i("Auth()", "using token auth with client-side signing"); + } else if(tokenDetails != null) { + Log.i("Auth()", "using token auth with supplied token only"); + } else { + /* no means to authenticate (Spec: RSA14) */ + Log.e("Auth()", "no authentication parameters supplied"); + throw AblyException.fromErrorInfo(new ErrorInfo("No authentication parameters supplied", 400, 40000)); + } + } + + public TokenDetails getTokenDetails() { + Log.i("TokenAuth.getTokenDetails()", ""); + return tokenDetails; + } + + public String getEncodedToken() { + Log.i("TokenAuth.getEncodedToken()", ""); + return encodedToken; + } + + private void setTokenDetails(String token) throws AblyException { + Log.i("TokenAuth.setTokenDetails()", ""); + this.tokenDetails = new TokenDetails(token); + this.encodedToken = Base64Coder.encodeString(token).replace("=", ""); + } + + private void setTokenDetails(TokenDetails tokenDetails) throws AblyException { + Log.i("TokenAuth.setTokenDetails()", ""); + setClientId(tokenDetails.clientId); + this.tokenDetails = tokenDetails; + this.encodedToken = Base64Coder.encodeString(tokenDetails.token).replace("=", ""); + } + + private void clearTokenDetails() { + Log.i("TokenAuth.clearTokenDetails()", ""); + this.tokenDetails = null; + this.encodedToken = null; + this.authHeader = null; + } + + public TokenDetails assertValidToken() throws AblyException { + return assertValidToken(tokenParams, authOptions, false); + } + + private TokenDetails assertValidToken(TokenParams params, AuthOptions options, boolean force) throws AblyException { + Log.i("Auth.assertValidToken()", ""); + if(tokenDetails != null) { + if(!force && (tokenDetails.expires == 0 || tokenValid(tokenDetails))) { + Log.i("Auth.assertValidToken()", "using cached token; expires = " + tokenDetails.expires); + return tokenDetails; + } else { + /* expired, so remove */ + Log.i("Auth.assertValidToken()", "deleting expired token"); + clearTokenDetails(); + } + } + Log.i("Auth.assertValidToken()", "requesting new token"); + setTokenDetails(requestToken(params, options)); + return tokenDetails; + } + + private boolean tokenValid(TokenDetails tokenDetails) { + /* RSA4b1: only perform a local check for token validity if we have time sync with the server */ + return (timeDelta == Long.MAX_VALUE) || (tokenDetails.expires > serverTimestamp()); + } + + /** + * Get the Authorization header, forcing the creation of a new token if requested + * @param forceRenew + * @return + * @throws AblyException + */ + public void assertAuthorizationHeader(boolean forceRenew) throws AblyException { + if(authHeader != null && !forceRenew) { + return; + } + if(getAuthMethod() == AuthMethod.basic) { + authHeader = "Basic " + Base64Coder.encodeString(getBasicCredentials()); + } else { + if (forceRenew) { + renew(); + } else { + assertValidToken(); + } + authHeader = "Bearer " + getEncodedToken(); + } + } + + public String getAuthorizationHeader() { + return authHeader; + } + + private static String random() { return String.format("%016d", (long)(Math.random() * 1E16)); } + + private static boolean equalNullableStrings(String one, String two) { + return (one == null) ? (two == null) : one.equals(two); + } + + private static String hmac(String text, String key) { + try { + Mac mac = Mac.getInstance("HmacSHA256"); + mac.init(new SecretKeySpec(key.getBytes(Charset.forName("UTF-8")), "HmacSHA256")); + return new String(Base64Coder.encode(mac.doFinal(text.getBytes(Charset.forName("UTF-8"))))); + } catch (GeneralSecurityException e) { Log.e("Auth.hmac", "Unexpected exception", e); return null; } + } + + /** + * Set the clientId, after first initialisation in the construction of the library + * therefore an existing null value is significant - it means that ClientOptions.clientId + * was null + * @param clientId + * @throws AblyException + */ + public void setClientId(String clientId) throws AblyException { + if(clientId == null) { + /* do nothing - we received a token without a clientId */ + return; + } + + if(this.clientId == null) { + /* RSA12a, RSA12b, RSA7b2, RSA7b3, RSA7b4: the given clientId is now our clientId */ + this.clientId = clientId; + this.ably.onClientIdSet(clientId); + return; + } + /* now this.clientId != null */ + if(this.clientId.equals(clientId)) { + /* this includes the wildcard case RSA7b4 */ + return; + } + if(WILDCARD_CLIENTID.equals(clientId)) { + /* this signifies that the credentials permit the use of any specific clientId */ + return; + } + throw AblyException.fromErrorInfo(new ErrorInfo("Unable to set different clientId from that given in options", 401, 40101)); + } + + /** + * Verify that a message, possibly containing a clientId, + * is compatible with Auth.clientId if it is set + * @param msg + * @param allowNullClientId: true if it is ok for there to be no resolved clientId + * @param connected: true if connected; if false it is ok for the library to be unidentified + * @return the resolved clientId + * @throws AblyException + */ + public String checkClientId(BaseMessage msg, boolean allowNullClientId, boolean connected) throws AblyException { + /* Check that the message doesn't contain the disallowed wildcard clientId + * RTL6g3 */ + String msgClientId = msg.clientId; + if(WILDCARD_CLIENTID.equals(msgClientId)) { + throw AblyException.fromErrorInfo(new ErrorInfo("Invalid wildcard clientId specified in message", 400, 40000)); + } + + /* Check that any clientId given in the message is compatible with the library clientId */ + boolean undeterminedClientId = (clientId == null && !connected); + if(msgClientId != null) { + if(msgClientId.equals(clientId) || WILDCARD_CLIENTID.equals(clientId) || undeterminedClientId) { + /* RTL6g4: be lenient checking against a null clientId if we're not connected */ + return msgClientId; + } + throw AblyException.fromErrorInfo(new ErrorInfo("Incompatible clientId specified in message", 400, 40012)); + } + + if(clientId == null || clientId.equals(WILDCARD_CLIENTID)) { + if(allowNullClientId || undeterminedClientId) { + /* the message is sent with no clientId */ + return null; + } + /* this case only applies to presence, when allowNullClientId=false */ + throw AblyException.fromErrorInfo(new ErrorInfo("Invalid attempt to enter with no clientId", 400, 91000)); + } + + /* the message is sent with no explicit clientId, but implicitly has the library clientId */ + return clientId; + } + + /** + * Using time delta obtained before guess current server time + */ + public long serverTimestamp() { + long clientTime = timestamp(); + long delta = timeDelta; + return delta != Long.MAX_VALUE ? clientTime + timeDelta : clientTime; + } + + private static final String TAG = Auth.class.getName(); + private final AblyBase ably; + private final AuthMethod method; + private AuthOptions authOptions; + private TokenParams tokenParams; + private String basicCredentials; + private TokenDetails tokenDetails; + private String encodedToken; + private String authHeader; + + /** + * Time delta is server time minus client time, in milliseconds, MAX_VALUE if not obtained yet + */ + private long timeDelta = Long.MAX_VALUE; + /** + * Time delta between System.nanoTime() and System.currentTimeMillis. If it changes significantly it + * suggests device time/date has changed + */ + private long nanoTimeDelta = System.currentTimeMillis() - System.nanoTime()/(1000*1000); + + public static final String WILDCARD_CLIENTID = "*"; + /** + * For testing purposes we need method to clear cached timeDelta + */ + public void clearCachedServerTime() { + timeDelta = Long.MAX_VALUE; + } } diff --git a/lib/src/main/java/io/ably/lib/rest/ChannelBase.java b/lib/src/main/java/io/ably/lib/rest/ChannelBase.java index 8c8808dfa..37b0574d0 100644 --- a/lib/src/main/java/io/ably/lib/rest/ChannelBase.java +++ b/lib/src/main/java/io/ably/lib/rest/ChannelBase.java @@ -30,195 +30,195 @@ */ public class ChannelBase { - /** - * The Channel name - */ - public final String name; - - /** - * The presence instance for this channel. - */ - public final Presence presence; - - /** - * Publish a message on this channel using the REST API. - * Since the REST API is stateless, this request is made independently - * of any other request on this or any other channel. - * @param name: the event name - * @param data: the message payload; see {@link io.ably.types.Data} for - * details of supported data types. - * @throws AblyException - */ - public void publish(String name, Object data) throws AblyException { - publishImpl(name, data).sync(); - } - - /** - * Publish a message on this channel using the REST API. - * Since the REST API is stateless, this request is made independently - * of any other request on this or any other channel. - * @param name: the event name - * @param data: the message payload; see {@link io.ably.types.Data} for - * @param listener - */ - public void publishAsync(String name, Object data, CompletionListener listener) { - publishImpl(name, data).async(new CompletionListener.ToCallback(listener)); - } - - private Http.Request publishImpl(String name, Object data) { - return publishImpl(new Message[] {new Message(name, data)}); - } - - /** - * Publish an array of messages on this channel. When there are - * multiple messages to be sent, it is more efficient to use this - * method to publish them in a single request, as compared with - * publishing via multiple independent requests. - * @param messages: array of messages to publish. - * @throws AblyException - */ - public void publish(final Message[] messages) throws AblyException { - publishImpl(messages).sync(); - } - - /** - * Asynchronously publish an array of messages on this channel - * @param messages - * @param listener - */ - public void publishAsync(final Message[] messages, final CompletionListener listener) { - publishImpl(messages).async(new CompletionListener.ToCallback(listener)); - } - - private Http.Request publishImpl(final Message[] messages) { - return ably.http.request(new Http.Execute() { - @Override - public void execute(HttpScheduler http, final Callback callback) throws AblyException { - /* handle message ids */ - boolean hasClientSuppliedId = false; - for(Message message : messages) { - /* RSL1k2 */ - hasClientSuppliedId |= (message.id != null); - /* RTL6g3 */ - ably.auth.checkClientId(message, true, false); - message.encode(options); - } - if(!hasClientSuppliedId && ably.options.idempotentRestPublishing) { - /* RSL1k1: populate the message id with a library-generated id */ - String messageId = Crypto.getRandomMessageId(); - for (int i = 0; i < messages.length; i++) { - messages[i].id = messageId + ':' + i; - } - } - - HttpCore.RequestBody requestBody = ably.options.useBinaryProtocol ? MessageSerializer.asMsgpackRequest(messages) : MessageSerializer.asJsonRequest(messages); - - http.post(basePath + "/messages", HttpUtils.defaultAcceptHeaders(ably.options.useBinaryProtocol), null, requestBody, null, true, callback); - } - }); - } - - /** - * Obtain recent history for this channel using the REST API. - * The history provided relqtes to all clients of this application, - * not just this instance. - * @param params: the request params. See the Ably REST API - * documentation for more details. - * @return: an array of Messages for this Channel. - * @throws AblyException - */ - public PaginatedResult history(Param[] params) throws AblyException { - return historyImpl(params).sync(); - } - - /** - * Asynchronously obtain recent history for this channel using the REST API. - * @param params: the request params. See the Ably REST API - * @param callback - * @return - */ - public void historyAsync(Param[] params, Callback> callback) { - historyImpl(params).async(callback); - } - - private BasePaginatedQuery.ResultRequest historyImpl(Param[] params) { - HttpCore.BodyHandler bodyHandler = MessageSerializer.getMessageResponseHandler(options); - return (new BasePaginatedQuery(ably.http, basePath + "/messages", HttpUtils.defaultAcceptHeaders(ably.options.useBinaryProtocol), params, bodyHandler)).get(); - } - - /** - * A class enabling access to Channel Presence information via the REST API. - * Since the library is stateless, REST clients are therefore never present - * themselves. This API enables the service to be queried to determine - * presence state for other clients on this channel. - */ - public class Presence { - - /** - * Get the presence state for this Channel. - * @return: the current present members. - * @throws AblyException - */ - public PaginatedResult get(Param[] params) throws AblyException { - return getImpl(params).sync(); - } - - /** - * Asynchronously get the presence state for this Channel. - * @param callback: on success returns the currently present members. - */ - public void getAsync(Param[] params, Callback> callback) { - getImpl(params).async(callback); - } - - private BasePaginatedQuery.ResultRequest getImpl(Param[] params) { - HttpCore.BodyHandler bodyHandler = PresenceSerializer.getPresenceResponseHandler(options); - return (new BasePaginatedQuery(ably.http, basePath + "/presence", HttpUtils.defaultAcceptHeaders(ably.options.useBinaryProtocol), params, bodyHandler)).get(); - } - - /** - * Asynchronously obtain presence history for this channel using the REST API. - * The history provided relqtes to all clients of this application, - * not just this instance. - * @param params: the request params. See the Ably REST API - * documentation for more details. - */ - public PaginatedResult history(Param[] params) throws AblyException { - return historyImpl(params).sync(); - } - - /** - * Asynchronously obtain recent history for this channel using the REST API. - * @param params: the request params. See the Ably REST API - * @param callback - * @return - */ - public void historyAsync(Param[] params, Callback> callback) { - historyImpl(params).async(callback); - } - - private BasePaginatedQuery.ResultRequest historyImpl(Param[] params) { - HttpCore.BodyHandler bodyHandler = PresenceSerializer.getPresenceResponseHandler(options); - return (new BasePaginatedQuery(ably.http, basePath + "/presence/history", HttpUtils.defaultAcceptHeaders(ably.options.useBinaryProtocol), params, bodyHandler)).get(); - } - - } - - /****************** - * internal - * @throws AblyException - ******************/ - - ChannelBase(AblyBase ably, String name, ChannelOptions options) throws AblyException { - this.ably = ably; - this.name = name; - this.options = options; - this.basePath = "/channels/" + HttpUtils.encodeURIComponent(name); - this.presence = new Presence(); - } - - private final AblyBase ably; - private final String basePath; - ChannelOptions options; + /** + * The Channel name + */ + public final String name; + + /** + * The presence instance for this channel. + */ + public final Presence presence; + + /** + * Publish a message on this channel using the REST API. + * Since the REST API is stateless, this request is made independently + * of any other request on this or any other channel. + * @param name: the event name + * @param data: the message payload; see {@link io.ably.types.Data} for + * details of supported data types. + * @throws AblyException + */ + public void publish(String name, Object data) throws AblyException { + publishImpl(name, data).sync(); + } + + /** + * Publish a message on this channel using the REST API. + * Since the REST API is stateless, this request is made independently + * of any other request on this or any other channel. + * @param name: the event name + * @param data: the message payload; see {@link io.ably.types.Data} for + * @param listener + */ + public void publishAsync(String name, Object data, CompletionListener listener) { + publishImpl(name, data).async(new CompletionListener.ToCallback(listener)); + } + + private Http.Request publishImpl(String name, Object data) { + return publishImpl(new Message[] {new Message(name, data)}); + } + + /** + * Publish an array of messages on this channel. When there are + * multiple messages to be sent, it is more efficient to use this + * method to publish them in a single request, as compared with + * publishing via multiple independent requests. + * @param messages: array of messages to publish. + * @throws AblyException + */ + public void publish(final Message[] messages) throws AblyException { + publishImpl(messages).sync(); + } + + /** + * Asynchronously publish an array of messages on this channel + * @param messages + * @param listener + */ + public void publishAsync(final Message[] messages, final CompletionListener listener) { + publishImpl(messages).async(new CompletionListener.ToCallback(listener)); + } + + private Http.Request publishImpl(final Message[] messages) { + return ably.http.request(new Http.Execute() { + @Override + public void execute(HttpScheduler http, final Callback callback) throws AblyException { + /* handle message ids */ + boolean hasClientSuppliedId = false; + for(Message message : messages) { + /* RSL1k2 */ + hasClientSuppliedId |= (message.id != null); + /* RTL6g3 */ + ably.auth.checkClientId(message, true, false); + message.encode(options); + } + if(!hasClientSuppliedId && ably.options.idempotentRestPublishing) { + /* RSL1k1: populate the message id with a library-generated id */ + String messageId = Crypto.getRandomMessageId(); + for (int i = 0; i < messages.length; i++) { + messages[i].id = messageId + ':' + i; + } + } + + HttpCore.RequestBody requestBody = ably.options.useBinaryProtocol ? MessageSerializer.asMsgpackRequest(messages) : MessageSerializer.asJsonRequest(messages); + + http.post(basePath + "/messages", HttpUtils.defaultAcceptHeaders(ably.options.useBinaryProtocol), null, requestBody, null, true, callback); + } + }); + } + + /** + * Obtain recent history for this channel using the REST API. + * The history provided relqtes to all clients of this application, + * not just this instance. + * @param params: the request params. See the Ably REST API + * documentation for more details. + * @return: an array of Messages for this Channel. + * @throws AblyException + */ + public PaginatedResult history(Param[] params) throws AblyException { + return historyImpl(params).sync(); + } + + /** + * Asynchronously obtain recent history for this channel using the REST API. + * @param params: the request params. See the Ably REST API + * @param callback + * @return + */ + public void historyAsync(Param[] params, Callback> callback) { + historyImpl(params).async(callback); + } + + private BasePaginatedQuery.ResultRequest historyImpl(Param[] params) { + HttpCore.BodyHandler bodyHandler = MessageSerializer.getMessageResponseHandler(options); + return (new BasePaginatedQuery(ably.http, basePath + "/messages", HttpUtils.defaultAcceptHeaders(ably.options.useBinaryProtocol), params, bodyHandler)).get(); + } + + /** + * A class enabling access to Channel Presence information via the REST API. + * Since the library is stateless, REST clients are therefore never present + * themselves. This API enables the service to be queried to determine + * presence state for other clients on this channel. + */ + public class Presence { + + /** + * Get the presence state for this Channel. + * @return: the current present members. + * @throws AblyException + */ + public PaginatedResult get(Param[] params) throws AblyException { + return getImpl(params).sync(); + } + + /** + * Asynchronously get the presence state for this Channel. + * @param callback: on success returns the currently present members. + */ + public void getAsync(Param[] params, Callback> callback) { + getImpl(params).async(callback); + } + + private BasePaginatedQuery.ResultRequest getImpl(Param[] params) { + HttpCore.BodyHandler bodyHandler = PresenceSerializer.getPresenceResponseHandler(options); + return (new BasePaginatedQuery(ably.http, basePath + "/presence", HttpUtils.defaultAcceptHeaders(ably.options.useBinaryProtocol), params, bodyHandler)).get(); + } + + /** + * Asynchronously obtain presence history for this channel using the REST API. + * The history provided relqtes to all clients of this application, + * not just this instance. + * @param params: the request params. See the Ably REST API + * documentation for more details. + */ + public PaginatedResult history(Param[] params) throws AblyException { + return historyImpl(params).sync(); + } + + /** + * Asynchronously obtain recent history for this channel using the REST API. + * @param params: the request params. See the Ably REST API + * @param callback + * @return + */ + public void historyAsync(Param[] params, Callback> callback) { + historyImpl(params).async(callback); + } + + private BasePaginatedQuery.ResultRequest historyImpl(Param[] params) { + HttpCore.BodyHandler bodyHandler = PresenceSerializer.getPresenceResponseHandler(options); + return (new BasePaginatedQuery(ably.http, basePath + "/presence/history", HttpUtils.defaultAcceptHeaders(ably.options.useBinaryProtocol), params, bodyHandler)).get(); + } + + } + + /****************** + * internal + * @throws AblyException + ******************/ + + ChannelBase(AblyBase ably, String name, ChannelOptions options) throws AblyException { + this.ably = ably; + this.name = name; + this.options = options; + this.basePath = "/channels/" + HttpUtils.encodeURIComponent(name); + this.presence = new Presence(); + } + + private final AblyBase ably; + private final String basePath; + ChannelOptions options; } diff --git a/lib/src/main/java/io/ably/lib/rest/DeviceDetails.java b/lib/src/main/java/io/ably/lib/rest/DeviceDetails.java index 4bb401d58..c44b68956 100644 --- a/lib/src/main/java/io/ably/lib/rest/DeviceDetails.java +++ b/lib/src/main/java/io/ably/lib/rest/DeviceDetails.java @@ -10,129 +10,129 @@ import io.ably.lib.util.Serialisation; public class DeviceDetails { - public String id; - public String platform; - public String formFactor; - public String clientId; - public JsonObject metadata; - - public Push push; - - public static class Push { - public JsonObject recipient; - public State state; - public ErrorInfo errorReason; - - public JsonObject toJsonObject() { - JsonObject o = new JsonObject(); - - o.add("recipient", recipient); - - return o; - } - - public enum State { - ACTIVE("ACTIVE"), - FAILING("FAILING"), - FAILED("FAILED"); - - public String code; - State(String code) { - this.code = code; - } - - public int toInt() { - State[] values = State.values(); - for (int i = 0; i < values.length; i++) { - if (this == values[i]) { - return i; - } - } - return -1; - } - - public static State fromInt(int i) { - State[] values = State.values(); - if (i < 0 || i >= values.length) { - return null; - } - return values[i]; - } - - public static State fromCode(String code) { - State[] values = State.values(); - for (State t : values) { - if (t.code.equals(code)) { - return t; - } - } - return null; - } - } - } - - public JsonObject toJsonObject() { - JsonObject o = new JsonObject(); - - o.addProperty("id", id); - o.addProperty("platform", platform); - o.addProperty("formFactor", formFactor); - o.addProperty("clientId", clientId); - if (metadata != null) { - o.add("metadata", metadata); - } - if (push != null) { - o.add("push", push.toJsonObject()); - } - - return o; - } - - public JsonObject pushRecipientJsonObject() { - return JsonUtils.object() - .add("push", JsonUtils.object() - .add("recipient", this.push.recipient)).toJson(); - } - - @Override - public boolean equals(Object o) { - if (!(o instanceof DeviceDetails)) { - return false; - } - DeviceDetails other = (DeviceDetails) o; - JsonObject thisJson = this.toJsonObject(); - JsonObject otherJson = other.toJsonObject(); - - // Disregard device token - thisJson.remove("deviceSecret"); - otherJson.remove("deviceSecret"); - - if ((this.metadata == null || this.metadata.entrySet().isEmpty()) && (other.metadata == null || other.metadata.entrySet().isEmpty())) { - // Empty metadata == null metadata. - thisJson.remove("metadata"); - otherJson.remove("metadata"); - } - - return thisJson.equals(otherJson); - } - - @Override - public String toString() { - return this.toJsonObject().toString(); - } - - public static DeviceDetails fromJsonObject(JsonObject o) { - return Serialisation.gson.fromJson(o, DeviceDetails.class); - } - - private static Serialisation.FromJsonElement fromJsonElement = new Serialisation.FromJsonElement() { - @Override - public DeviceDetails fromJsonElement(JsonElement e) { - return fromJsonObject((JsonObject) e); - } - }; - - public static HttpCore.ResponseHandler httpResponseHandler = new Serialisation.HttpResponseHandler(DeviceDetails.class, fromJsonElement); - - public static HttpCore.BodyHandler httpBodyHandler = new Serialisation.HttpBodyHandler(DeviceDetails[].class, fromJsonElement); + public String id; + public String platform; + public String formFactor; + public String clientId; + public JsonObject metadata; + + public Push push; + + public static class Push { + public JsonObject recipient; + public State state; + public ErrorInfo errorReason; + + public JsonObject toJsonObject() { + JsonObject o = new JsonObject(); + + o.add("recipient", recipient); + + return o; + } + + public enum State { + ACTIVE("ACTIVE"), + FAILING("FAILING"), + FAILED("FAILED"); + + public String code; + State(String code) { + this.code = code; + } + + public int toInt() { + State[] values = State.values(); + for (int i = 0; i < values.length; i++) { + if (this == values[i]) { + return i; + } + } + return -1; + } + + public static State fromInt(int i) { + State[] values = State.values(); + if (i < 0 || i >= values.length) { + return null; + } + return values[i]; + } + + public static State fromCode(String code) { + State[] values = State.values(); + for (State t : values) { + if (t.code.equals(code)) { + return t; + } + } + return null; + } + } + } + + public JsonObject toJsonObject() { + JsonObject o = new JsonObject(); + + o.addProperty("id", id); + o.addProperty("platform", platform); + o.addProperty("formFactor", formFactor); + o.addProperty("clientId", clientId); + if (metadata != null) { + o.add("metadata", metadata); + } + if (push != null) { + o.add("push", push.toJsonObject()); + } + + return o; + } + + public JsonObject pushRecipientJsonObject() { + return JsonUtils.object() + .add("push", JsonUtils.object() + .add("recipient", this.push.recipient)).toJson(); + } + + @Override + public boolean equals(Object o) { + if (!(o instanceof DeviceDetails)) { + return false; + } + DeviceDetails other = (DeviceDetails) o; + JsonObject thisJson = this.toJsonObject(); + JsonObject otherJson = other.toJsonObject(); + + // Disregard device token + thisJson.remove("deviceSecret"); + otherJson.remove("deviceSecret"); + + if ((this.metadata == null || this.metadata.entrySet().isEmpty()) && (other.metadata == null || other.metadata.entrySet().isEmpty())) { + // Empty metadata == null metadata. + thisJson.remove("metadata"); + otherJson.remove("metadata"); + } + + return thisJson.equals(otherJson); + } + + @Override + public String toString() { + return this.toJsonObject().toString(); + } + + public static DeviceDetails fromJsonObject(JsonObject o) { + return Serialisation.gson.fromJson(o, DeviceDetails.class); + } + + private static Serialisation.FromJsonElement fromJsonElement = new Serialisation.FromJsonElement() { + @Override + public DeviceDetails fromJsonElement(JsonElement e) { + return fromJsonObject((JsonObject) e); + } + }; + + public static HttpCore.ResponseHandler httpResponseHandler = new Serialisation.HttpResponseHandler(DeviceDetails.class, fromJsonElement); + + public static HttpCore.BodyHandler httpBodyHandler = new Serialisation.HttpBodyHandler(DeviceDetails[].class, fromJsonElement); } diff --git a/lib/src/main/java/io/ably/lib/transport/ConnectionManager.java b/lib/src/main/java/io/ably/lib/transport/ConnectionManager.java index 92b7892ed..1f10e06ee 100644 --- a/lib/src/main/java/io/ably/lib/transport/ConnectionManager.java +++ b/lib/src/main/java/io/ably/lib/transport/ConnectionManager.java @@ -20,1661 +20,1661 @@ public class ConnectionManager implements ConnectListener { - /************************************************************** - * ConnectionManager - * - * This class is responsible for coordinating all actions that - * relate to transports and connection state. - * - * It comprises two principal parts: - * - An action queue, and a thread that performs those actions. - * Actions comprise connection state change requests, plus other - * actions that arise from transport state indications. An - * action Handler thread runs, except during idle times when - * there is no current or pending connection activity, that - * performs queued actions. - * - * - A state machine that represents the current connection state, - * and the possible transitions between states. - **************************************************************/ - - private static final String TAG = ConnectionManager.class.getName(); - private static final String INTERNET_CHECK_URL = "https://internet-up.ably-realtime.com/is-the-internet-up.txt"; - private static final String INTERNET_CHECK_OK = "yes"; - - /*********************************** - * default errors - ***********************************/ - - static ErrorInfo REASON_CLOSED = new ErrorInfo("Connection closed by client", 200, 10000); - static ErrorInfo REASON_DISCONNECTED = new ErrorInfo("Connection temporarily unavailable", 503, 80003); - static ErrorInfo REASON_SUSPENDED = new ErrorInfo("Connection unavailable", 503, 80002); - static ErrorInfo REASON_FAILED = new ErrorInfo("Connection failed", 400, 80000); - static ErrorInfo REASON_REFUSED = new ErrorInfo("Access refused", 401, 40100); - static ErrorInfo REASON_TOO_BIG = new ErrorInfo("Connection closed; message too large", 400, 40000); - - /** - * Methods on the channels map owned by the {@link AblyRealtime} instance - * which the {@link ConnectionManager} needs access to. - */ - public interface Channels { - void onMessage(ProtocolMessage msg); - void suspendAll(ErrorInfo error, boolean notifyStateChange); - Iterable values(); - } - - /*********************************** - * a class encapsulating information - * associated with a currentState change - * request or notification - ***********************************/ - - public static class StateIndication { - final ConnectionState state; - final ErrorInfo reason; - final String fallback; - final String currentHost; - - StateIndication(ConnectionState state) { - this(state, null); - } - - public StateIndication(ConnectionState state, ErrorInfo reason) { - this(state, reason, null, null); - } - - StateIndication(ConnectionState state, ErrorInfo reason, String fallback, String currentHost) { - this.state = state; - this.reason = reason; - this.fallback = fallback; - this.currentHost = currentHost; - } - } - - /************************************* - * a class encapsulating state machine - * information for a given state - *************************************/ - - public abstract class State { - public final ConnectionState state; - public final ErrorInfo defaultErrorInfo; - public final boolean queueEvents; - public final boolean sendEvents; - - final boolean terminal; - public final long timeout; - - State(ConnectionState state, boolean queueEvents, boolean sendEvents, boolean terminal, long timeout, ErrorInfo defaultErrorInfo) { - this.state = state; - this.queueEvents = queueEvents; - this.sendEvents = sendEvents; - this.terminal = terminal; - this.timeout = timeout; - this.defaultErrorInfo = defaultErrorInfo; - } - - /** - * Called on the current state to determine the response to a - * give state change request. - * @param target: the state change request or event - * @return StateIndication result: the determined response to - * the request with the required state transition, if any. A - * null result indicates that there is no resulting transition. - */ - abstract StateIndication validateTransition(StateIndication target); - - /** - * Called when the timeout occurs for the current state. - * @return StateIndication result: the determined response to - * the timeout with the required state transition, if any. A - * null result indicates that there is no resulting transition. - */ - StateIndication onTimeout() { - return null; - } - - /** - * Perform a transition to this state. - * @param stateIndication: the transition request that triggered this transition - * @param change: the change event corresponding to this transition. - */ - void enact(StateIndication stateIndication, ConnectionStateChange change) { - if(change != null) { - /* if now connected, send queued messages, etc */ - if(sendEvents) { - sendQueuedMessages(); - } else if(!queueEvents) { - failQueuedMessages(stateIndication.reason); - } - for(final Channel channel : channels.values()) { - enactForChannel(stateIndication, change, channel); - } - } - } - - /** - * Perform a transition to this state for a given channel. - * @param stateIndication: the transition request that triggered this transition - * @param change: the change event corresponding to this transition. - * @param channel: the channel - */ - void enactForChannel(StateIndication stateIndication, ConnectionStateChange change, Channel channel) {} - } - - /************************************************** - * Initialized: the initial state - **************************************************/ - - class Initialized extends State { - Initialized() { - super(ConnectionState.initialized, true, false, false, 0, null); - } - - @Override - StateIndication validateTransition(StateIndication target) { - /* we can transition to any other state, other than ourselves */ - if(target.state == this.state) { - return null; - } - return target; - } - } - - /************************************************** - * Connecting: a connection attempt is in progress - **************************************************/ - - class Connecting extends State { - Connecting() { - super(ConnectionState.connecting, true, false, false, Defaults.TIMEOUT_CONNECT, null); - } - - @Override - StateIndication onTimeout() { - return checkSuspended(null); - } - - @Override - StateIndication validateTransition(StateIndication target) { - /* we can transition to any other state */ - return target; - } - - @Override - void enact(StateIndication stateIndication, ConnectionStateChange change) { - super.enact(stateIndication, change); - connectImpl(stateIndication); - } - } - - /************************************************** - * Connected: a connection is established - **************************************************/ - - class Connected extends State { - Connected() { - super(ConnectionState.connected, false, true, false, 0, null); - } - - @Override - StateIndication validateTransition(StateIndication target) { - if(target.state == this.state) { - /* RTN24: no currentState change, so no transition, required, but there will be an update event; - * connected is special case because we want to deliver reauth notifications to listeners as an update */ - addAction(new UpdateAction(null)); - return null; - } - - /* we can transition to any other state */ - return target; - } - - @Override - void enactForChannel(StateIndication stateIndication, ConnectionStateChange change, Channel channel) { - channel.setConnected(); - } - } - - /************************************************** - * Disconnected: no connection is established, but - * a reconnection attempt will be made on timer - * expiry, anticipating preservation of connection - * state on reconnection - **************************************************/ - - class Disconnected extends State { - Disconnected() { - super(ConnectionState.disconnected, true, false, false, Defaults.TIMEOUT_DISCONNECT, REASON_DISCONNECTED); - } - - @Override - StateIndication validateTransition(StateIndication target) { - /* we can't transition to ourselves */ - if(target.state == this.state) { - return null; - } - /* a closing event will transition directly to closed */ - if(target.state == ConnectionState.closing) { - return new StateIndication(ConnectionState.closed); - } - /* otherwise, the transition is valid */ - return target; - } - - @Override - StateIndication onTimeout() { - return new StateIndication(ConnectionState.connecting); - } - - @Override - void enactForChannel(StateIndication stateIndication, ConnectionStateChange change, Channel channel) { - /* (RTL3e) If the connection currentState enters the - * DISCONNECTED currentState, it will have no effect on the - * channel states. */ - } - - @Override - void enact(StateIndication stateIndication, ConnectionStateChange change) { - super.enact(stateIndication, change); - clearTransport(); - if(change.previous == ConnectionState.connected) { - setSuspendTime(); - /* we were connected, so retry immediately */ - if(!suppressRetry) { - requestState(ConnectionState.connecting); - } - } - } - } - - /************************************************** - * Suspended: no connection is established. A - * reconnection attempt will be made on timer expiry - * but there will be no continuity of connection - * state on reconnection - **************************************************/ - - class Suspended extends State { - Suspended() { - super(ConnectionState.suspended, false, false, false, Defaults.connectionStateTtl, REASON_SUSPENDED); - } - - @Override - StateIndication validateTransition(StateIndication target) { - /* we can't transition to ourselves */ - if(target.state == this.state) { - return null; - } - /* a closing event will transition directly to closed */ - if(target.state == ConnectionState.closing) { - return new StateIndication(ConnectionState.closed); - } - /* otherwise, the transition is valid */ - return target; - } - - @Override - StateIndication onTimeout() { - return new StateIndication(ConnectionState.connecting); - } - - @Override - void enactForChannel(StateIndication stateIndication, ConnectionStateChange change, Channel channel) { - /* (RTL3c) If the connection currentState enters the SUSPENDED - * currentState, then an ATTACHING or ATTACHED channel currentState - * will transition to SUSPENDED. */ - channel.setSuspended(defaultErrorInfo, true); - } - } - - /************************************************** - * Closing: a close sequence is in progress - **************************************************/ - - class Closing extends State { - Closing() { - super(ConnectionState.closing, false, false, false, Defaults.TIMEOUT_CONNECT, REASON_CLOSED); - } - - @Override - StateIndication validateTransition(StateIndication target) { - /* we can't transition to ourselves */ - if(target.state == this.state) { - return null; - } - /* any disconnection event will transition directly to closed */ - if(target.state == ConnectionState.disconnected || target.state == ConnectionState.suspended) { - return new StateIndication(ConnectionState.closed); - } - /* otherwise, the transition is valid */ - return target; - } - - @Override - StateIndication onTimeout() { - return new StateIndication(ConnectionState.closed); - } - - @Override - void enact(StateIndication stateIndication, ConnectionStateChange change) { - super.enact(stateIndication, change); - boolean closed = closeImpl(); - if(closed) { - addAction(new AsynchronousStateChangeAction(ConnectionState.closed)); - } - } - } - - /************************************************** - * Closed: the connection is closed, and no - * reconnection attempt will be made unless - * explicitly requested - **************************************************/ - - class Closed extends State { - Closed() { - super(ConnectionState.closed, false, false, true, 0, REASON_CLOSED); - } - - @Override - StateIndication validateTransition(StateIndication target) { - /* we only leave the closed state via a connection attempt */ - if(target.state == ConnectionState.connecting) { - return target; - } - /* otherwise, the transition is not valid */ - return null; - } - - @Override - void enactForChannel(StateIndication stateIndication, ConnectionStateChange change, Channel channel) { - /* (RTL3b) If the connection currentState enters the CLOSED - * currentState, then an ATTACHING or ATTACHED channel currentState - * will transition to DETACHED. */ - channel.setConnectionClosed(REASON_CLOSED); - } - } - - /************************************************** - * Failed: there is no connection, and there has - * been an error, either in options validation or - * as a response to a connection attempt, that - * implies no new connection attempt will succeed. - * No reconnection attempt will be made unless - * explicitly requested - **************************************************/ - - class Failed extends State { - Failed() { - super(ConnectionState.failed, false, false, true, 0, REASON_FAILED); - } - - @Override - StateIndication validateTransition(StateIndication target) { - /* we only leave the failed state via a connection attempt */ - if(target.state == ConnectionState.connecting) { - return target; - } - /* otherwise, the transition is not valid */ - return null; - } - - @Override - void enactForChannel(StateIndication stateIndication, ConnectionStateChange change, Channel channel) { - /* (RTL3a) If the connection currentState enters the FAILED - * currentState, then an ATTACHING or ATTACHED channel currentState - * will transition to FAILED, set the - * Channel#errorReason and emit the error event. */ - channel.setConnectionFailed(stateIndication.reason); - } - - @Override - void enact(StateIndication stateIndication, ConnectionStateChange change) { - super.enact(stateIndication, change); - clearTransport(); - } - } - - public ErrorInfo getStateErrorInfo() { - return stateError != null ? stateError : currentState.defaultErrorInfo; - } - - public boolean isActive() { - return currentState.queueEvents || currentState.sendEvents; - } - - /************************************* - * a class that listens for currentState change - * events for in-place authorization - *************************************/ - - private class ConnectionWaiter implements ConnectionStateListener { - private ConnectionStateChange change; - - private ConnectionWaiter() { - connection.on(this); - } - - /** - * Wait for a currentState change notification - */ - private synchronized ErrorInfo waitForChange() { - Log.d(TAG, "ConnectionWaiter.waitFor()"); - if (change == null) { - try { wait(); } catch(InterruptedException e) {} - } - Log.d(TAG, "ConnectionWaiter.waitFor done: currentState=" + currentState + ")"); - ErrorInfo reason = change.reason; - change = null; - return reason; - } - - /** - * ConnectionStateListener interface - */ - @Override - public void onConnectionStateChanged(ConnectionStateChange state) { - synchronized(this) { - change = state; - notify(); - } - } - } - - /*********************** - * Actions - ***********************/ - - /** - * A class that encapsulates actions to perform by the ConnectionManager - */ - private interface Action extends Runnable {} - - /** - * An class that performs a state transition - */ - private abstract class StateChangeAction { - - protected final ITransport transport; - protected final StateIndication stateIndication; - protected ConnectionStateChange change; - - StateChangeAction(ITransport transport, StateIndication stateIndication) { - this.transport = transport; - this.stateIndication = stateIndication; - } - - /** - * Make the change to the ConnectionManager currentState represented by this Action - */ - protected void setState() { - change = ConnectionManager.this.setState(transport, stateIndication); - } - - protected void enactState() { - if(change != null) { - if(change.current != change.previous) { - /* broadcast currentState change */ - connection.onConnectionStateChange(change); - } - - /* implement the state change */ - states.get(stateIndication.state).enact(stateIndication, change); - if(currentState.terminal) { - clearTransport(); - } - } - } - } - - /** - * An Action that enacts a state transition, making the ConnectionManager state change - * synchronously. This is for instances such as any transition away from the connected - * state, where the state is updated synchronously with the transport state change. - * This ensures that there is no possibility of an attempt to send on the transport - * after it has indicated that it is not available. - */ - private class SynchronousStateChangeAction extends StateChangeAction implements Action { - SynchronousStateChangeAction(ITransport transport, StateIndication stateIndication) { - super(transport, stateIndication); - setState(); - } - - @Override - public void run() { - enactState(); - } - } - - /** - * An Action that enacts a state transition, making the ConnectionManager state change - * asynchronously. This applies to all transitions that are not transitions away from - * the connected state. - */ - private class AsynchronousStateChangeAction extends StateChangeAction implements Action{ - AsynchronousStateChangeAction(ConnectionState state) { - super(null, new StateIndication(state, null)); - } - - AsynchronousStateChangeAction(ITransport transport, StateIndication stateIndication) { - super(transport, stateIndication); - } - - @Override - public void run() { - setState(); - enactState(); - } - } - - /** - * An Action that performs an inband reauthorisation - */ - private class ReauthAction implements Action { - @Override - public void run() { - handleReauth(); - } - } - - /** - * An Action that handles dissemination of update events arising from a - * connected -> connected transition - */ - private class UpdateAction implements Action { - private final ErrorInfo reason; - - UpdateAction(ErrorInfo reason) { - this.reason = reason; - } - - @Override - public void run() { - connection.emitUpdate(reason); - } - } - - /** - * A queue of Actions awaiting processing - */ - private static class ActionQueue extends ArrayDeque { - public synchronized boolean add(Action action) { - return super.add(action); - } - - public synchronized Action poll() { - return super.poll(); - } - - public synchronized Action peek() { - return super.peek(); - } - - public synchronized int size() { - return super.size(); - } - } - - /** - * Append an action to the pending action queue - * @param action: the action - */ - private synchronized void addAction(Action action) { - actionQueue.add(action); - notifyAll(); - } - - /** - * A handler that runs in a dedicated Thread that processes queued actions - */ - class ActionHandler implements Runnable { - - public void run() { - while(true) { - /* - * Until we're committed to exit we: - * - wait for an action or timeout - * - given an action, perform the action asynchronously; - * - if a timeout, perform the timeout state transition - */ - - /* Hold the lock until we obtain an action */ - synchronized(ConnectionManager.this) { - while(actionQueue.size() == 0) { - /* if we're in a terminal state, then this thread is done */ - if(currentState.terminal) { - /* indicate that this thread is committed to die */ - handlerThread = null; - stopConnectivityListener(); - return; - } - - /* wait for an action event or for expiry of the current currentState */ - tryWait(currentState.timeout); - - /* if during the wait some action was requested, handle it */ - Action act = actionQueue.peek(); - if (act != null) { - Log.d(TAG, "Wait ended by action: " + act.toString()); - break; - } - - /* if our currentState wants us to retry on timer expiry, do that */ - if (!suppressRetry) { - StateIndication nextState = currentState.onTimeout(); - if (nextState != null) { - requestState(nextState); - } - } - } - } - - /* perform outstanding actions, without the ConnectionManager locked */ - Action deferredAction; - while((deferredAction = actionQueue.poll()) != null) { - try { - deferredAction.run(); - } catch(Exception e) { - Log.e(TAG, "Action invocation failed with exception: action = " + deferredAction.toString(), e); - } - } - } - } - } - - /*********************** - * ConnectionManager - ***********************/ - - public ConnectionManager(final AblyRealtime ably, final Connection connection, final Channels channels) throws AblyException { - this.ably = ably; - this.connection = connection; - this.channels = channels; - - ClientOptions options = ably.options; - this.hosts = new Hosts(options.realtimeHost, Defaults.HOST_REALTIME, options); - - /* debug options */ - ITransport.Factory transportFactory = null; - RawProtocolListener protocolListener = null; - if(options instanceof DebugOptions) { - protocolListener = ((DebugOptions) options).protocolListener; - transportFactory = ((DebugOptions) options).transportFactory; - } - this.protocolListener = protocolListener; - this.transportFactory = (transportFactory != null) ? transportFactory : Defaults.TRANSPORT; - - /* construct all states */ - states.put(ConnectionState.initialized, new Initialized()); - states.put(ConnectionState.connecting, new Connecting()); - states.put(ConnectionState.connected, new Connected()); - states.put(ConnectionState.disconnected, new Disconnected()); - states.put(ConnectionState.suspended, new Suspended()); - states.put(ConnectionState.closing, new Closing()); - states.put(ConnectionState.closed, new Closed()); - states.put(ConnectionState.failed, new Failed()); - - currentState = states.get(ConnectionState.initialized); - - setSuspendTime(); - } - - /********************* - * host management - *********************/ - - /* This is only here for the benefit of ConnectionManagerTest. */ - public String getHost() { - return lastUsedHost; - } - - /********************* - * states API - *********************/ - - public synchronized State getConnectionState() { - return currentState; - } - - public synchronized void connect() { - /* connect() is the only action that will bring the ConnectionManager out of a terminal currentState */ - if(currentState.terminal || currentState.state == ConnectionState.initialized) { - startup(); - } - requestState(ConnectionState.connecting); - } - - public void close() { - requestState(ConnectionState.closing); - } - - public void requestState(ConnectionState state) { - requestState(new StateIndication(state, null)); - } - - public void requestState(StateIndication state) { - requestState(null, state); - } - - private synchronized void requestState(ITransport transport, StateIndication stateIndication) { - Log.v(TAG, "requestState(): requesting " + stateIndication.state + "; id = " + connection.id); - addAction(new AsynchronousStateChangeAction(transport, stateIndication)); - } - - private synchronized ConnectionStateChange setState(ITransport transport, StateIndication stateIndication) { - /* check validity of transport */ - if (transport != null && transport != this.transport) { - Log.v(TAG, "setState: action received for superseded transport; discarding"); - return null; - } - - /* check validity of transition */ - StateIndication validatedStateIndication = currentState.validateTransition(stateIndication); - if (validatedStateIndication == null) { - Log.v(TAG, "setState(): not transitioning; not a valid transition " + stateIndication.state); - return null; - } - - /* update currentState */ - ConnectionState newConnectionState = validatedStateIndication.state; - State newState = states.get(newConnectionState); - ErrorInfo reason = validatedStateIndication.reason; - if (reason == null) { - reason = newState.defaultErrorInfo; - } - Log.v(TAG, "setState(): setting " + newState.state + "; reason " + reason); - ConnectionStateChange change = new ConnectionStateChange(currentState.state, newConnectionState, newState.timeout, reason); - currentState = newState; - stateError = reason; - - return change; - } - - /********************* - * ping API - *********************/ - - public void ping(final CompletionListener listener) { - HeartbeatWaiter waiter = new HeartbeatWaiter(listener); - if(currentState.state != ConnectionState.connected) { - waiter.onError(new ErrorInfo("Unable to ping service; not connected", 40000, 400)); - return; - } - synchronized(heartbeatWaiters) { - heartbeatWaiters.add(waiter); - waiter.start(); - } - try { - send(new ProtocolMessage(ProtocolMessage.Action.heartbeat), false, null); - } catch (AblyException e) { - waiter.onError(e.errorInfo); - } - } - - /** - * A thread that waits for completion of a ping - */ - private class HeartbeatWaiter extends Thread { - private final CompletionListener listener; - - HeartbeatWaiter(CompletionListener listener) { - this.listener = listener; - } - - private void onSuccess() { - clear(); - if(listener != null) { - listener.onSuccess(); - } - } - - private void onError(ErrorInfo reason) { - clear(); - if(listener != null) { - listener.onError(reason); - } - } - - private boolean clear() { - boolean pending = heartbeatWaiters.remove(this); - if(pending) { - interrupt(); - } - return pending; - } - - @Override - public void run() { - boolean pending; - synchronized(heartbeatWaiters) { - try { - heartbeatWaiters.wait(HEARTBEAT_TIMEOUT); - } catch (InterruptedException ie) { - } - pending = clear(); - } - if(pending) { - onError(new ErrorInfo("Timed out waiting for heartbeat response", 50000, 500)); - } else { - onSuccess(); - } - } - } - - /*************************************** - * auth event handling - ***************************************/ - - /** - * (RTC8) For a realtime client, Auth.authorize instructs the library to - * obtain a token using the provided tokenParams and authOptions and upgrade - * the current connection to use that token; or if not currently connected, - * to connect with the token. - */ - public void onAuthUpdated(String token, boolean waitForResponse) throws AblyException { - ConnectionWaiter waiter = new ConnectionWaiter(); - switch(currentState.state) { - case connected: - /* (RTC8a) If the connection is in the CONNECTED currentState and - * auth.authorize is called or Ably requests a re-authentication - * (see RTN22), the client must obtain a new token, then send an - * AUTH ProtocolMessage to Ably with an auth attribute - * containing an AuthDetails object with the token string. */ - try { - ProtocolMessage msg = new ProtocolMessage(ProtocolMessage.Action.auth); - msg.auth = new ProtocolMessage.AuthDetails(token); - send(msg, false, null); - } catch (AblyException e) { - /* The send failed. Close the transport; if a subsequent - * reconnect succeeds, it will be with the new token. */ - Log.v(TAG, "onAuthUpdated: closing transport after send failure"); - transport.close(); - } - break; - - case connecting: - /* Close the connecting transport. */ - Log.v(TAG, "onAuthUpdated: closing connecting transport"); - ErrorInfo disconnectError = new ErrorInfo("Aborting incomplete connection with superseded auth params", 503, 80003); - requestState(new StateIndication(ConnectionState.disconnected, disconnectError, null, null)); - /* Start a new connection attempt. */ - connect(); - break; - - default: - /* Start a new connection attempt. */ - connect(); - break; - } - - if(!waitForResponse) { - return; - } - - /* Wait for a currentState transition into anything other than connecting or - * disconnected. Note that this includes the case that the connection - * was already connected, and the AUTH message prompted the server to - * send another connected message. */ - for (;;) { - ErrorInfo reason = waiter.waitForChange(); - switch (currentState.state) { - case connected: - Log.v(TAG, "onAuthUpdated: got connected"); - return; - case connecting: - case disconnected: - continue; - default: - /* suspended/closed/error: throw the error. */ - Log.v(TAG, "onAuthUpdated: throwing exception"); - throw AblyException.fromErrorInfo(reason); - } - } - } - - /** - * Called when where was an error during authentication attempt - * - * @param errorInfo Error associated with unsuccessful authentication - */ - public void onAuthError(ErrorInfo errorInfo) { - Log.i(TAG, String.format("onAuthError: (%d) %s", errorInfo.code, errorInfo.message)); - switch (currentState.state) { - case connecting: - ITransport transport = this.transport; - if (transport != null) - /* request that the current transport is closed */ - requestState(new StateIndication(ConnectionState.disconnected, errorInfo)); - break; - - case connected: - /* stay connected but notify of authentication error */ - addAction(new UpdateAction(errorInfo)); - break; - - default: - break; - } - } - - /*************************************** - * transport events/notifications - ***************************************/ - - /** - * React on message from the transport - * @param transport transport instance or null to bypass transport correctness check (for testing) - * @param message - * @throws AblyException - */ - public void onMessage(ITransport transport, ProtocolMessage message) throws AblyException { - if (transport != null && this.transport != transport) { - return; - } - if (Log.level <= Log.VERBOSE) { - Log.v(TAG, "onMessage() (transport = " + transport + "): " + message.action + ": " + new String(ProtocolSerializer.writeJSON(message))); - } - try { - if(protocolListener != null) { - protocolListener.onRawMessageRecv(message); - } - switch(message.action) { - case heartbeat: - onHeartbeat(message); - break; - case error: - ErrorInfo reason = message.error; - if(reason == null) { - Log.e(TAG, "onMessage(): ERROR message received (no error detail)"); - } else { - Log.e(TAG, "onMessage(): ERROR message received; message = " + reason.message + "; code = " + reason.code); - } - - /* an error message may signify an error currentState in a channel, or in the connection */ - if(message.channel != null) { - onChannelMessage(message); - } else { - onError(message); - } - break; - case connected: - onConnected(message); - break; - case disconnect: - case disconnected: - onDisconnected(message); - break; - case closed: - onClosed(message); - break; - case ack: - onAck(message); - break; - case nack: - onNack(message); - break; - case auth: - addAction(new ReauthAction()); - break; - default: - onChannelMessage(message); - } - } - catch(Exception e) { - // Prevent any non-AblyException to be thrown - throw AblyException.fromThrowable(e); - } - } - - private void onChannelMessage(ProtocolMessage message) { - if(message.connectionSerial != null) { - connection.serial = message.connectionSerial.longValue(); - if (connection.key != null) - connection.recoveryKey = connection.key + ":" + message.connectionSerial; - } - channels.onMessage(message); - } - - private synchronized void onConnected(ProtocolMessage message) { - /* if the returned connection id differs from - * the existing connection id, then this means - * we need to suspend all existing attachments to - * the old connection. - * If realtime did not reply with an error, it - * signifies that this was a result of an earlier - * connection being invalidated due to being stale. - * - * Suspend all channels attached to the previous id; - * this will be reattached in setConnection() */ - ErrorInfo error = message.error; - if(connection.id != null && !message.connectionId.equals(connection.id)) { - /* we need to suspend the original connection */ - if(error == null) { - error = REASON_SUSPENDED; - } - channels.suspendAll(error, false); - } - - /* set the new connection id */ - ConnectionDetails connectionDetails = message.connectionDetails; - connection.key = connectionDetails.connectionKey; - if (!message.connectionId.equals(connection.id)) { - /* The connection id has changed. Reset the message serial and the - * pending message queue (which fails the messages currently in - * there). */ - pendingMessages.reset(msgSerial, - new ErrorInfo("Connection resume failed", 500, 50000)); - msgSerial = 0; - } - connection.id = message.connectionId; - if(message.connectionSerial != null) { - connection.serial = message.connectionSerial.longValue(); - if (connection.key != null) - connection.recoveryKey = connection.key + ":" + message.connectionSerial; - } - - /* Get any parameters from connectionDetails. */ - maxIdleInterval = connectionDetails.maxIdleInterval; - connectionStateTtl = connectionDetails.connectionStateTtl; - - /* set the clientId resolved from token, if any */ - String clientId = connectionDetails.clientId; - try { - ably.auth.setClientId(clientId); - } catch (AblyException e) { - requestState(transport, new StateIndication(ConnectionState.failed, e.errorInfo)); - return; - } - - /* indicated connected currentState */ - setSuspendTime(); - requestState(new StateIndication(ConnectionState.connected, error)); - } - - private synchronized void onDisconnected(ProtocolMessage message) { - ErrorInfo reason = message.error; - if(reason != null && isTokenError(reason)) { - ably.auth.onAuthError(reason); - } - requestState(new StateIndication(ConnectionState.disconnected, reason)); - } - - private synchronized void onClosed(ProtocolMessage message) { - if(message.error != null) { - this.onError(message); - } else { - connection.key = null; - requestState(new StateIndication(ConnectionState.closed, null)); - } - } - - private synchronized void onError(ProtocolMessage message) { - connection.key = null; - ErrorInfo reason = message.error; - if(isTokenError(reason)) { - ably.auth.onAuthError(reason); - } - ConnectionState destinationState = isFatalError(reason) ? ConnectionState.failed : ConnectionState.disconnected; - requestState(transport, new StateIndication(destinationState, reason)); - } - - private void onAck(ProtocolMessage message) { - pendingMessages.ack(message.msgSerial, message.count, message.error); - } - - private void onNack(ProtocolMessage message) { - pendingMessages.nack(message.msgSerial, message.count, message.error); - } - - private void onHeartbeat(ProtocolMessage message) { - synchronized(heartbeatWaiters) { - heartbeatWaiters.clear(); - heartbeatWaiters.notifyAll(); - } - } - - /****************************** - * ConnectionManager lifecycle - ******************************/ - - private synchronized void startup() { - if(handlerThread == null) { - (handlerThread = new Thread(new ActionHandler())).start(); - startConnectivityListener(); - } - } - - private boolean checkConnectionStale() { - if(lastActivity == 0) { - return false; - } - long now = System.currentTimeMillis(); - long intervalSinceLastActivity = now - lastActivity; - if(intervalSinceLastActivity > (maxIdleInterval + connectionStateTtl)) { - /* RTN15g1, RTN15g2 Force a new connection if the previous one is stale; - * Clearing connection.key will ensure that we don't attempt to resume; - * leaving the original connection.id will mean that we notice at - * connection time that the connectionId has changed */ - if(connection.key != null) { - Log.v(TAG, "Clearing stale connection key to suppress resume"); - connection.key = null; - connection.recoveryKey = null; - } - return true; - } - return false; - } - - private synchronized void setSuspendTime() { - suspendTime = (System.currentTimeMillis() + connectionStateTtl); - } - - /** - * After a connection attempt failed, check to - * see whether we should attempt to use a fallback. - * @param reason - * @return StateIndication if a fallback connection attempt is required, otherwise null - */ - private StateIndication checkFallback(ErrorInfo reason) { - if(pendingConnect != null && (reason == null || reason.statusCode >= 500)) { - if (checkConnectivity()) { - /* we will try a fallback host */ - String hostFallback = hosts.getFallback(pendingConnect.host); - if (hostFallback != null) { - Log.v(TAG, "checkFallback: fallback to " + hostFallback); - return new StateIndication(ConnectionState.connecting, null, hostFallback, pendingConnect.host); - } - } - } - pendingConnect = null; - return null; - } - - private synchronized StateIndication checkSuspended(ErrorInfo reason) { - long currentTime = System.currentTimeMillis(); - long timeToSuspend = suspendTime - currentTime; - boolean suspendMode = timeToSuspend <= 0; - Log.v(TAG, "checkSuspended: timeToSuspend = " + timeToSuspend + "ms; suspendMode = " + suspendMode); - ConnectionState expiredState = suspendMode ? ConnectionState.suspended : ConnectionState.disconnected; - return new StateIndication(expiredState, reason); - } - - private void tryWait(long timeout) { - try { - if(timeout == 0) { - wait(); - } else { - wait(timeout); - } - } catch (InterruptedException e) {} - } - - private void handleReauth() { - if (currentState.state == ConnectionState.connected) { - Log.v(TAG, "Server initiated reauth"); - - ErrorInfo errorInfo = null; - - /* - * It is a server initiated reauth, it is issued while previous token is still valid for ~30 seconds, - * we have to clear cached token and get a new one - */ - try { - ably.auth.renew(); - } catch (AblyException e) { - errorInfo = e.errorInfo; - } - - /* report connection currentState in UPDATE event */ - if (currentState.state == ConnectionState.connected) { - connection.emitUpdate(errorInfo); - } - } - } - - @Override - public synchronized void onTransportAvailable(ITransport transport) { - if (this.transport != transport) { - /* This is from a transport that we have already abandoned. */ - Log.v(TAG, "onTransportAvailable: ignoring connection event from superseded transport"); - return; - } - if(protocolListener != null) { - protocolListener.onRawConnect(transport.getURL()); - } - } - - @Override - public synchronized void onTransportUnavailable(ITransport transport, ErrorInfo reason) { - if (this.transport != transport) { - /* This is from a transport that we have already abandoned. */ - Log.v(TAG, "onTransportUnavailable: ignoring disconnection event from superseded transport"); - return; - } - - /* if this is a failure of a pending connection attempt, decide whether or not to attempt a fallback host */ - StateIndication fallbackAttempt = checkFallback(reason); - if(fallbackAttempt != null) { - requestState(fallbackAttempt); - return; - } - - StateIndication stateIndication = null; - if(reason != null) { - if(isFatalError(reason)) { - Log.e(TAG, "onTransportUnavailable: unexpected transport error: " + reason.message); - stateIndication = new StateIndication(ConnectionState.failed, reason); - } else if(isTokenError(reason)) { - ably.auth.onAuthError(reason); - } - } - if(stateIndication == null) { - stateIndication = checkSuspended(reason); - } - addAction(new SynchronousStateChangeAction(transport, stateIndication)); - } - - private class ConnectParams extends TransportParams { - ConnectParams(ClientOptions options) { - super(options); - this.connectionKey = connection.key; - this.connectionSerial = String.valueOf(connection.serial); - this.port = Defaults.getPort(options); - } - } - - private void connectImpl(StateIndication request) { - /* determine the parameters of this connection attempt, and - * instance the transport. - * First, choose the transport. (Right now there's only one.) - * Second, choose the host. ConnectParams will use the default - * (or requested) host, unless fallback!=null, in which case - * checkSuspend has already chosen a fallback host at random */ - - String host = request.fallback; - if (host == null) { - host = hosts.getPreferredHost(); - } - checkConnectionStale(); - pendingConnect = new ConnectParams(ably.options); - pendingConnect.host = host; - lastUsedHost = host; - - /* try the connection */ - ITransport transport; - try { - transport = transportFactory.getTransport(pendingConnect, this); - } catch(Exception e) { - String msg = "Unable to instance transport class"; - Log.e(getClass().getName(), msg, e); - throw new RuntimeException(msg, e); - } - ITransport oldTransport; - synchronized(this) { - oldTransport = this.transport; - this.transport = transport; - } - if (oldTransport != null) { - oldTransport.close(); - } - transport.connect(this); - if(protocolListener != null) { - protocolListener.onRawConnectRequested(transport.getURL()); - } - } - - /** - * Close any existing transport - * @return closed if true, otherwise awaiting closed indication - */ - private boolean closeImpl() { - if(transport == null) { - return true; - } - - /* if connected, send an explicit close message and await response */ - boolean isConnected = currentState.state == ConnectionState.connected; - if(isConnected) { - try { - Log.v(TAG, "Requesting connection close"); - transport.send(new ProtocolMessage(ProtocolMessage.Action.close)); - return false; - } catch (AblyException e) { - /* we're closing, and the attempt to send the CLOSE message failed; - * continue, because we're not going to reinstate the transport - * just to send a CLOSE message */ - } - } - - /* just close the transport */ - Log.v(TAG, "Closing incomplete transport"); - clearTransport(); - return true; - } - - private void clearTransport() { - if(transport != null) { - transport.close(); - transport = null; - } - } - - /** - * Determine whether or not the client has connection to the network - * without reference to a specific ably host. This is to determine whether - * it is better to try a fallback host, or keep retrying with the default - * host. - * @return boolean, true if network is available - */ - protected boolean checkConnectivity() { - try { - return HttpHelpers.getUrlString(ably.httpCore, INTERNET_CHECK_URL).contains(INTERNET_CHECK_OK); - } catch(AblyException e) { - return false; - } - } - - protected void setLastActivity(long lastActivityTime) { - this.lastActivity = lastActivityTime; - } - - /****************** - * event queueing - ******************/ - - public static class QueuedMessage { - public final ProtocolMessage msg; - public final CompletionListener listener; - public QueuedMessage(ProtocolMessage msg, CompletionListener listener) { - this.msg = msg; - this.listener = listener; - } - } - - public void send(ProtocolMessage msg, boolean queueEvents, CompletionListener listener) throws AblyException { - State state; - synchronized(this) { - state = this.currentState; - if(state.sendEvents) { - sendImpl(msg, listener); - return; - } - if(state.queueEvents && queueEvents) { - queuedMessages.add(new QueuedMessage(msg, listener)); - return; - } - } - throw AblyException.fromErrorInfo(state.defaultErrorInfo); - } - - private void sendImpl(ProtocolMessage message, CompletionListener listener) throws AblyException { - if(transport == null) { - Log.v(TAG, "sendImpl(): Discarding message; transport unavailable"); - return; - } - if(ProtocolMessage.ackRequired(message)) { - message.msgSerial = msgSerial++; - pendingMessages.push(new QueuedMessage(message, listener)); - } - if(protocolListener != null) { - protocolListener.onRawMessageSend(message); - } - transport.send(message); - } - - private void sendImpl(QueuedMessage msg) throws AblyException { - if(transport == null) { - Log.v(TAG, "sendImpl(): Discarding message; transport unavailable"); - return; - } - ProtocolMessage message = msg.msg; - if(ProtocolMessage.ackRequired(message)) { - message.msgSerial = msgSerial++; - pendingMessages.push(msg); - } - if(protocolListener != null) { - protocolListener.onRawMessageSend(message); - } - transport.send(message); - } - - private void sendQueuedMessages() { - synchronized(this) { - while(queuedMessages.size() > 0) { - try { - sendImpl(queuedMessages.get(0)); - } catch (AblyException e) { - Log.e(TAG, "sendQueuedMessages(): Unexpected error sending queued messages", e); - } finally { - queuedMessages.remove(0); - } - } - } - } - - private void failQueuedMessages(ErrorInfo reason) { - synchronized(this) { - for (QueuedMessage queued: queuedMessages) { - if (queued.listener != null) { - try { - queued.listener.onError(reason); - } catch (Throwable t) { - Log.e(TAG, "failQueuedMessages(): Unexpected error calling listener", t); - } - } - } - queuedMessages.clear(); - } - } - - /** - * A class containing a queue of messages awaiting acknowledgement - */ - private class PendingMessageQueue { - private long startSerial = 0L; - private ArrayList queue = new ArrayList(); - - public synchronized void push(QueuedMessage msg) { - queue.add(msg); - } - - public void ack(long msgSerial, int count, ErrorInfo reason) { - QueuedMessage[] ackMessages = null, nackMessages = null; - synchronized(this) { - if(msgSerial < startSerial) { - /* this is an error condition and shouldn't happen but - * we can handle it gracefully by only processing the - * relevant portion of the response */ - count -= (int)(startSerial - msgSerial); - if(count < 0) - count = 0; - msgSerial = startSerial; - } - if(msgSerial > startSerial) { - /* this counts as a nack of the messages earlier than serial, - * as well as an ack */ - int nCount = (int)(msgSerial - startSerial); - List nackList = queue.subList(0, nCount); - nackMessages = nackList.toArray(new QueuedMessage[nCount]); - nackList.clear(); - startSerial = msgSerial; - } - if(msgSerial == startSerial) { - List ackList = queue.subList(0, count); - ackMessages = ackList.toArray(new QueuedMessage[count]); - ackList.clear(); - startSerial += count; - } - } - if(nackMessages != null) { - if(reason == null) - reason = new ErrorInfo("Unknown error", 500, 50000); - for(QueuedMessage msg : nackMessages) { - try { - if(msg.listener != null) - msg.listener.onError(reason); - } catch(Throwable t) { - Log.e(TAG, "ack(): listener exception", t); - } - } - } - if(ackMessages != null) { - for(QueuedMessage msg : ackMessages) { - try { - if(msg.listener != null) - msg.listener.onSuccess(); - } catch(Throwable t) { - Log.e(TAG, "ack(): listener exception", t); - } - } - } - } - - public synchronized void nack(long serial, int count, ErrorInfo reason) { - QueuedMessage[] nackMessages = null; - synchronized(this) { - if(serial != startSerial) { - /* this is an error condition and shouldn't happen but - * we can handle it gracefully by only processing the - * relevant portion of the response */ - count -= (int)(startSerial - serial); - serial = startSerial; - } - List nackList = queue.subList(0, count); - nackMessages = nackList.toArray(new QueuedMessage[count]); - nackList.clear(); - startSerial += count; - } - if(nackMessages != null) { - if(reason == null) - reason = new ErrorInfo("Unknown error", 500, 50000); - for(QueuedMessage msg : nackMessages) { - try { - if(msg.listener != null) - msg.listener.onError(reason); - } catch(Throwable t) { - Log.e(TAG, "nack(): listener exception", t); - } - } - } - } - - /** - * reset the pending message queue, failing any currently pending messages. - * Used when a resume fails and we get a different connection id. - * @param oldMsgSerial the next message serial number for the old - * connection, and thus one more than the highest message serial - * in the queue. - */ - public synchronized void reset(long oldMsgSerial, ErrorInfo err) { - nack(startSerial, (int)(oldMsgSerial - startSerial), err); - startSerial = 0; - } - - } - - /*********************** - * Network connectivity - **********************/ - - private class CMConnectivityListener implements NetworkConnectivityListener { - - @Override - public void onNetworkAvailable() { - ConnectionManager cm = ConnectionManager.this; - ConnectionState currentState = cm.getConnectionState().state; - Log.i(TAG, "onNetworkAvailable(): currentState = " + currentState.name()); - if(currentState == ConnectionState.disconnected || currentState == ConnectionState.suspended) { - Log.i(TAG, "onNetworkAvailable(): initiating reconnect"); - cm.connect(); - } - } - - @Override - public void onNetworkUnavailable(ErrorInfo reason) { - ConnectionManager cm = ConnectionManager.this; - ConnectionState currentState = cm.getConnectionState().state; - Log.i(TAG, "onNetworkUnavailable(); currentState = " + currentState.name() + "; reason = " + reason.toString()); - if(currentState == ConnectionState.connected || currentState == ConnectionState.connecting) { - Log.i(TAG, "onNetworkUnavailable(): closing connected transport"); - cm.requestState(new StateIndication(ConnectionState.disconnected, reason)); - } - } - } - - private void startConnectivityListener() { - connectivityListener = new CMConnectivityListener(); - ably.platform.getNetworkConnectivity().addListener(connectivityListener); - } - - private void stopConnectivityListener() { - ably.platform.getNetworkConnectivity().removeListener(connectivityListener); - connectivityListener = null; - } - - /******************* - * for tests only - ******************/ - - void disconnectAndSuppressRetries() { - if(transport != null) { - transport.close(); - } - suppressRetry = true; - } - - /******************* - * misc error handling - ******************/ - - private boolean isTokenError(ErrorInfo err) { - return ((err.code >= 40140) && (err.code < 40150)) || (err.code == 80019 && err.statusCode == 401); - } - - private boolean isFatalError(ErrorInfo err) { - if(err.code != 0) { - /* token errors are assumed to be recoverable */ - if(isTokenError(err)) { return false; } - /* 400 codes assumed to be fatal */ - if((err.code >= 40000) && (err.code < 50000)) { return true; } - } - /* otherwise, use statusCode */ - if(err.statusCode != 0 && err.statusCode < 500) { return true; } - return false; - } - - /******************* - * private members - ******************/ - - final AblyRealtime ably; - private final Channels channels; - private final Connection connection; - private final ITransport.Factory transportFactory; - private final List queuedMessages = new ArrayList<>(); - private final PendingMessageQueue pendingMessages = new PendingMessageQueue(); - private final HashSet heartbeatWaiters = new HashSet(); - private final ActionQueue actionQueue = new ActionQueue(); - private final Hosts hosts; - - private Thread handlerThread; - private final Map states = new HashMap<>(); - private State currentState; - private ErrorInfo stateError; - private ConnectParams pendingConnect; - private boolean suppressRetry; /* for tests only; modified via reflection */ - private ITransport transport; - private long suspendTime; - private long msgSerial; - private long lastActivity; - private CMConnectivityListener connectivityListener; - private long connectionStateTtl = Defaults.connectionStateTtl; - long maxIdleInterval = Defaults.maxIdleInterval; - - /* for debug/test only */ - private final RawProtocolListener protocolListener; - private String lastUsedHost; - - private static final long HEARTBEAT_TIMEOUT = 5000L; + /************************************************************** + * ConnectionManager + * + * This class is responsible for coordinating all actions that + * relate to transports and connection state. + * + * It comprises two principal parts: + * - An action queue, and a thread that performs those actions. + * Actions comprise connection state change requests, plus other + * actions that arise from transport state indications. An + * action Handler thread runs, except during idle times when + * there is no current or pending connection activity, that + * performs queued actions. + * + * - A state machine that represents the current connection state, + * and the possible transitions between states. + **************************************************************/ + + private static final String TAG = ConnectionManager.class.getName(); + private static final String INTERNET_CHECK_URL = "https://internet-up.ably-realtime.com/is-the-internet-up.txt"; + private static final String INTERNET_CHECK_OK = "yes"; + + /*********************************** + * default errors + ***********************************/ + + static ErrorInfo REASON_CLOSED = new ErrorInfo("Connection closed by client", 200, 10000); + static ErrorInfo REASON_DISCONNECTED = new ErrorInfo("Connection temporarily unavailable", 503, 80003); + static ErrorInfo REASON_SUSPENDED = new ErrorInfo("Connection unavailable", 503, 80002); + static ErrorInfo REASON_FAILED = new ErrorInfo("Connection failed", 400, 80000); + static ErrorInfo REASON_REFUSED = new ErrorInfo("Access refused", 401, 40100); + static ErrorInfo REASON_TOO_BIG = new ErrorInfo("Connection closed; message too large", 400, 40000); + + /** + * Methods on the channels map owned by the {@link AblyRealtime} instance + * which the {@link ConnectionManager} needs access to. + */ + public interface Channels { + void onMessage(ProtocolMessage msg); + void suspendAll(ErrorInfo error, boolean notifyStateChange); + Iterable values(); + } + + /*********************************** + * a class encapsulating information + * associated with a currentState change + * request or notification + ***********************************/ + + public static class StateIndication { + final ConnectionState state; + final ErrorInfo reason; + final String fallback; + final String currentHost; + + StateIndication(ConnectionState state) { + this(state, null); + } + + public StateIndication(ConnectionState state, ErrorInfo reason) { + this(state, reason, null, null); + } + + StateIndication(ConnectionState state, ErrorInfo reason, String fallback, String currentHost) { + this.state = state; + this.reason = reason; + this.fallback = fallback; + this.currentHost = currentHost; + } + } + + /************************************* + * a class encapsulating state machine + * information for a given state + *************************************/ + + public abstract class State { + public final ConnectionState state; + public final ErrorInfo defaultErrorInfo; + public final boolean queueEvents; + public final boolean sendEvents; + + final boolean terminal; + public final long timeout; + + State(ConnectionState state, boolean queueEvents, boolean sendEvents, boolean terminal, long timeout, ErrorInfo defaultErrorInfo) { + this.state = state; + this.queueEvents = queueEvents; + this.sendEvents = sendEvents; + this.terminal = terminal; + this.timeout = timeout; + this.defaultErrorInfo = defaultErrorInfo; + } + + /** + * Called on the current state to determine the response to a + * give state change request. + * @param target: the state change request or event + * @return StateIndication result: the determined response to + * the request with the required state transition, if any. A + * null result indicates that there is no resulting transition. + */ + abstract StateIndication validateTransition(StateIndication target); + + /** + * Called when the timeout occurs for the current state. + * @return StateIndication result: the determined response to + * the timeout with the required state transition, if any. A + * null result indicates that there is no resulting transition. + */ + StateIndication onTimeout() { + return null; + } + + /** + * Perform a transition to this state. + * @param stateIndication: the transition request that triggered this transition + * @param change: the change event corresponding to this transition. + */ + void enact(StateIndication stateIndication, ConnectionStateChange change) { + if(change != null) { + /* if now connected, send queued messages, etc */ + if(sendEvents) { + sendQueuedMessages(); + } else if(!queueEvents) { + failQueuedMessages(stateIndication.reason); + } + for(final Channel channel : channels.values()) { + enactForChannel(stateIndication, change, channel); + } + } + } + + /** + * Perform a transition to this state for a given channel. + * @param stateIndication: the transition request that triggered this transition + * @param change: the change event corresponding to this transition. + * @param channel: the channel + */ + void enactForChannel(StateIndication stateIndication, ConnectionStateChange change, Channel channel) {} + } + + /************************************************** + * Initialized: the initial state + **************************************************/ + + class Initialized extends State { + Initialized() { + super(ConnectionState.initialized, true, false, false, 0, null); + } + + @Override + StateIndication validateTransition(StateIndication target) { + /* we can transition to any other state, other than ourselves */ + if(target.state == this.state) { + return null; + } + return target; + } + } + + /************************************************** + * Connecting: a connection attempt is in progress + **************************************************/ + + class Connecting extends State { + Connecting() { + super(ConnectionState.connecting, true, false, false, Defaults.TIMEOUT_CONNECT, null); + } + + @Override + StateIndication onTimeout() { + return checkSuspended(null); + } + + @Override + StateIndication validateTransition(StateIndication target) { + /* we can transition to any other state */ + return target; + } + + @Override + void enact(StateIndication stateIndication, ConnectionStateChange change) { + super.enact(stateIndication, change); + connectImpl(stateIndication); + } + } + + /************************************************** + * Connected: a connection is established + **************************************************/ + + class Connected extends State { + Connected() { + super(ConnectionState.connected, false, true, false, 0, null); + } + + @Override + StateIndication validateTransition(StateIndication target) { + if(target.state == this.state) { + /* RTN24: no currentState change, so no transition, required, but there will be an update event; + * connected is special case because we want to deliver reauth notifications to listeners as an update */ + addAction(new UpdateAction(null)); + return null; + } + + /* we can transition to any other state */ + return target; + } + + @Override + void enactForChannel(StateIndication stateIndication, ConnectionStateChange change, Channel channel) { + channel.setConnected(); + } + } + + /************************************************** + * Disconnected: no connection is established, but + * a reconnection attempt will be made on timer + * expiry, anticipating preservation of connection + * state on reconnection + **************************************************/ + + class Disconnected extends State { + Disconnected() { + super(ConnectionState.disconnected, true, false, false, Defaults.TIMEOUT_DISCONNECT, REASON_DISCONNECTED); + } + + @Override + StateIndication validateTransition(StateIndication target) { + /* we can't transition to ourselves */ + if(target.state == this.state) { + return null; + } + /* a closing event will transition directly to closed */ + if(target.state == ConnectionState.closing) { + return new StateIndication(ConnectionState.closed); + } + /* otherwise, the transition is valid */ + return target; + } + + @Override + StateIndication onTimeout() { + return new StateIndication(ConnectionState.connecting); + } + + @Override + void enactForChannel(StateIndication stateIndication, ConnectionStateChange change, Channel channel) { + /* (RTL3e) If the connection currentState enters the + * DISCONNECTED currentState, it will have no effect on the + * channel states. */ + } + + @Override + void enact(StateIndication stateIndication, ConnectionStateChange change) { + super.enact(stateIndication, change); + clearTransport(); + if(change.previous == ConnectionState.connected) { + setSuspendTime(); + /* we were connected, so retry immediately */ + if(!suppressRetry) { + requestState(ConnectionState.connecting); + } + } + } + } + + /************************************************** + * Suspended: no connection is established. A + * reconnection attempt will be made on timer expiry + * but there will be no continuity of connection + * state on reconnection + **************************************************/ + + class Suspended extends State { + Suspended() { + super(ConnectionState.suspended, false, false, false, Defaults.connectionStateTtl, REASON_SUSPENDED); + } + + @Override + StateIndication validateTransition(StateIndication target) { + /* we can't transition to ourselves */ + if(target.state == this.state) { + return null; + } + /* a closing event will transition directly to closed */ + if(target.state == ConnectionState.closing) { + return new StateIndication(ConnectionState.closed); + } + /* otherwise, the transition is valid */ + return target; + } + + @Override + StateIndication onTimeout() { + return new StateIndication(ConnectionState.connecting); + } + + @Override + void enactForChannel(StateIndication stateIndication, ConnectionStateChange change, Channel channel) { + /* (RTL3c) If the connection currentState enters the SUSPENDED + * currentState, then an ATTACHING or ATTACHED channel currentState + * will transition to SUSPENDED. */ + channel.setSuspended(defaultErrorInfo, true); + } + } + + /************************************************** + * Closing: a close sequence is in progress + **************************************************/ + + class Closing extends State { + Closing() { + super(ConnectionState.closing, false, false, false, Defaults.TIMEOUT_CONNECT, REASON_CLOSED); + } + + @Override + StateIndication validateTransition(StateIndication target) { + /* we can't transition to ourselves */ + if(target.state == this.state) { + return null; + } + /* any disconnection event will transition directly to closed */ + if(target.state == ConnectionState.disconnected || target.state == ConnectionState.suspended) { + return new StateIndication(ConnectionState.closed); + } + /* otherwise, the transition is valid */ + return target; + } + + @Override + StateIndication onTimeout() { + return new StateIndication(ConnectionState.closed); + } + + @Override + void enact(StateIndication stateIndication, ConnectionStateChange change) { + super.enact(stateIndication, change); + boolean closed = closeImpl(); + if(closed) { + addAction(new AsynchronousStateChangeAction(ConnectionState.closed)); + } + } + } + + /************************************************** + * Closed: the connection is closed, and no + * reconnection attempt will be made unless + * explicitly requested + **************************************************/ + + class Closed extends State { + Closed() { + super(ConnectionState.closed, false, false, true, 0, REASON_CLOSED); + } + + @Override + StateIndication validateTransition(StateIndication target) { + /* we only leave the closed state via a connection attempt */ + if(target.state == ConnectionState.connecting) { + return target; + } + /* otherwise, the transition is not valid */ + return null; + } + + @Override + void enactForChannel(StateIndication stateIndication, ConnectionStateChange change, Channel channel) { + /* (RTL3b) If the connection currentState enters the CLOSED + * currentState, then an ATTACHING or ATTACHED channel currentState + * will transition to DETACHED. */ + channel.setConnectionClosed(REASON_CLOSED); + } + } + + /************************************************** + * Failed: there is no connection, and there has + * been an error, either in options validation or + * as a response to a connection attempt, that + * implies no new connection attempt will succeed. + * No reconnection attempt will be made unless + * explicitly requested + **************************************************/ + + class Failed extends State { + Failed() { + super(ConnectionState.failed, false, false, true, 0, REASON_FAILED); + } + + @Override + StateIndication validateTransition(StateIndication target) { + /* we only leave the failed state via a connection attempt */ + if(target.state == ConnectionState.connecting) { + return target; + } + /* otherwise, the transition is not valid */ + return null; + } + + @Override + void enactForChannel(StateIndication stateIndication, ConnectionStateChange change, Channel channel) { + /* (RTL3a) If the connection currentState enters the FAILED + * currentState, then an ATTACHING or ATTACHED channel currentState + * will transition to FAILED, set the + * Channel#errorReason and emit the error event. */ + channel.setConnectionFailed(stateIndication.reason); + } + + @Override + void enact(StateIndication stateIndication, ConnectionStateChange change) { + super.enact(stateIndication, change); + clearTransport(); + } + } + + public ErrorInfo getStateErrorInfo() { + return stateError != null ? stateError : currentState.defaultErrorInfo; + } + + public boolean isActive() { + return currentState.queueEvents || currentState.sendEvents; + } + + /************************************* + * a class that listens for currentState change + * events for in-place authorization + *************************************/ + + private class ConnectionWaiter implements ConnectionStateListener { + private ConnectionStateChange change; + + private ConnectionWaiter() { + connection.on(this); + } + + /** + * Wait for a currentState change notification + */ + private synchronized ErrorInfo waitForChange() { + Log.d(TAG, "ConnectionWaiter.waitFor()"); + if (change == null) { + try { wait(); } catch(InterruptedException e) {} + } + Log.d(TAG, "ConnectionWaiter.waitFor done: currentState=" + currentState + ")"); + ErrorInfo reason = change.reason; + change = null; + return reason; + } + + /** + * ConnectionStateListener interface + */ + @Override + public void onConnectionStateChanged(ConnectionStateChange state) { + synchronized(this) { + change = state; + notify(); + } + } + } + + /*********************** + * Actions + ***********************/ + + /** + * A class that encapsulates actions to perform by the ConnectionManager + */ + private interface Action extends Runnable {} + + /** + * An class that performs a state transition + */ + private abstract class StateChangeAction { + + protected final ITransport transport; + protected final StateIndication stateIndication; + protected ConnectionStateChange change; + + StateChangeAction(ITransport transport, StateIndication stateIndication) { + this.transport = transport; + this.stateIndication = stateIndication; + } + + /** + * Make the change to the ConnectionManager currentState represented by this Action + */ + protected void setState() { + change = ConnectionManager.this.setState(transport, stateIndication); + } + + protected void enactState() { + if(change != null) { + if(change.current != change.previous) { + /* broadcast currentState change */ + connection.onConnectionStateChange(change); + } + + /* implement the state change */ + states.get(stateIndication.state).enact(stateIndication, change); + if(currentState.terminal) { + clearTransport(); + } + } + } + } + + /** + * An Action that enacts a state transition, making the ConnectionManager state change + * synchronously. This is for instances such as any transition away from the connected + * state, where the state is updated synchronously with the transport state change. + * This ensures that there is no possibility of an attempt to send on the transport + * after it has indicated that it is not available. + */ + private class SynchronousStateChangeAction extends StateChangeAction implements Action { + SynchronousStateChangeAction(ITransport transport, StateIndication stateIndication) { + super(transport, stateIndication); + setState(); + } + + @Override + public void run() { + enactState(); + } + } + + /** + * An Action that enacts a state transition, making the ConnectionManager state change + * asynchronously. This applies to all transitions that are not transitions away from + * the connected state. + */ + private class AsynchronousStateChangeAction extends StateChangeAction implements Action{ + AsynchronousStateChangeAction(ConnectionState state) { + super(null, new StateIndication(state, null)); + } + + AsynchronousStateChangeAction(ITransport transport, StateIndication stateIndication) { + super(transport, stateIndication); + } + + @Override + public void run() { + setState(); + enactState(); + } + } + + /** + * An Action that performs an inband reauthorisation + */ + private class ReauthAction implements Action { + @Override + public void run() { + handleReauth(); + } + } + + /** + * An Action that handles dissemination of update events arising from a + * connected -> connected transition + */ + private class UpdateAction implements Action { + private final ErrorInfo reason; + + UpdateAction(ErrorInfo reason) { + this.reason = reason; + } + + @Override + public void run() { + connection.emitUpdate(reason); + } + } + + /** + * A queue of Actions awaiting processing + */ + private static class ActionQueue extends ArrayDeque { + public synchronized boolean add(Action action) { + return super.add(action); + } + + public synchronized Action poll() { + return super.poll(); + } + + public synchronized Action peek() { + return super.peek(); + } + + public synchronized int size() { + return super.size(); + } + } + + /** + * Append an action to the pending action queue + * @param action: the action + */ + private synchronized void addAction(Action action) { + actionQueue.add(action); + notifyAll(); + } + + /** + * A handler that runs in a dedicated Thread that processes queued actions + */ + class ActionHandler implements Runnable { + + public void run() { + while(true) { + /* + * Until we're committed to exit we: + * - wait for an action or timeout + * - given an action, perform the action asynchronously; + * - if a timeout, perform the timeout state transition + */ + + /* Hold the lock until we obtain an action */ + synchronized(ConnectionManager.this) { + while(actionQueue.size() == 0) { + /* if we're in a terminal state, then this thread is done */ + if(currentState.terminal) { + /* indicate that this thread is committed to die */ + handlerThread = null; + stopConnectivityListener(); + return; + } + + /* wait for an action event or for expiry of the current currentState */ + tryWait(currentState.timeout); + + /* if during the wait some action was requested, handle it */ + Action act = actionQueue.peek(); + if (act != null) { + Log.d(TAG, "Wait ended by action: " + act.toString()); + break; + } + + /* if our currentState wants us to retry on timer expiry, do that */ + if (!suppressRetry) { + StateIndication nextState = currentState.onTimeout(); + if (nextState != null) { + requestState(nextState); + } + } + } + } + + /* perform outstanding actions, without the ConnectionManager locked */ + Action deferredAction; + while((deferredAction = actionQueue.poll()) != null) { + try { + deferredAction.run(); + } catch(Exception e) { + Log.e(TAG, "Action invocation failed with exception: action = " + deferredAction.toString(), e); + } + } + } + } + } + + /*********************** + * ConnectionManager + ***********************/ + + public ConnectionManager(final AblyRealtime ably, final Connection connection, final Channels channels) throws AblyException { + this.ably = ably; + this.connection = connection; + this.channels = channels; + + ClientOptions options = ably.options; + this.hosts = new Hosts(options.realtimeHost, Defaults.HOST_REALTIME, options); + + /* debug options */ + ITransport.Factory transportFactory = null; + RawProtocolListener protocolListener = null; + if(options instanceof DebugOptions) { + protocolListener = ((DebugOptions) options).protocolListener; + transportFactory = ((DebugOptions) options).transportFactory; + } + this.protocolListener = protocolListener; + this.transportFactory = (transportFactory != null) ? transportFactory : Defaults.TRANSPORT; + + /* construct all states */ + states.put(ConnectionState.initialized, new Initialized()); + states.put(ConnectionState.connecting, new Connecting()); + states.put(ConnectionState.connected, new Connected()); + states.put(ConnectionState.disconnected, new Disconnected()); + states.put(ConnectionState.suspended, new Suspended()); + states.put(ConnectionState.closing, new Closing()); + states.put(ConnectionState.closed, new Closed()); + states.put(ConnectionState.failed, new Failed()); + + currentState = states.get(ConnectionState.initialized); + + setSuspendTime(); + } + + /********************* + * host management + *********************/ + + /* This is only here for the benefit of ConnectionManagerTest. */ + public String getHost() { + return lastUsedHost; + } + + /********************* + * states API + *********************/ + + public synchronized State getConnectionState() { + return currentState; + } + + public synchronized void connect() { + /* connect() is the only action that will bring the ConnectionManager out of a terminal currentState */ + if(currentState.terminal || currentState.state == ConnectionState.initialized) { + startup(); + } + requestState(ConnectionState.connecting); + } + + public void close() { + requestState(ConnectionState.closing); + } + + public void requestState(ConnectionState state) { + requestState(new StateIndication(state, null)); + } + + public void requestState(StateIndication state) { + requestState(null, state); + } + + private synchronized void requestState(ITransport transport, StateIndication stateIndication) { + Log.v(TAG, "requestState(): requesting " + stateIndication.state + "; id = " + connection.id); + addAction(new AsynchronousStateChangeAction(transport, stateIndication)); + } + + private synchronized ConnectionStateChange setState(ITransport transport, StateIndication stateIndication) { + /* check validity of transport */ + if (transport != null && transport != this.transport) { + Log.v(TAG, "setState: action received for superseded transport; discarding"); + return null; + } + + /* check validity of transition */ + StateIndication validatedStateIndication = currentState.validateTransition(stateIndication); + if (validatedStateIndication == null) { + Log.v(TAG, "setState(): not transitioning; not a valid transition " + stateIndication.state); + return null; + } + + /* update currentState */ + ConnectionState newConnectionState = validatedStateIndication.state; + State newState = states.get(newConnectionState); + ErrorInfo reason = validatedStateIndication.reason; + if (reason == null) { + reason = newState.defaultErrorInfo; + } + Log.v(TAG, "setState(): setting " + newState.state + "; reason " + reason); + ConnectionStateChange change = new ConnectionStateChange(currentState.state, newConnectionState, newState.timeout, reason); + currentState = newState; + stateError = reason; + + return change; + } + + /********************* + * ping API + *********************/ + + public void ping(final CompletionListener listener) { + HeartbeatWaiter waiter = new HeartbeatWaiter(listener); + if(currentState.state != ConnectionState.connected) { + waiter.onError(new ErrorInfo("Unable to ping service; not connected", 40000, 400)); + return; + } + synchronized(heartbeatWaiters) { + heartbeatWaiters.add(waiter); + waiter.start(); + } + try { + send(new ProtocolMessage(ProtocolMessage.Action.heartbeat), false, null); + } catch (AblyException e) { + waiter.onError(e.errorInfo); + } + } + + /** + * A thread that waits for completion of a ping + */ + private class HeartbeatWaiter extends Thread { + private final CompletionListener listener; + + HeartbeatWaiter(CompletionListener listener) { + this.listener = listener; + } + + private void onSuccess() { + clear(); + if(listener != null) { + listener.onSuccess(); + } + } + + private void onError(ErrorInfo reason) { + clear(); + if(listener != null) { + listener.onError(reason); + } + } + + private boolean clear() { + boolean pending = heartbeatWaiters.remove(this); + if(pending) { + interrupt(); + } + return pending; + } + + @Override + public void run() { + boolean pending; + synchronized(heartbeatWaiters) { + try { + heartbeatWaiters.wait(HEARTBEAT_TIMEOUT); + } catch (InterruptedException ie) { + } + pending = clear(); + } + if(pending) { + onError(new ErrorInfo("Timed out waiting for heartbeat response", 50000, 500)); + } else { + onSuccess(); + } + } + } + + /*************************************** + * auth event handling + ***************************************/ + + /** + * (RTC8) For a realtime client, Auth.authorize instructs the library to + * obtain a token using the provided tokenParams and authOptions and upgrade + * the current connection to use that token; or if not currently connected, + * to connect with the token. + */ + public void onAuthUpdated(String token, boolean waitForResponse) throws AblyException { + ConnectionWaiter waiter = new ConnectionWaiter(); + switch(currentState.state) { + case connected: + /* (RTC8a) If the connection is in the CONNECTED currentState and + * auth.authorize is called or Ably requests a re-authentication + * (see RTN22), the client must obtain a new token, then send an + * AUTH ProtocolMessage to Ably with an auth attribute + * containing an AuthDetails object with the token string. */ + try { + ProtocolMessage msg = new ProtocolMessage(ProtocolMessage.Action.auth); + msg.auth = new ProtocolMessage.AuthDetails(token); + send(msg, false, null); + } catch (AblyException e) { + /* The send failed. Close the transport; if a subsequent + * reconnect succeeds, it will be with the new token. */ + Log.v(TAG, "onAuthUpdated: closing transport after send failure"); + transport.close(); + } + break; + + case connecting: + /* Close the connecting transport. */ + Log.v(TAG, "onAuthUpdated: closing connecting transport"); + ErrorInfo disconnectError = new ErrorInfo("Aborting incomplete connection with superseded auth params", 503, 80003); + requestState(new StateIndication(ConnectionState.disconnected, disconnectError, null, null)); + /* Start a new connection attempt. */ + connect(); + break; + + default: + /* Start a new connection attempt. */ + connect(); + break; + } + + if(!waitForResponse) { + return; + } + + /* Wait for a currentState transition into anything other than connecting or + * disconnected. Note that this includes the case that the connection + * was already connected, and the AUTH message prompted the server to + * send another connected message. */ + for (;;) { + ErrorInfo reason = waiter.waitForChange(); + switch (currentState.state) { + case connected: + Log.v(TAG, "onAuthUpdated: got connected"); + return; + case connecting: + case disconnected: + continue; + default: + /* suspended/closed/error: throw the error. */ + Log.v(TAG, "onAuthUpdated: throwing exception"); + throw AblyException.fromErrorInfo(reason); + } + } + } + + /** + * Called when where was an error during authentication attempt + * + * @param errorInfo Error associated with unsuccessful authentication + */ + public void onAuthError(ErrorInfo errorInfo) { + Log.i(TAG, String.format("onAuthError: (%d) %s", errorInfo.code, errorInfo.message)); + switch (currentState.state) { + case connecting: + ITransport transport = this.transport; + if (transport != null) + /* request that the current transport is closed */ + requestState(new StateIndication(ConnectionState.disconnected, errorInfo)); + break; + + case connected: + /* stay connected but notify of authentication error */ + addAction(new UpdateAction(errorInfo)); + break; + + default: + break; + } + } + + /*************************************** + * transport events/notifications + ***************************************/ + + /** + * React on message from the transport + * @param transport transport instance or null to bypass transport correctness check (for testing) + * @param message + * @throws AblyException + */ + public void onMessage(ITransport transport, ProtocolMessage message) throws AblyException { + if (transport != null && this.transport != transport) { + return; + } + if (Log.level <= Log.VERBOSE) { + Log.v(TAG, "onMessage() (transport = " + transport + "): " + message.action + ": " + new String(ProtocolSerializer.writeJSON(message))); + } + try { + if(protocolListener != null) { + protocolListener.onRawMessageRecv(message); + } + switch(message.action) { + case heartbeat: + onHeartbeat(message); + break; + case error: + ErrorInfo reason = message.error; + if(reason == null) { + Log.e(TAG, "onMessage(): ERROR message received (no error detail)"); + } else { + Log.e(TAG, "onMessage(): ERROR message received; message = " + reason.message + "; code = " + reason.code); + } + + /* an error message may signify an error currentState in a channel, or in the connection */ + if(message.channel != null) { + onChannelMessage(message); + } else { + onError(message); + } + break; + case connected: + onConnected(message); + break; + case disconnect: + case disconnected: + onDisconnected(message); + break; + case closed: + onClosed(message); + break; + case ack: + onAck(message); + break; + case nack: + onNack(message); + break; + case auth: + addAction(new ReauthAction()); + break; + default: + onChannelMessage(message); + } + } + catch(Exception e) { + // Prevent any non-AblyException to be thrown + throw AblyException.fromThrowable(e); + } + } + + private void onChannelMessage(ProtocolMessage message) { + if(message.connectionSerial != null) { + connection.serial = message.connectionSerial.longValue(); + if (connection.key != null) + connection.recoveryKey = connection.key + ":" + message.connectionSerial; + } + channels.onMessage(message); + } + + private synchronized void onConnected(ProtocolMessage message) { + /* if the returned connection id differs from + * the existing connection id, then this means + * we need to suspend all existing attachments to + * the old connection. + * If realtime did not reply with an error, it + * signifies that this was a result of an earlier + * connection being invalidated due to being stale. + * + * Suspend all channels attached to the previous id; + * this will be reattached in setConnection() */ + ErrorInfo error = message.error; + if(connection.id != null && !message.connectionId.equals(connection.id)) { + /* we need to suspend the original connection */ + if(error == null) { + error = REASON_SUSPENDED; + } + channels.suspendAll(error, false); + } + + /* set the new connection id */ + ConnectionDetails connectionDetails = message.connectionDetails; + connection.key = connectionDetails.connectionKey; + if (!message.connectionId.equals(connection.id)) { + /* The connection id has changed. Reset the message serial and the + * pending message queue (which fails the messages currently in + * there). */ + pendingMessages.reset(msgSerial, + new ErrorInfo("Connection resume failed", 500, 50000)); + msgSerial = 0; + } + connection.id = message.connectionId; + if(message.connectionSerial != null) { + connection.serial = message.connectionSerial.longValue(); + if (connection.key != null) + connection.recoveryKey = connection.key + ":" + message.connectionSerial; + } + + /* Get any parameters from connectionDetails. */ + maxIdleInterval = connectionDetails.maxIdleInterval; + connectionStateTtl = connectionDetails.connectionStateTtl; + + /* set the clientId resolved from token, if any */ + String clientId = connectionDetails.clientId; + try { + ably.auth.setClientId(clientId); + } catch (AblyException e) { + requestState(transport, new StateIndication(ConnectionState.failed, e.errorInfo)); + return; + } + + /* indicated connected currentState */ + setSuspendTime(); + requestState(new StateIndication(ConnectionState.connected, error)); + } + + private synchronized void onDisconnected(ProtocolMessage message) { + ErrorInfo reason = message.error; + if(reason != null && isTokenError(reason)) { + ably.auth.onAuthError(reason); + } + requestState(new StateIndication(ConnectionState.disconnected, reason)); + } + + private synchronized void onClosed(ProtocolMessage message) { + if(message.error != null) { + this.onError(message); + } else { + connection.key = null; + requestState(new StateIndication(ConnectionState.closed, null)); + } + } + + private synchronized void onError(ProtocolMessage message) { + connection.key = null; + ErrorInfo reason = message.error; + if(isTokenError(reason)) { + ably.auth.onAuthError(reason); + } + ConnectionState destinationState = isFatalError(reason) ? ConnectionState.failed : ConnectionState.disconnected; + requestState(transport, new StateIndication(destinationState, reason)); + } + + private void onAck(ProtocolMessage message) { + pendingMessages.ack(message.msgSerial, message.count, message.error); + } + + private void onNack(ProtocolMessage message) { + pendingMessages.nack(message.msgSerial, message.count, message.error); + } + + private void onHeartbeat(ProtocolMessage message) { + synchronized(heartbeatWaiters) { + heartbeatWaiters.clear(); + heartbeatWaiters.notifyAll(); + } + } + + /****************************** + * ConnectionManager lifecycle + ******************************/ + + private synchronized void startup() { + if(handlerThread == null) { + (handlerThread = new Thread(new ActionHandler())).start(); + startConnectivityListener(); + } + } + + private boolean checkConnectionStale() { + if(lastActivity == 0) { + return false; + } + long now = System.currentTimeMillis(); + long intervalSinceLastActivity = now - lastActivity; + if(intervalSinceLastActivity > (maxIdleInterval + connectionStateTtl)) { + /* RTN15g1, RTN15g2 Force a new connection if the previous one is stale; + * Clearing connection.key will ensure that we don't attempt to resume; + * leaving the original connection.id will mean that we notice at + * connection time that the connectionId has changed */ + if(connection.key != null) { + Log.v(TAG, "Clearing stale connection key to suppress resume"); + connection.key = null; + connection.recoveryKey = null; + } + return true; + } + return false; + } + + private synchronized void setSuspendTime() { + suspendTime = (System.currentTimeMillis() + connectionStateTtl); + } + + /** + * After a connection attempt failed, check to + * see whether we should attempt to use a fallback. + * @param reason + * @return StateIndication if a fallback connection attempt is required, otherwise null + */ + private StateIndication checkFallback(ErrorInfo reason) { + if(pendingConnect != null && (reason == null || reason.statusCode >= 500)) { + if (checkConnectivity()) { + /* we will try a fallback host */ + String hostFallback = hosts.getFallback(pendingConnect.host); + if (hostFallback != null) { + Log.v(TAG, "checkFallback: fallback to " + hostFallback); + return new StateIndication(ConnectionState.connecting, null, hostFallback, pendingConnect.host); + } + } + } + pendingConnect = null; + return null; + } + + private synchronized StateIndication checkSuspended(ErrorInfo reason) { + long currentTime = System.currentTimeMillis(); + long timeToSuspend = suspendTime - currentTime; + boolean suspendMode = timeToSuspend <= 0; + Log.v(TAG, "checkSuspended: timeToSuspend = " + timeToSuspend + "ms; suspendMode = " + suspendMode); + ConnectionState expiredState = suspendMode ? ConnectionState.suspended : ConnectionState.disconnected; + return new StateIndication(expiredState, reason); + } + + private void tryWait(long timeout) { + try { + if(timeout == 0) { + wait(); + } else { + wait(timeout); + } + } catch (InterruptedException e) {} + } + + private void handleReauth() { + if (currentState.state == ConnectionState.connected) { + Log.v(TAG, "Server initiated reauth"); + + ErrorInfo errorInfo = null; + + /* + * It is a server initiated reauth, it is issued while previous token is still valid for ~30 seconds, + * we have to clear cached token and get a new one + */ + try { + ably.auth.renew(); + } catch (AblyException e) { + errorInfo = e.errorInfo; + } + + /* report connection currentState in UPDATE event */ + if (currentState.state == ConnectionState.connected) { + connection.emitUpdate(errorInfo); + } + } + } + + @Override + public synchronized void onTransportAvailable(ITransport transport) { + if (this.transport != transport) { + /* This is from a transport that we have already abandoned. */ + Log.v(TAG, "onTransportAvailable: ignoring connection event from superseded transport"); + return; + } + if(protocolListener != null) { + protocolListener.onRawConnect(transport.getURL()); + } + } + + @Override + public synchronized void onTransportUnavailable(ITransport transport, ErrorInfo reason) { + if (this.transport != transport) { + /* This is from a transport that we have already abandoned. */ + Log.v(TAG, "onTransportUnavailable: ignoring disconnection event from superseded transport"); + return; + } + + /* if this is a failure of a pending connection attempt, decide whether or not to attempt a fallback host */ + StateIndication fallbackAttempt = checkFallback(reason); + if(fallbackAttempt != null) { + requestState(fallbackAttempt); + return; + } + + StateIndication stateIndication = null; + if(reason != null) { + if(isFatalError(reason)) { + Log.e(TAG, "onTransportUnavailable: unexpected transport error: " + reason.message); + stateIndication = new StateIndication(ConnectionState.failed, reason); + } else if(isTokenError(reason)) { + ably.auth.onAuthError(reason); + } + } + if(stateIndication == null) { + stateIndication = checkSuspended(reason); + } + addAction(new SynchronousStateChangeAction(transport, stateIndication)); + } + + private class ConnectParams extends TransportParams { + ConnectParams(ClientOptions options) { + super(options); + this.connectionKey = connection.key; + this.connectionSerial = String.valueOf(connection.serial); + this.port = Defaults.getPort(options); + } + } + + private void connectImpl(StateIndication request) { + /* determine the parameters of this connection attempt, and + * instance the transport. + * First, choose the transport. (Right now there's only one.) + * Second, choose the host. ConnectParams will use the default + * (or requested) host, unless fallback!=null, in which case + * checkSuspend has already chosen a fallback host at random */ + + String host = request.fallback; + if (host == null) { + host = hosts.getPreferredHost(); + } + checkConnectionStale(); + pendingConnect = new ConnectParams(ably.options); + pendingConnect.host = host; + lastUsedHost = host; + + /* try the connection */ + ITransport transport; + try { + transport = transportFactory.getTransport(pendingConnect, this); + } catch(Exception e) { + String msg = "Unable to instance transport class"; + Log.e(getClass().getName(), msg, e); + throw new RuntimeException(msg, e); + } + ITransport oldTransport; + synchronized(this) { + oldTransport = this.transport; + this.transport = transport; + } + if (oldTransport != null) { + oldTransport.close(); + } + transport.connect(this); + if(protocolListener != null) { + protocolListener.onRawConnectRequested(transport.getURL()); + } + } + + /** + * Close any existing transport + * @return closed if true, otherwise awaiting closed indication + */ + private boolean closeImpl() { + if(transport == null) { + return true; + } + + /* if connected, send an explicit close message and await response */ + boolean isConnected = currentState.state == ConnectionState.connected; + if(isConnected) { + try { + Log.v(TAG, "Requesting connection close"); + transport.send(new ProtocolMessage(ProtocolMessage.Action.close)); + return false; + } catch (AblyException e) { + /* we're closing, and the attempt to send the CLOSE message failed; + * continue, because we're not going to reinstate the transport + * just to send a CLOSE message */ + } + } + + /* just close the transport */ + Log.v(TAG, "Closing incomplete transport"); + clearTransport(); + return true; + } + + private void clearTransport() { + if(transport != null) { + transport.close(); + transport = null; + } + } + + /** + * Determine whether or not the client has connection to the network + * without reference to a specific ably host. This is to determine whether + * it is better to try a fallback host, or keep retrying with the default + * host. + * @return boolean, true if network is available + */ + protected boolean checkConnectivity() { + try { + return HttpHelpers.getUrlString(ably.httpCore, INTERNET_CHECK_URL).contains(INTERNET_CHECK_OK); + } catch(AblyException e) { + return false; + } + } + + protected void setLastActivity(long lastActivityTime) { + this.lastActivity = lastActivityTime; + } + + /****************** + * event queueing + ******************/ + + public static class QueuedMessage { + public final ProtocolMessage msg; + public final CompletionListener listener; + public QueuedMessage(ProtocolMessage msg, CompletionListener listener) { + this.msg = msg; + this.listener = listener; + } + } + + public void send(ProtocolMessage msg, boolean queueEvents, CompletionListener listener) throws AblyException { + State state; + synchronized(this) { + state = this.currentState; + if(state.sendEvents) { + sendImpl(msg, listener); + return; + } + if(state.queueEvents && queueEvents) { + queuedMessages.add(new QueuedMessage(msg, listener)); + return; + } + } + throw AblyException.fromErrorInfo(state.defaultErrorInfo); + } + + private void sendImpl(ProtocolMessage message, CompletionListener listener) throws AblyException { + if(transport == null) { + Log.v(TAG, "sendImpl(): Discarding message; transport unavailable"); + return; + } + if(ProtocolMessage.ackRequired(message)) { + message.msgSerial = msgSerial++; + pendingMessages.push(new QueuedMessage(message, listener)); + } + if(protocolListener != null) { + protocolListener.onRawMessageSend(message); + } + transport.send(message); + } + + private void sendImpl(QueuedMessage msg) throws AblyException { + if(transport == null) { + Log.v(TAG, "sendImpl(): Discarding message; transport unavailable"); + return; + } + ProtocolMessage message = msg.msg; + if(ProtocolMessage.ackRequired(message)) { + message.msgSerial = msgSerial++; + pendingMessages.push(msg); + } + if(protocolListener != null) { + protocolListener.onRawMessageSend(message); + } + transport.send(message); + } + + private void sendQueuedMessages() { + synchronized(this) { + while(queuedMessages.size() > 0) { + try { + sendImpl(queuedMessages.get(0)); + } catch (AblyException e) { + Log.e(TAG, "sendQueuedMessages(): Unexpected error sending queued messages", e); + } finally { + queuedMessages.remove(0); + } + } + } + } + + private void failQueuedMessages(ErrorInfo reason) { + synchronized(this) { + for (QueuedMessage queued: queuedMessages) { + if (queued.listener != null) { + try { + queued.listener.onError(reason); + } catch (Throwable t) { + Log.e(TAG, "failQueuedMessages(): Unexpected error calling listener", t); + } + } + } + queuedMessages.clear(); + } + } + + /** + * A class containing a queue of messages awaiting acknowledgement + */ + private class PendingMessageQueue { + private long startSerial = 0L; + private ArrayList queue = new ArrayList(); + + public synchronized void push(QueuedMessage msg) { + queue.add(msg); + } + + public void ack(long msgSerial, int count, ErrorInfo reason) { + QueuedMessage[] ackMessages = null, nackMessages = null; + synchronized(this) { + if(msgSerial < startSerial) { + /* this is an error condition and shouldn't happen but + * we can handle it gracefully by only processing the + * relevant portion of the response */ + count -= (int)(startSerial - msgSerial); + if(count < 0) + count = 0; + msgSerial = startSerial; + } + if(msgSerial > startSerial) { + /* this counts as a nack of the messages earlier than serial, + * as well as an ack */ + int nCount = (int)(msgSerial - startSerial); + List nackList = queue.subList(0, nCount); + nackMessages = nackList.toArray(new QueuedMessage[nCount]); + nackList.clear(); + startSerial = msgSerial; + } + if(msgSerial == startSerial) { + List ackList = queue.subList(0, count); + ackMessages = ackList.toArray(new QueuedMessage[count]); + ackList.clear(); + startSerial += count; + } + } + if(nackMessages != null) { + if(reason == null) + reason = new ErrorInfo("Unknown error", 500, 50000); + for(QueuedMessage msg : nackMessages) { + try { + if(msg.listener != null) + msg.listener.onError(reason); + } catch(Throwable t) { + Log.e(TAG, "ack(): listener exception", t); + } + } + } + if(ackMessages != null) { + for(QueuedMessage msg : ackMessages) { + try { + if(msg.listener != null) + msg.listener.onSuccess(); + } catch(Throwable t) { + Log.e(TAG, "ack(): listener exception", t); + } + } + } + } + + public synchronized void nack(long serial, int count, ErrorInfo reason) { + QueuedMessage[] nackMessages = null; + synchronized(this) { + if(serial != startSerial) { + /* this is an error condition and shouldn't happen but + * we can handle it gracefully by only processing the + * relevant portion of the response */ + count -= (int)(startSerial - serial); + serial = startSerial; + } + List nackList = queue.subList(0, count); + nackMessages = nackList.toArray(new QueuedMessage[count]); + nackList.clear(); + startSerial += count; + } + if(nackMessages != null) { + if(reason == null) + reason = new ErrorInfo("Unknown error", 500, 50000); + for(QueuedMessage msg : nackMessages) { + try { + if(msg.listener != null) + msg.listener.onError(reason); + } catch(Throwable t) { + Log.e(TAG, "nack(): listener exception", t); + } + } + } + } + + /** + * reset the pending message queue, failing any currently pending messages. + * Used when a resume fails and we get a different connection id. + * @param oldMsgSerial the next message serial number for the old + * connection, and thus one more than the highest message serial + * in the queue. + */ + public synchronized void reset(long oldMsgSerial, ErrorInfo err) { + nack(startSerial, (int)(oldMsgSerial - startSerial), err); + startSerial = 0; + } + + } + + /*********************** + * Network connectivity + **********************/ + + private class CMConnectivityListener implements NetworkConnectivityListener { + + @Override + public void onNetworkAvailable() { + ConnectionManager cm = ConnectionManager.this; + ConnectionState currentState = cm.getConnectionState().state; + Log.i(TAG, "onNetworkAvailable(): currentState = " + currentState.name()); + if(currentState == ConnectionState.disconnected || currentState == ConnectionState.suspended) { + Log.i(TAG, "onNetworkAvailable(): initiating reconnect"); + cm.connect(); + } + } + + @Override + public void onNetworkUnavailable(ErrorInfo reason) { + ConnectionManager cm = ConnectionManager.this; + ConnectionState currentState = cm.getConnectionState().state; + Log.i(TAG, "onNetworkUnavailable(); currentState = " + currentState.name() + "; reason = " + reason.toString()); + if(currentState == ConnectionState.connected || currentState == ConnectionState.connecting) { + Log.i(TAG, "onNetworkUnavailable(): closing connected transport"); + cm.requestState(new StateIndication(ConnectionState.disconnected, reason)); + } + } + } + + private void startConnectivityListener() { + connectivityListener = new CMConnectivityListener(); + ably.platform.getNetworkConnectivity().addListener(connectivityListener); + } + + private void stopConnectivityListener() { + ably.platform.getNetworkConnectivity().removeListener(connectivityListener); + connectivityListener = null; + } + + /******************* + * for tests only + ******************/ + + void disconnectAndSuppressRetries() { + if(transport != null) { + transport.close(); + } + suppressRetry = true; + } + + /******************* + * misc error handling + ******************/ + + private boolean isTokenError(ErrorInfo err) { + return ((err.code >= 40140) && (err.code < 40150)) || (err.code == 80019 && err.statusCode == 401); + } + + private boolean isFatalError(ErrorInfo err) { + if(err.code != 0) { + /* token errors are assumed to be recoverable */ + if(isTokenError(err)) { return false; } + /* 400 codes assumed to be fatal */ + if((err.code >= 40000) && (err.code < 50000)) { return true; } + } + /* otherwise, use statusCode */ + if(err.statusCode != 0 && err.statusCode < 500) { return true; } + return false; + } + + /******************* + * private members + ******************/ + + final AblyRealtime ably; + private final Channels channels; + private final Connection connection; + private final ITransport.Factory transportFactory; + private final List queuedMessages = new ArrayList<>(); + private final PendingMessageQueue pendingMessages = new PendingMessageQueue(); + private final HashSet heartbeatWaiters = new HashSet(); + private final ActionQueue actionQueue = new ActionQueue(); + private final Hosts hosts; + + private Thread handlerThread; + private final Map states = new HashMap<>(); + private State currentState; + private ErrorInfo stateError; + private ConnectParams pendingConnect; + private boolean suppressRetry; /* for tests only; modified via reflection */ + private ITransport transport; + private long suspendTime; + private long msgSerial; + private long lastActivity; + private CMConnectivityListener connectivityListener; + private long connectionStateTtl = Defaults.connectionStateTtl; + long maxIdleInterval = Defaults.maxIdleInterval; + + /* for debug/test only */ + private final RawProtocolListener protocolListener; + private String lastUsedHost; + + private static final long HEARTBEAT_TIMEOUT = 5000L; } diff --git a/lib/src/main/java/io/ably/lib/transport/Defaults.java b/lib/src/main/java/io/ably/lib/transport/Defaults.java index 6f288a309..9feaca4ff 100644 --- a/lib/src/main/java/io/ably/lib/transport/Defaults.java +++ b/lib/src/main/java/io/ably/lib/transport/Defaults.java @@ -6,51 +6,51 @@ import java.text.DecimalFormat; public class Defaults { - /* versions */ - public static final float ABLY_VERSION_NUMBER = 1.2f; - public static final String ABLY_VERSION = new DecimalFormat("0.0").format(ABLY_VERSION_NUMBER); - public static final String ABLY_LIB_VERSION = String.format("%s-%s", BuildConfig.LIBRARY_NAME, BuildConfig.VERSION); - - /* params */ - public static final String ABLY_VERSION_PARAM = "v"; - public static final String ABLY_LIB_PARAM = "lib"; - - /* Headers */ - public static final String ABLY_VERSION_HEADER = "X-Ably-Version"; - public static final String ABLY_LIB_HEADER = "X-Ably-Lib"; - - /* Hosts */ - public static final String[] HOST_FALLBACKS = { "A.ably-realtime.com", "B.ably-realtime.com", "C.ably-realtime.com", "D.ably-realtime.com", "E.ably-realtime.com" }; - public static final String HOST_REST = "rest.ably.io"; - public static final String HOST_REALTIME = "realtime.ably.io"; - public static final int PORT = 80; - public static final int TLS_PORT = 443; - - /* Timeouts */ - public static int TIMEOUT_CONNECT = 15000; - public static int TIMEOUT_DISCONNECT = 15000; - public static int TIMEOUT_CHANNEL_RETRY = 15000; - - /* TO313 */ - public static int TIMEOUT_HTTP_OPEN = 4000; - /* TO314 */ - public static int TIMEOUT_HTTP_REQUEST = 15000; - /* DF1b */ - public static long realtimeRequestTimeout = 10000L; - /* TO3l10 */ - public static long fallbackRetryTimeout = 10*60*1000L; - /* CD2h (but no default in the spec) */ - public static long maxIdleInterval = 20000L; - /* DF1a */ - public static long connectionStateTtl = 60000L; - - public static final ITransport.Factory TRANSPORT = new WebSocketTransport.Factory(); - public static final int HTTP_MAX_RETRY_COUNT = 3; - public static final int HTTP_ASYNC_THREADPOOL_SIZE = 64; - - public static int getPort(ClientOptions options) { - return options.tls - ? ((options.tlsPort != 0) ? options.tlsPort : Defaults.TLS_PORT) - : ((options.port != 0) ? options.port : Defaults.PORT); - } + /* versions */ + public static final float ABLY_VERSION_NUMBER = 1.2f; + public static final String ABLY_VERSION = new DecimalFormat("0.0").format(ABLY_VERSION_NUMBER); + public static final String ABLY_LIB_VERSION = String.format("%s-%s", BuildConfig.LIBRARY_NAME, BuildConfig.VERSION); + + /* params */ + public static final String ABLY_VERSION_PARAM = "v"; + public static final String ABLY_LIB_PARAM = "lib"; + + /* Headers */ + public static final String ABLY_VERSION_HEADER = "X-Ably-Version"; + public static final String ABLY_LIB_HEADER = "X-Ably-Lib"; + + /* Hosts */ + public static final String[] HOST_FALLBACKS = { "A.ably-realtime.com", "B.ably-realtime.com", "C.ably-realtime.com", "D.ably-realtime.com", "E.ably-realtime.com" }; + public static final String HOST_REST = "rest.ably.io"; + public static final String HOST_REALTIME = "realtime.ably.io"; + public static final int PORT = 80; + public static final int TLS_PORT = 443; + + /* Timeouts */ + public static int TIMEOUT_CONNECT = 15000; + public static int TIMEOUT_DISCONNECT = 15000; + public static int TIMEOUT_CHANNEL_RETRY = 15000; + + /* TO313 */ + public static int TIMEOUT_HTTP_OPEN = 4000; + /* TO314 */ + public static int TIMEOUT_HTTP_REQUEST = 15000; + /* DF1b */ + public static long realtimeRequestTimeout = 10000L; + /* TO3l10 */ + public static long fallbackRetryTimeout = 10*60*1000L; + /* CD2h (but no default in the spec) */ + public static long maxIdleInterval = 20000L; + /* DF1a */ + public static long connectionStateTtl = 60000L; + + public static final ITransport.Factory TRANSPORT = new WebSocketTransport.Factory(); + public static final int HTTP_MAX_RETRY_COUNT = 3; + public static final int HTTP_ASYNC_THREADPOOL_SIZE = 64; + + public static int getPort(ClientOptions options) { + return options.tls + ? ((options.tlsPort != 0) ? options.tlsPort : Defaults.TLS_PORT) + : ((options.port != 0) ? options.port : Defaults.PORT); + } } diff --git a/lib/src/main/java/io/ably/lib/transport/Hosts.java b/lib/src/main/java/io/ably/lib/transport/Hosts.java index 987065f58..1ef657568 100644 --- a/lib/src/main/java/io/ably/lib/transport/Hosts.java +++ b/lib/src/main/java/io/ably/lib/transport/Hosts.java @@ -12,169 +12,169 @@ * Object to encapsulate primary host name and shuffled fallback host names. */ public class Hosts { - private String primaryHost; - private String prefHost; - private long prefHostExpiry; - boolean primaryHostIsDefault; - private final String defaultHost; - private final String[] fallbackHosts; - private final boolean fallbackHostsIsDefault; - private final boolean fallbackHostsUseDefault; - private final long fallbackRetryTimeout; + private String primaryHost; + private String prefHost; + private long prefHostExpiry; + boolean primaryHostIsDefault; + private final String defaultHost; + private final String[] fallbackHosts; + private final boolean fallbackHostsIsDefault; + private final boolean fallbackHostsUseDefault; + private final long fallbackRetryTimeout; - /** - * Create Hosts object - * - * @param primaryHost the primary hostname, null if not configured - * @param defaultHost the default hostname that the primary hostname must - * match for fallback to occur - * @param options ClientOptions to get environment and fallbackHosts from - * - * The fallback and environment processing here is used when the Hosts - * object is used by a ConnectionManager (for a realtime connection) or by - * an HttpCore for a rest connection. The case where the Hosts object is used - * by an HttpCore that is being used by a ConnectionManager goes through this - * code, but the results are ignored because ConnectionManager then calls - * setHost() and fallback is not used. - */ - public Hosts(String primaryHost, String defaultHost, ClientOptions options) throws AblyException { - this.defaultHost = defaultHost; - if (primaryHost != null) { - setPrimaryHost(primaryHost); - if (options.environment != null) { - /* TO3k2: It is never valid to provide both a restHost and environment value - * TO3k3: It is never valid to provide both a realtimeHost and environment value */ - throw AblyException.fromErrorInfo(new ErrorInfo("cannot set both restHost/realtimeHost and environment options", 40000, 400)); - } - } else if (options.environment != null && !options.environment.equalsIgnoreCase("production")) { - /* RSC11: If ClientOptions.environment is set and is not - * "production", then the primary hostname is set to the default - * hostname with the environment setting used as a prefix. - * Note that this does not happen if there is an explicit setting - * of ClientOptions.restHost or ClientOptions.realtimeHost (as - * appropriate). The spec is not clear on which one should take - * precedence. */ - setPrimaryHost(options.environment + "-" + defaultHost); - } else { - setPrimaryHost(defaultHost); - } - fallbackHostsUseDefault = options.fallbackHostsUseDefault; - if (options.fallbackHosts == null) { - fallbackHosts = Arrays.copyOf(Defaults.HOST_FALLBACKS, Defaults.HOST_FALLBACKS.length); - fallbackHostsIsDefault = true; - } else { - /* RSC15a: use ClientOptions#fallbackHosts if set */ - fallbackHosts = Arrays.copyOf(options.fallbackHosts, options.fallbackHosts.length); - fallbackHostsIsDefault = false; - if (options.fallbackHostsUseDefault) { - /* TO3k7: It is never valid to configure fallbackHost and set - * fallbackHostsUseDefault to true */ - throw AblyException.fromErrorInfo(new ErrorInfo("cannot set both fallbackHosts and fallbackHostsUseDefault options", 40000, 400)); - } - } - /* RSC15a: shuffle the fallback hosts. */ - Collections.shuffle(Arrays.asList(fallbackHosts)); - fallbackRetryTimeout = options.fallbackRetryTimeout; - } + /** + * Create Hosts object + * + * @param primaryHost the primary hostname, null if not configured + * @param defaultHost the default hostname that the primary hostname must + * match for fallback to occur + * @param options ClientOptions to get environment and fallbackHosts from + * + * The fallback and environment processing here is used when the Hosts + * object is used by a ConnectionManager (for a realtime connection) or by + * an HttpCore for a rest connection. The case where the Hosts object is used + * by an HttpCore that is being used by a ConnectionManager goes through this + * code, but the results are ignored because ConnectionManager then calls + * setHost() and fallback is not used. + */ + public Hosts(String primaryHost, String defaultHost, ClientOptions options) throws AblyException { + this.defaultHost = defaultHost; + if (primaryHost != null) { + setPrimaryHost(primaryHost); + if (options.environment != null) { + /* TO3k2: It is never valid to provide both a restHost and environment value + * TO3k3: It is never valid to provide both a realtimeHost and environment value */ + throw AblyException.fromErrorInfo(new ErrorInfo("cannot set both restHost/realtimeHost and environment options", 40000, 400)); + } + } else if (options.environment != null && !options.environment.equalsIgnoreCase("production")) { + /* RSC11: If ClientOptions.environment is set and is not + * "production", then the primary hostname is set to the default + * hostname with the environment setting used as a prefix. + * Note that this does not happen if there is an explicit setting + * of ClientOptions.restHost or ClientOptions.realtimeHost (as + * appropriate). The spec is not clear on which one should take + * precedence. */ + setPrimaryHost(options.environment + "-" + defaultHost); + } else { + setPrimaryHost(defaultHost); + } + fallbackHostsUseDefault = options.fallbackHostsUseDefault; + if (options.fallbackHosts == null) { + fallbackHosts = Arrays.copyOf(Defaults.HOST_FALLBACKS, Defaults.HOST_FALLBACKS.length); + fallbackHostsIsDefault = true; + } else { + /* RSC15a: use ClientOptions#fallbackHosts if set */ + fallbackHosts = Arrays.copyOf(options.fallbackHosts, options.fallbackHosts.length); + fallbackHostsIsDefault = false; + if (options.fallbackHostsUseDefault) { + /* TO3k7: It is never valid to configure fallbackHost and set + * fallbackHostsUseDefault to true */ + throw AblyException.fromErrorInfo(new ErrorInfo("cannot set both fallbackHosts and fallbackHostsUseDefault options", 40000, 400)); + } + } + /* RSC15a: shuffle the fallback hosts. */ + Collections.shuffle(Arrays.asList(fallbackHosts)); + fallbackRetryTimeout = options.fallbackRetryTimeout; + } - /** - * set primary hostname - */ - private void setPrimaryHost(String primaryHost) { - this.primaryHost = primaryHost; - primaryHostIsDefault = primaryHost.equalsIgnoreCase(defaultHost); - } + /** + * set primary hostname + */ + private void setPrimaryHost(String primaryHost) { + this.primaryHost = primaryHost; + primaryHostIsDefault = primaryHost.equalsIgnoreCase(defaultHost); + } - /** - * set preferred hostname, which might not be the primary - */ - public void setPreferredHost(String prefHost, boolean temporary) { - if(prefHost.equals(this.prefHost)) { - /* a successful request against a fallback; don't update the expiry time */ - return; - } - if(prefHost.equals(this.primaryHost)) { - /* a successful request against the primary host; reset */ - clearPreferredHost(); - } else { - this.prefHost = prefHost; - this.prefHostExpiry = temporary ? System.currentTimeMillis() + fallbackRetryTimeout : 0; - } - } + /** + * set preferred hostname, which might not be the primary + */ + public void setPreferredHost(String prefHost, boolean temporary) { + if(prefHost.equals(this.prefHost)) { + /* a successful request against a fallback; don't update the expiry time */ + return; + } + if(prefHost.equals(this.primaryHost)) { + /* a successful request against the primary host; reset */ + clearPreferredHost(); + } else { + this.prefHost = prefHost; + this.prefHostExpiry = temporary ? System.currentTimeMillis() + fallbackRetryTimeout : 0; + } + } - private void clearPreferredHost() { - this.prefHost = null; - this.prefHostExpiry = 0; - } + private void clearPreferredHost() { + this.prefHost = null; + this.prefHostExpiry = 0; + } - /** - * Get primary host name - */ - public String getPrimaryHost() { - return primaryHost; - } + /** + * Get primary host name + */ + public String getPrimaryHost() { + return primaryHost; + } - /** - * Get preferred host name (taking into account any affinity to a fallback: see RSC15f) - */ - public String getPreferredHost() { - checkPreferredHostExpiry(); - return (prefHost == null) ? primaryHost : prefHost; - } + /** + * Get preferred host name (taking into account any affinity to a fallback: see RSC15f) + */ + public String getPreferredHost() { + checkPreferredHostExpiry(); + return (prefHost == null) ? primaryHost : prefHost; + } - private String checkPreferredHostExpiry() { - /* reset if expired */ - if(prefHostExpiry > 0 && prefHostExpiry <= System.currentTimeMillis()) { - prefHostExpiry = 0; - prefHost = null; - } - return prefHost; - } + private String checkPreferredHostExpiry() { + /* reset if expired */ + if(prefHostExpiry > 0 && prefHostExpiry <= System.currentTimeMillis()) { + prefHostExpiry = 0; + prefHost = null; + } + return prefHost; + } - /** - * Get next fallback host if any - * - * @param lastHost - * @return Successor host that can be used as a fallback. - * null, if there is no successor fallback available. - */ - public String getFallback(String lastHost) { - if (fallbackHosts == null) - return null; - int idx; - if (lastHost.equals(primaryHost)) { - /* RSC15b, RTN17b: only use fallback if the hostname has not been overridden - * or if ClientOptions#fallbackHostsUseDefault is true - * or if ClientOptions#fallbackHosts was provided. */ - if (!primaryHostIsDefault && !fallbackHostsUseDefault && fallbackHostsIsDefault) - return null; - idx = 0; - } else if(lastHost.equals(checkPreferredHostExpiry())) { - /* RSC15f: there was a failure on an unexpired, cached fallback; so try again using the primary */ - clearPreferredHost(); - return primaryHost; - } else { - /* Onto next fallback. */ - idx = Arrays.asList(fallbackHosts).indexOf(lastHost); - if (idx < 0) { - return null; - } - ++idx; - } - if (idx >= fallbackHosts.length) { - return null; - } - return fallbackHosts[idx]; - } + /** + * Get next fallback host if any + * + * @param lastHost + * @return Successor host that can be used as a fallback. + * null, if there is no successor fallback available. + */ + public String getFallback(String lastHost) { + if (fallbackHosts == null) + return null; + int idx; + if (lastHost.equals(primaryHost)) { + /* RSC15b, RTN17b: only use fallback if the hostname has not been overridden + * or if ClientOptions#fallbackHostsUseDefault is true + * or if ClientOptions#fallbackHosts was provided. */ + if (!primaryHostIsDefault && !fallbackHostsUseDefault && fallbackHostsIsDefault) + return null; + idx = 0; + } else if(lastHost.equals(checkPreferredHostExpiry())) { + /* RSC15f: there was a failure on an unexpired, cached fallback; so try again using the primary */ + clearPreferredHost(); + return primaryHost; + } else { + /* Onto next fallback. */ + idx = Arrays.asList(fallbackHosts).indexOf(lastHost); + if (idx < 0) { + return null; + } + ++idx; + } + if (idx >= fallbackHosts.length) { + return null; + } + return fallbackHosts[idx]; + } - public int fallbackHostsRemaining(String candidateHost) { - if(fallbackHosts == null) { - return 0; - } - if(candidateHost.equals(primaryHost) || candidateHost.equals(prefHost)) { - return fallbackHosts.length; - } - return fallbackHosts.length - Arrays.asList(fallbackHosts).indexOf(candidateHost) - 1; - } + public int fallbackHostsRemaining(String candidateHost) { + if(fallbackHosts == null) { + return 0; + } + if(candidateHost.equals(primaryHost) || candidateHost.equals(prefHost)) { + return fallbackHosts.length; + } + return fallbackHosts.length - Arrays.asList(fallbackHosts).indexOf(candidateHost) - 1; + } } diff --git a/lib/src/main/java/io/ably/lib/transport/ITransport.java b/lib/src/main/java/io/ably/lib/transport/ITransport.java index 9911509cd..875905ff8 100644 --- a/lib/src/main/java/io/ably/lib/transport/ITransport.java +++ b/lib/src/main/java/io/ably/lib/transport/ITransport.java @@ -18,113 +18,113 @@ public interface ITransport { - String TAG = ITransport.class.getName(); - - interface Factory { - /** - * Obtain and instance of this transport based on the specified options. - */ - ITransport getTransport(TransportParams transportParams, ConnectionManager connectionManager); - } - - enum Mode { - clean, - resume, - recover - } - - class TransportParams { - protected ClientOptions options; - protected String host; - protected int port; - protected String connectionKey; - protected String connectionSerial; - protected Mode mode; - protected boolean heartbeats; - - public TransportParams(ClientOptions options) { - this.options = options; - heartbeats = true; /* default to requiring Ably heartbeats */ - } - - public String getHost() { - return host; - } - - public int getPort() { - return port; - } - - public ClientOptions getClientOptions() { - return options; - } - - public Param[] getConnectParams(Param[] baseParams) { - List paramList = new ArrayList(Arrays.asList(baseParams)); - paramList.add(new Param(Defaults.ABLY_VERSION_PARAM, Defaults.ABLY_VERSION)); - paramList.add(new Param("format", (options.useBinaryProtocol ? "msgpack" : "json"))); - if(!options.echoMessages) - paramList.add(new Param("echo", "false")); - if(connectionKey != null) { - mode = Mode.resume; - paramList.add(new Param("resume", connectionKey)); - if(connectionSerial != null) - paramList.add(new Param("connectionSerial", connectionSerial)); - } else if(options.recover != null) { - mode = Mode.recover; - Pattern recoverSpec = Pattern.compile("^([\\w\\-\\!]+):(\\-?\\d+)$"); - Matcher match = recoverSpec.matcher(options.recover); - if(match.matches()) { - paramList.add(new Param("recover", match.group(1))); - paramList.add(new Param("connectionSerial", match.group(2))); - } else { - Log.e(TAG, "Invalid recover string specified"); - } - } - if(options.clientId != null) - paramList.add(new Param("clientId", options.clientId)); - if(!heartbeats) - paramList.add(new Param("heartbeats", "false")); - - if(options.transportParams != null) { - paramList.addAll(Arrays.asList(options.transportParams)); - } - paramList.add(new Param(Defaults.ABLY_LIB_PARAM, Defaults.ABLY_LIB_VERSION)); - Log.d(TAG, "getConnectParams: params = " + paramList); - return paramList.toArray(new Param[paramList.size()]); - } - } - - interface ConnectListener { - void onTransportAvailable(ITransport transport); - void onTransportUnavailable(ITransport transport, ErrorInfo reason); - } - - /** - * Initiate a connection attempt; the transport will be activated, - * and attempt to remain connected, until disconnect() is called. - * @throws AblyException - */ - void connect(ConnectListener connectListener); - - /** - * Close this transport. - */ - void close(); - - /** - * Send a message on the channel - * @param msg - * @throws IOException - */ - void send(ProtocolMessage msg) throws AblyException; - - /** - * Get connection URL - * @return - */ - String getURL(); - - String getHost(); + String TAG = ITransport.class.getName(); + + interface Factory { + /** + * Obtain and instance of this transport based on the specified options. + */ + ITransport getTransport(TransportParams transportParams, ConnectionManager connectionManager); + } + + enum Mode { + clean, + resume, + recover + } + + class TransportParams { + protected ClientOptions options; + protected String host; + protected int port; + protected String connectionKey; + protected String connectionSerial; + protected Mode mode; + protected boolean heartbeats; + + public TransportParams(ClientOptions options) { + this.options = options; + heartbeats = true; /* default to requiring Ably heartbeats */ + } + + public String getHost() { + return host; + } + + public int getPort() { + return port; + } + + public ClientOptions getClientOptions() { + return options; + } + + public Param[] getConnectParams(Param[] baseParams) { + List paramList = new ArrayList(Arrays.asList(baseParams)); + paramList.add(new Param(Defaults.ABLY_VERSION_PARAM, Defaults.ABLY_VERSION)); + paramList.add(new Param("format", (options.useBinaryProtocol ? "msgpack" : "json"))); + if(!options.echoMessages) + paramList.add(new Param("echo", "false")); + if(connectionKey != null) { + mode = Mode.resume; + paramList.add(new Param("resume", connectionKey)); + if(connectionSerial != null) + paramList.add(new Param("connectionSerial", connectionSerial)); + } else if(options.recover != null) { + mode = Mode.recover; + Pattern recoverSpec = Pattern.compile("^([\\w\\-\\!]+):(\\-?\\d+)$"); + Matcher match = recoverSpec.matcher(options.recover); + if(match.matches()) { + paramList.add(new Param("recover", match.group(1))); + paramList.add(new Param("connectionSerial", match.group(2))); + } else { + Log.e(TAG, "Invalid recover string specified"); + } + } + if(options.clientId != null) + paramList.add(new Param("clientId", options.clientId)); + if(!heartbeats) + paramList.add(new Param("heartbeats", "false")); + + if(options.transportParams != null) { + paramList.addAll(Arrays.asList(options.transportParams)); + } + paramList.add(new Param(Defaults.ABLY_LIB_PARAM, Defaults.ABLY_LIB_VERSION)); + Log.d(TAG, "getConnectParams: params = " + paramList); + return paramList.toArray(new Param[paramList.size()]); + } + } + + interface ConnectListener { + void onTransportAvailable(ITransport transport); + void onTransportUnavailable(ITransport transport, ErrorInfo reason); + } + + /** + * Initiate a connection attempt; the transport will be activated, + * and attempt to remain connected, until disconnect() is called. + * @throws AblyException + */ + void connect(ConnectListener connectListener); + + /** + * Close this transport. + */ + void close(); + + /** + * Send a message on the channel + * @param msg + * @throws IOException + */ + void send(ProtocolMessage msg) throws AblyException; + + /** + * Get connection URL + * @return + */ + String getURL(); + + String getHost(); } diff --git a/lib/src/main/java/io/ably/lib/transport/NetworkConnectivity.java b/lib/src/main/java/io/ably/lib/transport/NetworkConnectivity.java index 6816fcfaf..bc1f09823 100644 --- a/lib/src/main/java/io/ably/lib/transport/NetworkConnectivity.java +++ b/lib/src/main/java/io/ably/lib/transport/NetworkConnectivity.java @@ -7,73 +7,73 @@ public abstract class NetworkConnectivity { - public interface NetworkConnectivityListener { - void onNetworkAvailable(); - void onNetworkUnavailable(ErrorInfo reason); - } + public interface NetworkConnectivityListener { + void onNetworkAvailable(); + void onNetworkUnavailable(ErrorInfo reason); + } - public void addListener(NetworkConnectivityListener listener) { - boolean wasEmpty; - synchronized (this) { - wasEmpty = listeners.isEmpty(); - listeners.add(listener); - } - if(wasEmpty) { - onNonempty(); - } - } + public void addListener(NetworkConnectivityListener listener) { + boolean wasEmpty; + synchronized (this) { + wasEmpty = listeners.isEmpty(); + listeners.add(listener); + } + if(wasEmpty) { + onNonempty(); + } + } - public void removeListener(NetworkConnectivityListener listener) { - boolean isEmpty; - synchronized (this) { - listeners.remove(listener); - isEmpty = listeners.isEmpty(); - } - if(isEmpty) { - onEmpty(); - } - } + public void removeListener(NetworkConnectivityListener listener) { + boolean isEmpty; + synchronized (this) { + listeners.remove(listener); + isEmpty = listeners.isEmpty(); + } + if(isEmpty) { + onEmpty(); + } + } - protected void notifyNetworkAvailable() { - NetworkConnectivityListener[] allListeners; - synchronized(this) { - allListeners = listeners.toArray(new NetworkConnectivityListener[listeners.size()]); - } - for(NetworkConnectivityListener listener: allListeners) { - listener.onNetworkAvailable(); - } - } + protected void notifyNetworkAvailable() { + NetworkConnectivityListener[] allListeners; + synchronized(this) { + allListeners = listeners.toArray(new NetworkConnectivityListener[listeners.size()]); + } + for(NetworkConnectivityListener listener: allListeners) { + listener.onNetworkAvailable(); + } + } - protected void notifyNetworkUnavailable(ErrorInfo reason) { - NetworkConnectivityListener[] allListeners; - synchronized(this) { - allListeners = listeners.toArray(new NetworkConnectivityListener[listeners.size()]); - } - for(NetworkConnectivityListener listener: allListeners) { - listener.onNetworkUnavailable(reason); - } - } + protected void notifyNetworkUnavailable(ErrorInfo reason) { + NetworkConnectivityListener[] allListeners; + synchronized(this) { + allListeners = listeners.toArray(new NetworkConnectivityListener[listeners.size()]); + } + for(NetworkConnectivityListener listener: allListeners) { + listener.onNetworkUnavailable(reason); + } + } - protected synchronized boolean isEmpty() { - return listeners.isEmpty(); - } + protected synchronized boolean isEmpty() { + return listeners.isEmpty(); + } - protected void onEmpty() {} + protected void onEmpty() {} - protected void onNonempty() {} + protected void onNonempty() {} - protected Set listeners = new HashSet(); + protected Set listeners = new HashSet(); - public static class DefaultNetworkConnectivity extends NetworkConnectivity {} + public static class DefaultNetworkConnectivity extends NetworkConnectivity {} - public static class DelegatedNetworkConnectivity extends NetworkConnectivity implements NetworkConnectivityListener { - @Override - public void onNetworkAvailable() { - notifyNetworkAvailable(); - } - @Override - public void onNetworkUnavailable(ErrorInfo reason) { - notifyNetworkUnavailable(reason); - } - } + public static class DelegatedNetworkConnectivity extends NetworkConnectivity implements NetworkConnectivityListener { + @Override + public void onNetworkAvailable() { + notifyNetworkAvailable(); + } + @Override + public void onNetworkUnavailable(ErrorInfo reason) { + notifyNetworkUnavailable(reason); + } + } } diff --git a/lib/src/main/java/io/ably/lib/transport/WebSocketTransport.java b/lib/src/main/java/io/ably/lib/transport/WebSocketTransport.java index da724c372..c733703a7 100644 --- a/lib/src/main/java/io/ably/lib/transport/WebSocketTransport.java +++ b/lib/src/main/java/io/ably/lib/transport/WebSocketTransport.java @@ -26,317 +26,317 @@ public class WebSocketTransport implements ITransport { - private static final String TAG = WebSocketTransport.class.getName(); - - /****************** - * public factory API - ******************/ - - public static class Factory implements ITransport.Factory { - @Override - public WebSocketTransport getTransport(TransportParams params, ConnectionManager connectionManager) { - return new WebSocketTransport(params, connectionManager); - } - } - - /****************** - * protected constructor - ******************/ - - protected WebSocketTransport(TransportParams params, ConnectionManager connectionManager) { - this.params = params; - this.connectionManager = connectionManager; - this.channelBinaryMode = params.options.useBinaryProtocol; - /* We do not require Ably heartbeats, as we can use WebSocket pings instead. */ - params.heartbeats = false; - } - - /****************** - * ITransport methods - ******************/ - - @Override - public void connect(ConnectListener connectListener) { - this.connectListener = connectListener; - try { - boolean isTls = params.options.tls; - String wsScheme = isTls ? "wss://" : "ws://"; - wsUri = wsScheme + params.host + ':' + String.valueOf(params.port) + "/"; - Param[] authParams = connectionManager.ably.auth.getAuthParams(); - Param[] connectParams = params.getConnectParams(authParams); - if(connectParams.length > 0) - wsUri = HttpUtils.encodeParams(wsUri, connectParams); - - Log.d(TAG, "connect(); wsUri = " + wsUri); - synchronized(this) { - wsConnection = new WsClient(URI.create(wsUri)); - if(isTls) { - SSLContext sslContext = SSLContext.getInstance("TLS"); - sslContext.init( null, null, null ); - SSLSocketFactory factory = sslContext.getSocketFactory();// (SSLSocketFactory) SSLSocketFactory.getDefault(); - wsConnection.setSocketFactory(factory); - } - } - wsConnection.connect(); - } catch(AblyException e) { - Log.e(TAG, "Unexpected exception attempting connection; wsUri = " + wsUri, e); - connectListener.onTransportUnavailable(this, e.errorInfo); - } catch(Throwable t) { - Log.e(TAG, "Unexpected exception attempting connection; wsUri = " + wsUri, t); - connectListener.onTransportUnavailable(this, AblyException.fromThrowable(t).errorInfo); - } - } - - @Override - public void close() { - Log.d(TAG, "close()"); - synchronized(this) { - if(wsConnection != null) { - wsConnection.close(); - wsConnection = null; - } - } - } - - @Override - public void send(ProtocolMessage msg) throws AblyException { - Log.d(TAG, "send(); action = " + msg.action); - try { - if(channelBinaryMode) { - byte[] encodedMsg = ProtocolSerializer.writeMsgpack(msg); - if (Log.level <= Log.VERBOSE) { - ProtocolMessage decodedMsg = ProtocolSerializer.readMsgpack(encodedMsg); - Log.v(TAG, "send(): " + decodedMsg.action + ": " + new String(ProtocolSerializer.writeJSON(decodedMsg))); - } - wsConnection.send(encodedMsg); - } else { - if (Log.level <= Log.VERBOSE) - Log.v(TAG, "send(): " + new String(ProtocolSerializer.writeJSON(msg))); - wsConnection.send(ProtocolSerializer.writeJSON(msg)); - } - } catch (Exception e) { - throw AblyException.fromThrowable(e); - } - } - - @Override - public String getHost() { - return params.host; - } - - protected void preProcessReceivedMessage(ProtocolMessage message) - { - //Gives the chance to child classes to do message pre-processing - } - - /************************** - * WebSocketHandler methods - **************************/ - - class WsClient extends WebSocketClient { - - WsClient(URI serverUri) { - super(serverUri); - } - - @Override - public void onOpen(ServerHandshake handshakedata) { - Log.d(TAG, "onOpen()"); - connectListener.onTransportAvailable(WebSocketTransport.this); - flagActivity(); - } - - @Override - public void onMessage(ByteBuffer blob) { - try { - ProtocolMessage msg = ProtocolSerializer.readMsgpack(blob.array()); - Log.d(TAG, "onMessage(): msg (binary) = " + msg); - WebSocketTransport.this.preProcessReceivedMessage(msg); - connectionManager.onMessage(WebSocketTransport.this, msg); - } catch (AblyException e) { - String msg = "Unexpected exception processing received binary message"; - Log.e(TAG, msg, e); - } - flagActivity(); - } - - @Override - public void onMessage(String string) { - try { - ProtocolMessage msg = ProtocolSerializer.fromJSON(string); - Log.d(TAG, "onMessage(): msg (text) = " + msg); - WebSocketTransport.this.preProcessReceivedMessage(msg); - connectionManager.onMessage(WebSocketTransport.this, msg); - } catch (AblyException e) { - String msg = "Unexpected exception processing received text message"; - Log.e(TAG, msg, e); - } - flagActivity(); - } - - /* This allows us to detect a websocket ping, so we don't need Ably pings. */ - @Override - public void onWebsocketPing( WebSocket conn, Framedata f ) { - Log.d(TAG, "onWebsocketPing()"); - /* Call superclass to ensure the pong is sent. */ - super.onWebsocketPing( conn, f ); - flagActivity(); - } - - @Override - public void onClose(final int wsCode, final String wsReason, final boolean remote) { - Log.d(TAG, "onClose(): wsCode = " + wsCode + "; wsReason = " + wsReason + "; remote = " + remote); - - ErrorInfo reason; - switch(wsCode) { - case NEVER_CONNECTED: - case CLOSE_NORMAL: - case BUGGYCLOSE: - case GOING_AWAY: - case ABNORMAL_CLOSE: - /* we don't know the specific reason that the connection closed in these cases, - * but we have to assume it's a problem with connectivity rather than some other - * application problem */ - reason = ConnectionManager.REASON_DISCONNECTED; - break; - case REFUSE: - case POLICY_VALIDATION: - reason = ConnectionManager.REASON_REFUSED; - break; - case TOOBIG: - reason = ConnectionManager.REASON_TOO_BIG; - break; - case NO_UTF8: - case CLOSE_PROTOCOL_ERROR: - case UNEXPECTED_CONDITION: - case EXTENSION: - case TLS_ERROR: - default: - /* we don't know the specific reason that the connection closed in these cases, - * but we have to assume it's an application problem, and the problem will - * recur if we try again. The failed state means that we won't automatically - * try again. */ - reason = ConnectionManager.REASON_FAILED; - break; - } - connectListener.onTransportUnavailable(WebSocketTransport.this, reason); - dispose(); - } - - @Override - public void onError(final Exception e) { - connectListener.onTransportUnavailable(WebSocketTransport.this, new ErrorInfo(e.getMessage(), 503, 80000)); - } - - private synchronized void dispose() { - /* dispose timer */ - try { - timer.cancel(); - timer = null; - } catch(IllegalStateException e) {} - } - - private synchronized void flagActivity() { - lastActivityTime = System.currentTimeMillis(); - connectionManager.setLastActivity(lastActivityTime); - if (activityTimerTask == null && connectionManager.maxIdleInterval != 0) { - /* No timer currently running because previously there was no - * maxIdleInterval configured, but now there is a - * maxIdleInterval configured. Call checkActivity so a timer - * gets started. This happens when flagActivity gets called - * just after processing the connect message that configures - * maxIdleInterval. */ - checkActivity(); - } - } - - private synchronized void checkActivity() { - long timeout = connectionManager.maxIdleInterval; - if (timeout == 0) { - Log.v(TAG, "checkActivity: infinite timeout"); - return; - } - if(activityTimerTask != null) { - /* timer already running */ - return; - } - timeout += connectionManager.ably.options.realtimeRequestTimeout; - long now = System.currentTimeMillis(); - long next = lastActivityTime + timeout; - if (now < next) { - /* We have not reached maxIdleInterval+realtimeRequestTimeout - * of inactivity. Schedule a new timer for that long after the - * last activity time. */ - Log.v(TAG, "checkActivity: ok"); - schedule((activityTimerTask = new TimerTask() { - public void run() { - try { - checkActivity(); - } catch(Throwable t) { - Log.e(TAG, "Unexpected exception in activity timer handler", t); - } - } - }), next - now); - } else { - /* Timeout has been reached. Close the connection. */ - Log.e(TAG, "No activity for " + timeout + "ms, closing connection"); - closeConnection(CloseFrame.ABNORMAL_CLOSE, "timed out"); - } - } - - private synchronized void schedule(TimerTask task, long delay) { - if(timer != null) { - try { - timer.schedule(task, delay); - } catch(IllegalStateException ise) { - Log.e(TAG, "Unexpected exception scheduling activity timer", ise); - } - } - } - - /*************************** - * WsClient private members - ***************************/ - - private Timer timer = new Timer(); - private TimerTask activityTimerTask = null; - private long lastActivityTime; - } - - public String toString() { - return WebSocketTransport.class.getName() + " [" + getURL() + "]"; - } - - public String getURL() { - return wsUri; - } - - /****************** - * private members - ******************/ - - private final TransportParams params; - private final ConnectionManager connectionManager; - private final boolean channelBinaryMode; - private String wsUri; - private ConnectListener connectListener; - - private WsClient wsConnection; - - private static final int NEVER_CONNECTED = -1; - private static final int BUGGYCLOSE = -2; - private static final int CLOSE_NORMAL = 1000; - private static final int GOING_AWAY = 1001; - private static final int CLOSE_PROTOCOL_ERROR = 1002; - private static final int REFUSE = 1003; + private static final String TAG = WebSocketTransport.class.getName(); + + /****************** + * public factory API + ******************/ + + public static class Factory implements ITransport.Factory { + @Override + public WebSocketTransport getTransport(TransportParams params, ConnectionManager connectionManager) { + return new WebSocketTransport(params, connectionManager); + } + } + + /****************** + * protected constructor + ******************/ + + protected WebSocketTransport(TransportParams params, ConnectionManager connectionManager) { + this.params = params; + this.connectionManager = connectionManager; + this.channelBinaryMode = params.options.useBinaryProtocol; + /* We do not require Ably heartbeats, as we can use WebSocket pings instead. */ + params.heartbeats = false; + } + + /****************** + * ITransport methods + ******************/ + + @Override + public void connect(ConnectListener connectListener) { + this.connectListener = connectListener; + try { + boolean isTls = params.options.tls; + String wsScheme = isTls ? "wss://" : "ws://"; + wsUri = wsScheme + params.host + ':' + String.valueOf(params.port) + "/"; + Param[] authParams = connectionManager.ably.auth.getAuthParams(); + Param[] connectParams = params.getConnectParams(authParams); + if(connectParams.length > 0) + wsUri = HttpUtils.encodeParams(wsUri, connectParams); + + Log.d(TAG, "connect(); wsUri = " + wsUri); + synchronized(this) { + wsConnection = new WsClient(URI.create(wsUri)); + if(isTls) { + SSLContext sslContext = SSLContext.getInstance("TLS"); + sslContext.init( null, null, null ); + SSLSocketFactory factory = sslContext.getSocketFactory();// (SSLSocketFactory) SSLSocketFactory.getDefault(); + wsConnection.setSocketFactory(factory); + } + } + wsConnection.connect(); + } catch(AblyException e) { + Log.e(TAG, "Unexpected exception attempting connection; wsUri = " + wsUri, e); + connectListener.onTransportUnavailable(this, e.errorInfo); + } catch(Throwable t) { + Log.e(TAG, "Unexpected exception attempting connection; wsUri = " + wsUri, t); + connectListener.onTransportUnavailable(this, AblyException.fromThrowable(t).errorInfo); + } + } + + @Override + public void close() { + Log.d(TAG, "close()"); + synchronized(this) { + if(wsConnection != null) { + wsConnection.close(); + wsConnection = null; + } + } + } + + @Override + public void send(ProtocolMessage msg) throws AblyException { + Log.d(TAG, "send(); action = " + msg.action); + try { + if(channelBinaryMode) { + byte[] encodedMsg = ProtocolSerializer.writeMsgpack(msg); + if (Log.level <= Log.VERBOSE) { + ProtocolMessage decodedMsg = ProtocolSerializer.readMsgpack(encodedMsg); + Log.v(TAG, "send(): " + decodedMsg.action + ": " + new String(ProtocolSerializer.writeJSON(decodedMsg))); + } + wsConnection.send(encodedMsg); + } else { + if (Log.level <= Log.VERBOSE) + Log.v(TAG, "send(): " + new String(ProtocolSerializer.writeJSON(msg))); + wsConnection.send(ProtocolSerializer.writeJSON(msg)); + } + } catch (Exception e) { + throw AblyException.fromThrowable(e); + } + } + + @Override + public String getHost() { + return params.host; + } + + protected void preProcessReceivedMessage(ProtocolMessage message) + { + //Gives the chance to child classes to do message pre-processing + } + + /************************** + * WebSocketHandler methods + **************************/ + + class WsClient extends WebSocketClient { + + WsClient(URI serverUri) { + super(serverUri); + } + + @Override + public void onOpen(ServerHandshake handshakedata) { + Log.d(TAG, "onOpen()"); + connectListener.onTransportAvailable(WebSocketTransport.this); + flagActivity(); + } + + @Override + public void onMessage(ByteBuffer blob) { + try { + ProtocolMessage msg = ProtocolSerializer.readMsgpack(blob.array()); + Log.d(TAG, "onMessage(): msg (binary) = " + msg); + WebSocketTransport.this.preProcessReceivedMessage(msg); + connectionManager.onMessage(WebSocketTransport.this, msg); + } catch (AblyException e) { + String msg = "Unexpected exception processing received binary message"; + Log.e(TAG, msg, e); + } + flagActivity(); + } + + @Override + public void onMessage(String string) { + try { + ProtocolMessage msg = ProtocolSerializer.fromJSON(string); + Log.d(TAG, "onMessage(): msg (text) = " + msg); + WebSocketTransport.this.preProcessReceivedMessage(msg); + connectionManager.onMessage(WebSocketTransport.this, msg); + } catch (AblyException e) { + String msg = "Unexpected exception processing received text message"; + Log.e(TAG, msg, e); + } + flagActivity(); + } + + /* This allows us to detect a websocket ping, so we don't need Ably pings. */ + @Override + public void onWebsocketPing( WebSocket conn, Framedata f ) { + Log.d(TAG, "onWebsocketPing()"); + /* Call superclass to ensure the pong is sent. */ + super.onWebsocketPing( conn, f ); + flagActivity(); + } + + @Override + public void onClose(final int wsCode, final String wsReason, final boolean remote) { + Log.d(TAG, "onClose(): wsCode = " + wsCode + "; wsReason = " + wsReason + "; remote = " + remote); + + ErrorInfo reason; + switch(wsCode) { + case NEVER_CONNECTED: + case CLOSE_NORMAL: + case BUGGYCLOSE: + case GOING_AWAY: + case ABNORMAL_CLOSE: + /* we don't know the specific reason that the connection closed in these cases, + * but we have to assume it's a problem with connectivity rather than some other + * application problem */ + reason = ConnectionManager.REASON_DISCONNECTED; + break; + case REFUSE: + case POLICY_VALIDATION: + reason = ConnectionManager.REASON_REFUSED; + break; + case TOOBIG: + reason = ConnectionManager.REASON_TOO_BIG; + break; + case NO_UTF8: + case CLOSE_PROTOCOL_ERROR: + case UNEXPECTED_CONDITION: + case EXTENSION: + case TLS_ERROR: + default: + /* we don't know the specific reason that the connection closed in these cases, + * but we have to assume it's an application problem, and the problem will + * recur if we try again. The failed state means that we won't automatically + * try again. */ + reason = ConnectionManager.REASON_FAILED; + break; + } + connectListener.onTransportUnavailable(WebSocketTransport.this, reason); + dispose(); + } + + @Override + public void onError(final Exception e) { + connectListener.onTransportUnavailable(WebSocketTransport.this, new ErrorInfo(e.getMessage(), 503, 80000)); + } + + private synchronized void dispose() { + /* dispose timer */ + try { + timer.cancel(); + timer = null; + } catch(IllegalStateException e) {} + } + + private synchronized void flagActivity() { + lastActivityTime = System.currentTimeMillis(); + connectionManager.setLastActivity(lastActivityTime); + if (activityTimerTask == null && connectionManager.maxIdleInterval != 0) { + /* No timer currently running because previously there was no + * maxIdleInterval configured, but now there is a + * maxIdleInterval configured. Call checkActivity so a timer + * gets started. This happens when flagActivity gets called + * just after processing the connect message that configures + * maxIdleInterval. */ + checkActivity(); + } + } + + private synchronized void checkActivity() { + long timeout = connectionManager.maxIdleInterval; + if (timeout == 0) { + Log.v(TAG, "checkActivity: infinite timeout"); + return; + } + if(activityTimerTask != null) { + /* timer already running */ + return; + } + timeout += connectionManager.ably.options.realtimeRequestTimeout; + long now = System.currentTimeMillis(); + long next = lastActivityTime + timeout; + if (now < next) { + /* We have not reached maxIdleInterval+realtimeRequestTimeout + * of inactivity. Schedule a new timer for that long after the + * last activity time. */ + Log.v(TAG, "checkActivity: ok"); + schedule((activityTimerTask = new TimerTask() { + public void run() { + try { + checkActivity(); + } catch(Throwable t) { + Log.e(TAG, "Unexpected exception in activity timer handler", t); + } + } + }), next - now); + } else { + /* Timeout has been reached. Close the connection. */ + Log.e(TAG, "No activity for " + timeout + "ms, closing connection"); + closeConnection(CloseFrame.ABNORMAL_CLOSE, "timed out"); + } + } + + private synchronized void schedule(TimerTask task, long delay) { + if(timer != null) { + try { + timer.schedule(task, delay); + } catch(IllegalStateException ise) { + Log.e(TAG, "Unexpected exception scheduling activity timer", ise); + } + } + } + + /*************************** + * WsClient private members + ***************************/ + + private Timer timer = new Timer(); + private TimerTask activityTimerTask = null; + private long lastActivityTime; + } + + public String toString() { + return WebSocketTransport.class.getName() + " [" + getURL() + "]"; + } + + public String getURL() { + return wsUri; + } + + /****************** + * private members + ******************/ + + private final TransportParams params; + private final ConnectionManager connectionManager; + private final boolean channelBinaryMode; + private String wsUri; + private ConnectListener connectListener; + + private WsClient wsConnection; + + private static final int NEVER_CONNECTED = -1; + private static final int BUGGYCLOSE = -2; + private static final int CLOSE_NORMAL = 1000; + private static final int GOING_AWAY = 1001; + private static final int CLOSE_PROTOCOL_ERROR = 1002; + private static final int REFUSE = 1003; /* private static final int UNUSED = 1004; */ /* private static final int NOCODE = 1005; */ - private static final int ABNORMAL_CLOSE = 1006; - private static final int NO_UTF8 = 1007; - private static final int POLICY_VALIDATION = 1008; - private static final int TOOBIG = 1009; - private static final int EXTENSION = 1010; - private static final int UNEXPECTED_CONDITION = 1011; - private static final int TLS_ERROR = 1015; + private static final int ABNORMAL_CLOSE = 1006; + private static final int NO_UTF8 = 1007; + private static final int POLICY_VALIDATION = 1008; + private static final int TOOBIG = 1009; + private static final int EXTENSION = 1010; + private static final int UNEXPECTED_CONDITION = 1011; + private static final int TLS_ERROR = 1015; } diff --git a/lib/src/main/java/io/ably/lib/types/AblyException.java b/lib/src/main/java/io/ably/lib/types/AblyException.java index 781893511..60b0b2c95 100644 --- a/lib/src/main/java/io/ably/lib/types/AblyException.java +++ b/lib/src/main/java/io/ably/lib/types/AblyException.java @@ -9,56 +9,56 @@ * An exception type encapsulating an Ably error code */ public class AblyException extends Exception { - private static final long serialVersionUID = -3804072091596832634L; - public ErrorInfo errorInfo; + private static final long serialVersionUID = -3804072091596832634L; + public ErrorInfo errorInfo; - /** - * Constructor for use where there is an ErrorInfo available - */ - protected AblyException(Throwable throwable, ErrorInfo reason) { - super(throwable); - this.errorInfo = reason; - } + /** + * Constructor for use where there is an ErrorInfo available + */ + protected AblyException(Throwable throwable, ErrorInfo reason) { + super(throwable); + this.errorInfo = reason; + } - public static AblyException fromErrorInfo(ErrorInfo errorInfo) { - return fromErrorInfo(new Exception(errorInfo.message), errorInfo); - } + public static AblyException fromErrorInfo(ErrorInfo errorInfo) { + return fromErrorInfo(new Exception(errorInfo.message), errorInfo); + } - public static AblyException fromErrorInfo(Throwable t, ErrorInfo errorInfo) { - /* If status code is one of server error HTTP response codes */ - if (errorInfo.statusCode >= 500 && - errorInfo.statusCode <= 504) { - return new HostFailedException( - t, - errorInfo - ); - } + public static AblyException fromErrorInfo(Throwable t, ErrorInfo errorInfo) { + /* If status code is one of server error HTTP response codes */ + if (errorInfo.statusCode >= 500 && + errorInfo.statusCode <= 504) { + return new HostFailedException( + t, + errorInfo + ); + } - return new AblyException( - t, - errorInfo); - } + return new AblyException( + t, + errorInfo); + } - /** - * Get an exception from a throwable occurring locally - * @param t - * @return - */ - public static AblyException fromThrowable(Throwable t) { - if(t instanceof AblyException) - return (AblyException)t; - if(t instanceof ConnectException || t instanceof SocketTimeoutException || t instanceof UnknownHostException || t instanceof NoRouteToHostException) - return new HostFailedException(t, ErrorInfo.fromThrowable(t)); + /** + * Get an exception from a throwable occurring locally + * @param t + * @return + */ + public static AblyException fromThrowable(Throwable t) { + if(t instanceof AblyException) + return (AblyException)t; + if(t instanceof ConnectException || t instanceof SocketTimeoutException || t instanceof UnknownHostException || t instanceof NoRouteToHostException) + return new HostFailedException(t, ErrorInfo.fromThrowable(t)); - return new AblyException(t, ErrorInfo.fromThrowable(t)); - } + return new AblyException(t, ErrorInfo.fromThrowable(t)); + } - public static class HostFailedException extends AblyException { - private static final long serialVersionUID = 1L; + public static class HostFailedException extends AblyException { + private static final long serialVersionUID = 1L; - HostFailedException(Throwable throwable, ErrorInfo reason) { - super(throwable, reason); - } - } + HostFailedException(Throwable throwable, ErrorInfo reason) { + super(throwable, reason); + } + } } \ No newline at end of file diff --git a/lib/src/main/java/io/ably/lib/types/AsyncHttpPaginatedResponse.java b/lib/src/main/java/io/ably/lib/types/AsyncHttpPaginatedResponse.java index 9146d4eea..16aef5167 100644 --- a/lib/src/main/java/io/ably/lib/types/AsyncHttpPaginatedResponse.java +++ b/lib/src/main/java/io/ably/lib/types/AsyncHttpPaginatedResponse.java @@ -3,42 +3,42 @@ import com.google.gson.JsonElement; public abstract class AsyncHttpPaginatedResponse { - public boolean success; - public int statusCode; - public int errorCode; - public String errorMessage; - public Param[] headers; + public boolean success; + public int statusCode; + public int errorCode; + public String errorMessage; + public Param[] headers; - /** - * Get the contents as an array of component type - */ - public abstract JsonElement[] items(); + /** + * Get the contents as an array of component type + */ + public abstract JsonElement[] items(); - /** - * Obtain params required to perform the given relative query - */ - public abstract void first(Callback callback); - public abstract void current(Callback callback); - public abstract void next(Callback callback); + /** + * Obtain params required to perform the given relative query + */ + public abstract void first(Callback callback); + public abstract void current(Callback callback); + public abstract void next(Callback callback); - public abstract boolean hasFirst(); - public abstract boolean hasCurrent(); - public abstract boolean hasNext(); + public abstract boolean hasFirst(); + public abstract boolean hasCurrent(); + public abstract boolean hasNext(); - /** - * An interface allowing a client to be notified of the outcome - * of an asynchronous operation. - */ - public interface Callback { - /** - * Called when the associated request completes with an Http response, - */ - void onResponse(AsyncHttpPaginatedResponse response); + /** + * An interface allowing a client to be notified of the outcome + * of an asynchronous operation. + */ + public interface Callback { + /** + * Called when the associated request completes with an Http response, + */ + void onResponse(AsyncHttpPaginatedResponse response); - /** - * Called when the associated operation completes with an error. - * @param reason: information about the error. - */ - void onError(ErrorInfo reason); - } + /** + * Called when the associated operation completes with an error. + * @param reason: information about the error. + */ + void onError(ErrorInfo reason); + } } diff --git a/lib/src/main/java/io/ably/lib/types/AsyncPaginatedResult.java b/lib/src/main/java/io/ably/lib/types/AsyncPaginatedResult.java index 1e7e5d133..abd05397f 100644 --- a/lib/src/main/java/io/ably/lib/types/AsyncPaginatedResult.java +++ b/lib/src/main/java/io/ably/lib/types/AsyncPaginatedResult.java @@ -9,19 +9,19 @@ */ public interface AsyncPaginatedResult { - /** - * Get the contents as an array of component type - */ - T[] items(); + /** + * Get the contents as an array of component type + */ + T[] items(); - /** - * Obtain params required to perform the given relative query - */ - void first(Callback> callback); - void current(Callback> callback); - void next(Callback> callback); + /** + * Obtain params required to perform the given relative query + */ + void first(Callback> callback); + void current(Callback> callback); + void next(Callback> callback); - boolean hasFirst(); - boolean hasCurrent(); - boolean hasNext(); + boolean hasFirst(); + boolean hasCurrent(); + boolean hasNext(); } diff --git a/lib/src/main/java/io/ably/lib/types/BaseMessage.java b/lib/src/main/java/io/ably/lib/types/BaseMessage.java index 29c0f003f..3913796c9 100644 --- a/lib/src/main/java/io/ably/lib/types/BaseMessage.java +++ b/lib/src/main/java/io/ably/lib/types/BaseMessage.java @@ -22,319 +22,319 @@ import java.util.regex.Pattern; public class BaseMessage implements Cloneable { - /** - * A unique id for this message - */ - public String id; - - /** - * The timestamp for this message - */ - public long timestamp; - - /** - * The id of the publisher of this message - */ - public String clientId; - - /** - * The connection id of the publisher of this message - */ - public String connectionId; - - /** - * Any transformation applied to the data for this message - */ - public String encoding; - - /** - * The message payload. - */ - public Object data; - - private static final String TIMESTAMP = "timestamp"; - private static final String ID = "id"; - private static final String CLIENT_ID = "clientId"; - private static final String CONNECTION_ID = "connectionId"; - private static final String ENCODING = "encoding"; - private static final String DATA = "data"; - - /** - * Generate a String summary of this BaseMessage - * @return string - */ - public void getDetails(StringBuilder builder) { - if(clientId != null) - builder.append(" clientId=").append(clientId); - if(connectionId != null) - builder.append(" connectionId=").append(connectionId); - if(data != null) - builder.append(" data=").append(data); - if(encoding != null) - builder.append(" encoding=").append(encoding); - if(id != null) - builder.append(" id=").append(id); - } - - public void decode(ChannelOptions opts) throws MessageDecodeException { - - this.decode(opts, new DecodingContext()); - } - - private final static VCDiffDecoder vcdiffDecoder = VCDiffDecoderBuilder.builder().buildSimple(); - - private static byte[] vcdiffApply(byte[] delta, byte[] base) throws MessageDecodeException { - try { - ByteArrayOutputStream decoded = new ByteArrayOutputStream(); - vcdiffDecoder.decode(base, delta, decoded); - return decoded.toByteArray(); - } catch (Throwable t) { - throw MessageDecodeException.fromThrowableAndErrorInfo(t, new ErrorInfo("VCDIFF delta decode failed", 400, 40018)); - } - } - - public void decode(ChannelOptions opts, DecodingContext context) throws MessageDecodeException { - - Object lastPayload = data; - - if(encoding != null) { - String[] xforms = encoding.split("\\/"); - int lastProcessedEncodingIndex = 0, encodingsToProcess = xforms.length; - try { - while((lastProcessedEncodingIndex = encodingsToProcess ) > 0) { - Matcher match = xformPattern.matcher(xforms[--encodingsToProcess ]); - if(!match.matches()) break; - switch(match.group(1)) { - case "base64": - try { - data = Base64Coder.decode((String) data); - } catch (IllegalArgumentException e) { - throw MessageDecodeException.fromDescription("Invalid base64 data received"); - } - if(lastProcessedEncodingIndex == xforms.length) { - lastPayload = data; - } - continue; - - case "utf-8": - try { data = new String((byte[])data, "UTF-8"); } catch(UnsupportedEncodingException|ClassCastException e) {} - continue; - - case "json": - try { - String jsonText = ((String)data).trim(); - data = Serialisation.gsonParser.parse(jsonText); - } catch(JsonParseException e) { - throw MessageDecodeException.fromDescription("Invalid JSON data received"); - } - continue; - - case "cipher": - if(opts != null && opts.encrypted) { - try { - data = opts.getCipher().decrypt((byte[]) data); - } catch(AblyException e) { - throw MessageDecodeException.fromDescription(e.errorInfo.message); - } - continue; - } - else { - throw MessageDecodeException.fromDescription("Encrypted message received but encryption is not set up"); - } - case "vcdiff": - data = vcdiffApply((byte[]) data, context.getLastMessageData()); - lastPayload = data; - - continue; - } - break; - } - } finally { - encoding = (lastProcessedEncodingIndex <= 0) ? null : join(xforms, '/', 0, lastProcessedEncodingIndex ); - } - } - - //last message bookkeping - if(lastPayload instanceof String) - context.setLastMessageData((String)lastPayload); - else if (lastPayload instanceof byte[]) - context.setLastMessageData((byte[])lastPayload); - else - throw MessageDecodeException.fromDescription("Message data neither String nor byte[]. Unsupported message data type."); - } - - public void encode(ChannelOptions opts) throws AblyException { - if(data != null) { - if(data instanceof JsonElement) { - data = Serialisation.gson.toJson((JsonElement)data); - encoding = ((encoding == null) ? "" : encoding + "/") + "json"; - } - if(data instanceof String) { - if (opts != null && opts.encrypted) { - try { data = ((String)data).getBytes("UTF-8"); } catch(UnsupportedEncodingException e) {} - encoding = ((encoding == null) ? "" : encoding + "/") + "utf-8"; - } - } else if(!(data instanceof byte[])) { - Log.d(TAG, "Message data must be either `byte[]`, `String` or `JSONElement`; implicit coercion of other types to String is deprecated"); - throw AblyException.fromErrorInfo(new ErrorInfo("Invalid message data or encoding", 400, 40013)); - } - } - if (opts != null && opts.encrypted) { - ChannelCipher cipher = opts.getCipher(); - data = cipher.encrypt((byte[]) data); - encoding = ((encoding == null) ? "" : encoding + "/") + "cipher+" + cipher.getAlgorithm(); - } - } - - /* trivial utilities for processing encoding string */ - private static Pattern xformPattern = Pattern.compile("([\\-\\w]+)(\\+([\\-\\w]+))?"); - private String join(String[] elements, char separator, int start, int end) { - StringBuilder result = new StringBuilder(elements[start++]); - for(int i = start; i < end; i++) - result.append(separator).append(elements[i]); - return result.toString(); - } - - /** - * Base for gson serialisers. - */ - public static JsonObject toJsonObject(final BaseMessage message) { - JsonObject json = new JsonObject(); - Object data = message.data; - String encoding = message.encoding; - if(data != null) { - if(data instanceof byte[]) { - byte[] dataBytes = (byte[])data; - json.addProperty("data", new String(Base64Coder.encode(dataBytes))); - encoding = (encoding == null) ? "base64" : encoding + "/base64"; - } else { - json.addProperty("data", data.toString()); - } - if(encoding != null) json.addProperty("encoding", encoding); - } - if(message.id != null) json.addProperty("id", message.id); - if(message.clientId != null) json.addProperty("clientId", message.clientId); - if(message.connectionId != null) json.addProperty("connectionId", message.connectionId); - return json; - } - - /** - * Populate fields from JSON. - */ - protected void read(final JsonObject map) throws MessageDecodeException { - final Long optionalTimestamp = readLong(map, TIMESTAMP); - if (null != optionalTimestamp) { - timestamp = optionalTimestamp; // unbox - } - - id = readString(map, ID); - clientId = readString(map, CLIENT_ID); - connectionId = readString(map, CONNECTION_ID); - encoding = readString(map, ENCODING); - data = readString(map, DATA); - } - - /** - * Read an optional textual value. - * @return The value, or null if the key was not present in the map. - * @throws ClassCastException if an element exists for that key and that element is not a {@link JsonPrimitive} - * or is not a valid string value. - */ - protected String readString(final JsonObject map, final String key) { - final JsonElement element = map.get(key); - if (null == element || element instanceof JsonNull) { - return null; - } - return element.getAsString(); - } - - /** - * Read an optional numerical value. - * @return The value, or null if the key was not present in the map. - * @throws ClassCastException if an element exists for that key and that element is not a {@link JsonPrimitive} - * or is not a valid long value. - */ - protected Long readLong(final JsonObject map, final String key) { - final JsonElement element = map.get(key); - if (null == element || element instanceof JsonNull) { - return null; - } - return element.getAsLong(); - } - - /* Msgpack processing */ - boolean readField(MessageUnpacker unpacker, String fieldName, MessageFormat fieldType) throws IOException { - boolean result = true; - switch (fieldName) { - case TIMESTAMP: - timestamp = unpacker.unpackLong(); break; - case ID: - id = unpacker.unpackString(); break; - case CLIENT_ID: - clientId = unpacker.unpackString(); break; - case CONNECTION_ID: - connectionId = unpacker.unpackString(); break; - case ENCODING: - encoding = unpacker.unpackString(); break; - case DATA: - if(fieldType.getValueType().isBinaryType()) { - byte[] byteData = new byte[unpacker.unpackBinaryHeader()]; - unpacker.readPayload(byteData); - data = byteData; - } else { - data = unpacker.unpackString(); - } - break; - default: - result = false; - break; - } - return result; - } - - protected int countFields() { - int fieldCount = 0; - if(timestamp > 0) ++fieldCount; - if(id != null) ++fieldCount; - if(clientId != null) ++fieldCount; - if(connectionId != null) ++fieldCount; - if(encoding != null) ++fieldCount; - if(data != null) ++fieldCount; - return fieldCount; - } - - void writeFields(MessagePacker packer) throws IOException { - if(timestamp > 0) { - packer.packString(TIMESTAMP); - packer.packLong(timestamp); - } - if(id != null) { - packer.packString(ID); - packer.packString(id); - } - if(clientId != null) { - packer.packString(CLIENT_ID); - packer.packString(clientId); - } - if(connectionId != null) { - packer.packString(CONNECTION_ID); - packer.packString(connectionId); - } - if(encoding != null) { - packer.packString(ENCODING); - packer.packString(encoding); - } - if(data != null) { - packer.packString(DATA); - if(data instanceof byte[]) { - byte[] byteData = (byte[])data; - packer.packBinaryHeader(byteData.length); - packer.writePayload(byteData); - } else { - packer.packString(data.toString()); - } - } - } - - private static final String TAG = BaseMessage.class.getName(); + /** + * A unique id for this message + */ + public String id; + + /** + * The timestamp for this message + */ + public long timestamp; + + /** + * The id of the publisher of this message + */ + public String clientId; + + /** + * The connection id of the publisher of this message + */ + public String connectionId; + + /** + * Any transformation applied to the data for this message + */ + public String encoding; + + /** + * The message payload. + */ + public Object data; + + private static final String TIMESTAMP = "timestamp"; + private static final String ID = "id"; + private static final String CLIENT_ID = "clientId"; + private static final String CONNECTION_ID = "connectionId"; + private static final String ENCODING = "encoding"; + private static final String DATA = "data"; + + /** + * Generate a String summary of this BaseMessage + * @return string + */ + public void getDetails(StringBuilder builder) { + if(clientId != null) + builder.append(" clientId=").append(clientId); + if(connectionId != null) + builder.append(" connectionId=").append(connectionId); + if(data != null) + builder.append(" data=").append(data); + if(encoding != null) + builder.append(" encoding=").append(encoding); + if(id != null) + builder.append(" id=").append(id); + } + + public void decode(ChannelOptions opts) throws MessageDecodeException { + + this.decode(opts, new DecodingContext()); + } + + private final static VCDiffDecoder vcdiffDecoder = VCDiffDecoderBuilder.builder().buildSimple(); + + private static byte[] vcdiffApply(byte[] delta, byte[] base) throws MessageDecodeException { + try { + ByteArrayOutputStream decoded = new ByteArrayOutputStream(); + vcdiffDecoder.decode(base, delta, decoded); + return decoded.toByteArray(); + } catch (Throwable t) { + throw MessageDecodeException.fromThrowableAndErrorInfo(t, new ErrorInfo("VCDIFF delta decode failed", 400, 40018)); + } + } + + public void decode(ChannelOptions opts, DecodingContext context) throws MessageDecodeException { + + Object lastPayload = data; + + if(encoding != null) { + String[] xforms = encoding.split("\\/"); + int lastProcessedEncodingIndex = 0, encodingsToProcess = xforms.length; + try { + while((lastProcessedEncodingIndex = encodingsToProcess ) > 0) { + Matcher match = xformPattern.matcher(xforms[--encodingsToProcess ]); + if(!match.matches()) break; + switch(match.group(1)) { + case "base64": + try { + data = Base64Coder.decode((String) data); + } catch (IllegalArgumentException e) { + throw MessageDecodeException.fromDescription("Invalid base64 data received"); + } + if(lastProcessedEncodingIndex == xforms.length) { + lastPayload = data; + } + continue; + + case "utf-8": + try { data = new String((byte[])data, "UTF-8"); } catch(UnsupportedEncodingException|ClassCastException e) {} + continue; + + case "json": + try { + String jsonText = ((String)data).trim(); + data = Serialisation.gsonParser.parse(jsonText); + } catch(JsonParseException e) { + throw MessageDecodeException.fromDescription("Invalid JSON data received"); + } + continue; + + case "cipher": + if(opts != null && opts.encrypted) { + try { + data = opts.getCipher().decrypt((byte[]) data); + } catch(AblyException e) { + throw MessageDecodeException.fromDescription(e.errorInfo.message); + } + continue; + } + else { + throw MessageDecodeException.fromDescription("Encrypted message received but encryption is not set up"); + } + case "vcdiff": + data = vcdiffApply((byte[]) data, context.getLastMessageData()); + lastPayload = data; + + continue; + } + break; + } + } finally { + encoding = (lastProcessedEncodingIndex <= 0) ? null : join(xforms, '/', 0, lastProcessedEncodingIndex ); + } + } + + //last message bookkeping + if(lastPayload instanceof String) + context.setLastMessageData((String)lastPayload); + else if (lastPayload instanceof byte[]) + context.setLastMessageData((byte[])lastPayload); + else + throw MessageDecodeException.fromDescription("Message data neither String nor byte[]. Unsupported message data type."); + } + + public void encode(ChannelOptions opts) throws AblyException { + if(data != null) { + if(data instanceof JsonElement) { + data = Serialisation.gson.toJson((JsonElement)data); + encoding = ((encoding == null) ? "" : encoding + "/") + "json"; + } + if(data instanceof String) { + if (opts != null && opts.encrypted) { + try { data = ((String)data).getBytes("UTF-8"); } catch(UnsupportedEncodingException e) {} + encoding = ((encoding == null) ? "" : encoding + "/") + "utf-8"; + } + } else if(!(data instanceof byte[])) { + Log.d(TAG, "Message data must be either `byte[]`, `String` or `JSONElement`; implicit coercion of other types to String is deprecated"); + throw AblyException.fromErrorInfo(new ErrorInfo("Invalid message data or encoding", 400, 40013)); + } + } + if (opts != null && opts.encrypted) { + ChannelCipher cipher = opts.getCipher(); + data = cipher.encrypt((byte[]) data); + encoding = ((encoding == null) ? "" : encoding + "/") + "cipher+" + cipher.getAlgorithm(); + } + } + + /* trivial utilities for processing encoding string */ + private static Pattern xformPattern = Pattern.compile("([\\-\\w]+)(\\+([\\-\\w]+))?"); + private String join(String[] elements, char separator, int start, int end) { + StringBuilder result = new StringBuilder(elements[start++]); + for(int i = start; i < end; i++) + result.append(separator).append(elements[i]); + return result.toString(); + } + + /** + * Base for gson serialisers. + */ + public static JsonObject toJsonObject(final BaseMessage message) { + JsonObject json = new JsonObject(); + Object data = message.data; + String encoding = message.encoding; + if(data != null) { + if(data instanceof byte[]) { + byte[] dataBytes = (byte[])data; + json.addProperty("data", new String(Base64Coder.encode(dataBytes))); + encoding = (encoding == null) ? "base64" : encoding + "/base64"; + } else { + json.addProperty("data", data.toString()); + } + if(encoding != null) json.addProperty("encoding", encoding); + } + if(message.id != null) json.addProperty("id", message.id); + if(message.clientId != null) json.addProperty("clientId", message.clientId); + if(message.connectionId != null) json.addProperty("connectionId", message.connectionId); + return json; + } + + /** + * Populate fields from JSON. + */ + protected void read(final JsonObject map) throws MessageDecodeException { + final Long optionalTimestamp = readLong(map, TIMESTAMP); + if (null != optionalTimestamp) { + timestamp = optionalTimestamp; // unbox + } + + id = readString(map, ID); + clientId = readString(map, CLIENT_ID); + connectionId = readString(map, CONNECTION_ID); + encoding = readString(map, ENCODING); + data = readString(map, DATA); + } + + /** + * Read an optional textual value. + * @return The value, or null if the key was not present in the map. + * @throws ClassCastException if an element exists for that key and that element is not a {@link JsonPrimitive} + * or is not a valid string value. + */ + protected String readString(final JsonObject map, final String key) { + final JsonElement element = map.get(key); + if (null == element || element instanceof JsonNull) { + return null; + } + return element.getAsString(); + } + + /** + * Read an optional numerical value. + * @return The value, or null if the key was not present in the map. + * @throws ClassCastException if an element exists for that key and that element is not a {@link JsonPrimitive} + * or is not a valid long value. + */ + protected Long readLong(final JsonObject map, final String key) { + final JsonElement element = map.get(key); + if (null == element || element instanceof JsonNull) { + return null; + } + return element.getAsLong(); + } + + /* Msgpack processing */ + boolean readField(MessageUnpacker unpacker, String fieldName, MessageFormat fieldType) throws IOException { + boolean result = true; + switch (fieldName) { + case TIMESTAMP: + timestamp = unpacker.unpackLong(); break; + case ID: + id = unpacker.unpackString(); break; + case CLIENT_ID: + clientId = unpacker.unpackString(); break; + case CONNECTION_ID: + connectionId = unpacker.unpackString(); break; + case ENCODING: + encoding = unpacker.unpackString(); break; + case DATA: + if(fieldType.getValueType().isBinaryType()) { + byte[] byteData = new byte[unpacker.unpackBinaryHeader()]; + unpacker.readPayload(byteData); + data = byteData; + } else { + data = unpacker.unpackString(); + } + break; + default: + result = false; + break; + } + return result; + } + + protected int countFields() { + int fieldCount = 0; + if(timestamp > 0) ++fieldCount; + if(id != null) ++fieldCount; + if(clientId != null) ++fieldCount; + if(connectionId != null) ++fieldCount; + if(encoding != null) ++fieldCount; + if(data != null) ++fieldCount; + return fieldCount; + } + + void writeFields(MessagePacker packer) throws IOException { + if(timestamp > 0) { + packer.packString(TIMESTAMP); + packer.packLong(timestamp); + } + if(id != null) { + packer.packString(ID); + packer.packString(id); + } + if(clientId != null) { + packer.packString(CLIENT_ID); + packer.packString(clientId); + } + if(connectionId != null) { + packer.packString(CONNECTION_ID); + packer.packString(connectionId); + } + if(encoding != null) { + packer.packString(ENCODING); + packer.packString(encoding); + } + if(data != null) { + packer.packString(DATA); + if(data instanceof byte[]) { + byte[] byteData = (byte[])data; + packer.packBinaryHeader(byteData.length); + packer.writePayload(byteData); + } else { + packer.packString(data.toString()); + } + } + } + + private static final String TAG = BaseMessage.class.getName(); } diff --git a/lib/src/main/java/io/ably/lib/types/BasePaginatedResult.java b/lib/src/main/java/io/ably/lib/types/BasePaginatedResult.java index d49fc2d2e..7c4b6edc7 100644 --- a/lib/src/main/java/io/ably/lib/types/BasePaginatedResult.java +++ b/lib/src/main/java/io/ably/lib/types/BasePaginatedResult.java @@ -13,21 +13,21 @@ * @param */ public interface BasePaginatedResult { - /** - * Get the contents as an array of component type - */ - T[] items(); + /** + * Get the contents as an array of component type + */ + T[] items(); - /** - * Perform the given relative query - */ - Http.Request> first(); - Http.Request> current(); - Http.Request> next(); + /** + * Perform the given relative query + */ + Http.Request> first(); + Http.Request> current(); + Http.Request> next(); - boolean hasFirst(); - boolean hasCurrent(); - boolean hasNext(); + boolean hasFirst(); + boolean hasCurrent(); + boolean hasNext(); - boolean isLast(); + boolean isLast(); } diff --git a/lib/src/main/java/io/ably/lib/types/Callback.java b/lib/src/main/java/io/ably/lib/types/Callback.java index 297f927a3..a34dc2c72 100644 --- a/lib/src/main/java/io/ably/lib/types/Callback.java +++ b/lib/src/main/java/io/ably/lib/types/Callback.java @@ -7,34 +7,34 @@ * of an asynchronous operation. */ public interface Callback { - /** - * Called when the associated operation completes successfully, - */ - void onSuccess(T result); - - /** - * Called when the associated operation completes with an error. - * @param reason: information about the error. - */ - void onError(ErrorInfo reason); - - abstract class Map implements Callback { - private final Callback callback; - - public abstract U map(T result); - - public Map(Callback callback) { - this.callback = callback; - } - - @Override - public void onSuccess(T result) { - callback.onSuccess(map(result)); - } - - @Override - public void onError(ErrorInfo reason) { - callback.onError(reason); - } - } + /** + * Called when the associated operation completes successfully, + */ + void onSuccess(T result); + + /** + * Called when the associated operation completes with an error. + * @param reason: information about the error. + */ + void onError(ErrorInfo reason); + + abstract class Map implements Callback { + private final Callback callback; + + public abstract U map(T result); + + public Map(Callback callback) { + this.callback = callback; + } + + @Override + public void onSuccess(T result) { + callback.onSuccess(map(result)); + } + + @Override + public void onError(ErrorInfo reason) { + callback.onError(reason); + } + } } diff --git a/lib/src/main/java/io/ably/lib/types/Capability.java b/lib/src/main/java/io/ably/lib/types/Capability.java index d56632170..f62c5ad16 100644 --- a/lib/src/main/java/io/ably/lib/types/Capability.java +++ b/lib/src/main/java/io/ably/lib/types/Capability.java @@ -18,174 +18,174 @@ */ public class Capability { - /** - * Convenience method to canonicalise a JSON capability expression - * - * @param capability: a capability string, which is the JSON text for the capability - * @return a capability string which has been canonicalised - * @throws AblyException if there is an error processing the given string - * (if for example it is not valid JSON) - */ - public static final String c14n(String capability) throws AblyException { - if(capability == null || capability.isEmpty()) return ""; - try { - JsonObject json = (JsonObject)gsonParser.parse(capability); - return (new Capability(json)).toString(); - } catch(ClassCastException e) { - throw AblyException.fromThrowable(e); - } catch(JsonParseException e) { - throw AblyException.fromThrowable(e); - } - } + /** + * Convenience method to canonicalise a JSON capability expression + * + * @param capability: a capability string, which is the JSON text for the capability + * @return a capability string which has been canonicalised + * @throws AblyException if there is an error processing the given string + * (if for example it is not valid JSON) + */ + public static final String c14n(String capability) throws AblyException { + if(capability == null || capability.isEmpty()) return ""; + try { + JsonObject json = (JsonObject)gsonParser.parse(capability); + return (new Capability(json)).toString(); + } catch(ClassCastException e) { + throw AblyException.fromThrowable(e); + } catch(JsonParseException e) { + throw AblyException.fromThrowable(e); + } + } - /** - * Construct a new empty Capability - */ - public Capability() { - json = new JsonObject(); - } + /** + * Construct a new empty Capability + */ + public Capability() { + json = new JsonObject(); + } - /** - * Private constructor; create a new Capability instance given a JSONObject - * - * @param json the JSONObject - */ - private Capability(JsonObject json) { - this.json = json; - dirty = true; - } + /** + * Private constructor; create a new Capability instance given a JSONObject + * + * @param json the JSONObject + */ + private Capability(JsonObject json) { + this.json = json; + dirty = true; + } - /** - * Add a resource to an existing Capability instance with the - * given set of operations. If the resource already exists, - * it is wholly replaced by the given set of operations. - * - * @param resource the resource string - * @param ops a String[] of the operations permitted for this resource; - * the array does not need to be sorted - */ - public void addResource(String resource, String[] ops) { - JsonArray jsonOps = (JsonArray)gson.toJsonTree(ops); - json.add(resource, jsonOps); - dirty = true; - } + /** + * Add a resource to an existing Capability instance with the + * given set of operations. If the resource already exists, + * it is wholly replaced by the given set of operations. + * + * @param resource the resource string + * @param ops a String[] of the operations permitted for this resource; + * the array does not need to be sorted + */ + public void addResource(String resource, String[] ops) { + JsonArray jsonOps = (JsonArray)gson.toJsonTree(ops); + json.add(resource, jsonOps); + dirty = true; + } - /** - * Add a resource to an existing Capability instance with the - * given single operation. If the resource already exists, - * it is wholly replaced by the given set of operations. - * - * @param resource the resource string - * @param op a single operation String to be permitted for this resource; - */ - public void addResource(String resource, String op) { - addResource(resource, new String[]{op}); - } + /** + * Add a resource to an existing Capability instance with the + * given single operation. If the resource already exists, + * it is wholly replaced by the given set of operations. + * + * @param resource the resource string + * @param op a single operation String to be permitted for this resource; + */ + public void addResource(String resource, String op) { + addResource(resource, new String[]{op}); + } - /** - * Add a resource to an existing Capability instance with an - * empty set of operations. If the resource already exists, - * the effect is to reset its set of operations to empty. - * - * @param resource the resource string - */ - public void addResource(String resource) { - addResource(resource, new String[0]); - } - /** - * Remove a resource from an existing Capability instance - * - * @param resource the (possibly existing) resource - */ - public void removeResource(String resource) { - json.remove(resource); - /* removal doesn't break the sort order, so dirty can remain true */ - } + /** + * Add a resource to an existing Capability instance with an + * empty set of operations. If the resource already exists, + * the effect is to reset its set of operations to empty. + * + * @param resource the resource string + */ + public void addResource(String resource) { + addResource(resource, new String[0]); + } + /** + * Remove a resource from an existing Capability instance + * + * @param resource the (possibly existing) resource + */ + public void removeResource(String resource) { + json.remove(resource); + /* removal doesn't break the sort order, so dirty can remain true */ + } - /** - * Add an operation to an existing Capability instance for a - * given resource. If the resource does not already exist, - * it is created. - * - * @param resource the resource string - * @param op a single operation String to be added for this resource; - */ - public void addOperation(String resource, String op) { - JsonArray jsonOps = (JsonArray)json.get(resource); - if(jsonOps == null) { - jsonOps = new JsonArray(); - json.add(resource, jsonOps); - } - int opCount = jsonOps.size(); - for(int i = 0; i < opCount; i++) - if(jsonOps.get(i).getAsString().equals(op)) - return; + /** + * Add an operation to an existing Capability instance for a + * given resource. If the resource does not already exist, + * it is created. + * + * @param resource the resource string + * @param op a single operation String to be added for this resource; + */ + public void addOperation(String resource, String op) { + JsonArray jsonOps = (JsonArray)json.get(resource); + if(jsonOps == null) { + jsonOps = new JsonArray(); + json.add(resource, jsonOps); + } + int opCount = jsonOps.size(); + for(int i = 0; i < opCount; i++) + if(jsonOps.get(i).getAsString().equals(op)) + return; - jsonOps.add(op); - dirty = true; - } + jsonOps.add(op); + dirty = true; + } - /** - * Remove an operation for a given resource. - * If the resource becomes empty as a result, - * it is removed. - * - * @param resource the resource string - * @param op a operation String to be removed for this resource; - */ - public void removeOperation(String resource, String op) { - JsonArray jsonOps = (JsonArray)json.get(resource); - if(jsonOps == null) - return; + /** + * Remove an operation for a given resource. + * If the resource becomes empty as a result, + * it is removed. + * + * @param resource the resource string + * @param op a operation String to be removed for this resource; + */ + public void removeOperation(String resource, String op) { + JsonArray jsonOps = (JsonArray)json.get(resource); + if(jsonOps == null) + return; - int opCount = jsonOps.size(); - for(int i = 0; i < opCount; i++) - if(jsonOps.get(i).getAsString().equals(op)) { - if(opCount == 1) { - json.remove(resource); - } else { - jsonOps.remove(i); - } - return; - } - /* removal doesn't break sort order, so dirty can remain false */ - } - - /** - * Get the canonicalised String text for a Capability instance. - * The json object and its members are sorted if the object has been modified - * since the last time it was canonicalised. - * - * @return the canonicalised String text - */ - public String toString() { - if(dirty) { - Set> entries = json.entrySet(); - if(entries.isEmpty()) - return ""; - String[] resources = new String[entries.size()]; - int idx = 0; - for(Entry entry : entries) - resources[idx++] = entry.getKey(); - Arrays.sort(resources); - JsonObject c14nJson = new JsonObject(); - for(String resource : resources) { - JsonArray jsonOps = json.get(resource).getAsJsonArray(); - int count = jsonOps.size(); - String[] ops = new String[count]; - for(int i = 0; i < count; i++) - ops[i] = jsonOps.get(i).getAsString(); - Arrays.sort(ops); - c14nJson.add(resource, gson.toJsonTree(ops)); - } - json = c14nJson; - dirty = false; - } - return gson.toJson(json); - } + int opCount = jsonOps.size(); + for(int i = 0; i < opCount; i++) + if(jsonOps.get(i).getAsString().equals(op)) { + if(opCount == 1) { + json.remove(resource); + } else { + jsonOps.remove(i); + } + return; + } + /* removal doesn't break sort order, so dirty can remain false */ + } + + /** + * Get the canonicalised String text for a Capability instance. + * The json object and its members are sorted if the object has been modified + * since the last time it was canonicalised. + * + * @return the canonicalised String text + */ + public String toString() { + if(dirty) { + Set> entries = json.entrySet(); + if(entries.isEmpty()) + return ""; + String[] resources = new String[entries.size()]; + int idx = 0; + for(Entry entry : entries) + resources[idx++] = entry.getKey(); + Arrays.sort(resources); + JsonObject c14nJson = new JsonObject(); + for(String resource : resources) { + JsonArray jsonOps = json.get(resource).getAsJsonArray(); + int count = jsonOps.size(); + String[] ops = new String[count]; + for(int i = 0; i < count; i++) + ops[i] = jsonOps.get(i).getAsString(); + Arrays.sort(ops); + c14nJson.add(resource, gson.toJsonTree(ops)); + } + json = c14nJson; + dirty = false; + } + return gson.toJson(json); + } - private JsonObject json; - private boolean dirty; - private static final Gson gson = new Gson(); - private static final JsonParser gsonParser = new JsonParser(); + private JsonObject json; + private boolean dirty; + private static final Gson gson = new Gson(); + private static final JsonParser gsonParser = new JsonParser(); } diff --git a/lib/src/main/java/io/ably/lib/types/ChannelMode.java b/lib/src/main/java/io/ably/lib/types/ChannelMode.java index cf39cd0c4..5c251468b 100644 --- a/lib/src/main/java/io/ably/lib/types/ChannelMode.java +++ b/lib/src/main/java/io/ably/lib/types/ChannelMode.java @@ -6,29 +6,29 @@ import io.ably.lib.types.ProtocolMessage.Flag; public enum ChannelMode { - presence(Flag.presence), - publish(Flag.publish), - subscribe(Flag.subscribe), - presence_subscribe(Flag.presence_subscribe); + presence(Flag.presence), + publish(Flag.publish), + subscribe(Flag.subscribe), + presence_subscribe(Flag.presence_subscribe); - private final int mask; + private final int mask; - ChannelMode(final Flag flag) { - mask = flag.getMask(); - } + ChannelMode(final Flag flag) { + mask = flag.getMask(); + } - public int getMask() { - return mask; - } + public int getMask() { + return mask; + } - public static Set toSet(final int flags) { - final Set set = new HashSet<>(); - for (final ChannelMode mode : ChannelMode.values()) { - final int mask = mode.getMask(); - if ((flags & mask) == mask) { - set.add(mode); - } - } - return set; - } + public static Set toSet(final int flags) { + final Set set = new HashSet<>(); + for (final ChannelMode mode : ChannelMode.values()) { + final int mask = mode.getMask(); + if ((flags & mask) == mask) { + set.add(mode); + } + } + return set; + } } diff --git a/lib/src/main/java/io/ably/lib/types/ChannelOptions.java b/lib/src/main/java/io/ably/lib/types/ChannelOptions.java index 217fbfd99..2757d064a 100644 --- a/lib/src/main/java/io/ably/lib/types/ChannelOptions.java +++ b/lib/src/main/java/io/ably/lib/types/ChannelOptions.java @@ -7,98 +7,98 @@ import io.ably.lib.util.Crypto.ChannelCipher; public class ChannelOptions { - public Map params; - - public ChannelMode[] modes; + public Map params; + + public ChannelMode[] modes; - /** - * Cipher in use. - */ - private ChannelCipher cipher; + /** + * Cipher in use. + */ + private ChannelCipher cipher; - /** - * Parameters for the cipher. - */ - public Object cipherParams; + /** + * Parameters for the cipher. + */ + public Object cipherParams; - /** - * Whether or not this ChannelOptions is encrypted. - */ - public boolean encrypted; - - public boolean hasModes() { - return null != modes && 0 != modes.length; - } - - public boolean hasParams() { - return null != params && !params.isEmpty(); - } - - public int getModeFlags() { - int flags = 0; - for (final ChannelMode mode : modes) { - flags |= mode.getMask(); - } - return flags; - } - - public ChannelCipher getCipher() throws AblyException { - if(!this.encrypted) { - return null; - } - if(this.cipher != null) { - return this.cipher; - } else { - this.cipher = Crypto.getCipher(this); - return this.cipher; - } - } + /** + * Whether or not this ChannelOptions is encrypted. + */ + public boolean encrypted; + + public boolean hasModes() { + return null != modes && 0 != modes.length; + } + + public boolean hasParams() { + return null != params && !params.isEmpty(); + } + + public int getModeFlags() { + int flags = 0; + for (final ChannelMode mode : modes) { + flags |= mode.getMask(); + } + return flags; + } + + public ChannelCipher getCipher() throws AblyException { + if(!this.encrypted) { + return null; + } + if(this.cipher != null) { + return this.cipher; + } else { + this.cipher = Crypto.getCipher(this); + return this.cipher; + } + } - /** - * Deprecated. Use withCipherKey(byte[]) instead.

- * Create ChannelOptions from the given cipher key. - * @param key Byte array cipher key. - * @return Created ChannelOptions. - * @throws AblyException If something goes wrong. - */ - @Deprecated - public static ChannelOptions fromCipherKey(byte[] key) throws AblyException { - return withCipherKey(key); - } + /** + * Deprecated. Use withCipherKey(byte[]) instead.

+ * Create ChannelOptions from the given cipher key. + * @param key Byte array cipher key. + * @return Created ChannelOptions. + * @throws AblyException If something goes wrong. + */ + @Deprecated + public static ChannelOptions fromCipherKey(byte[] key) throws AblyException { + return withCipherKey(key); + } - /** - * Deprecated. Use withCipherKey(String) instead.

- * Create ChannelOptions from the given cipher key. - * @param base64Key The cipher key as a base64-encoded String, - * @return Created ChannelOptions. - * @throws AblyException If something goes wrong. - */ - @Deprecated - public static ChannelOptions fromCipherKey(String base64Key) throws AblyException { - return fromCipherKey(Base64Coder.decode(base64Key)); - } + /** + * Deprecated. Use withCipherKey(String) instead.

+ * Create ChannelOptions from the given cipher key. + * @param base64Key The cipher key as a base64-encoded String, + * @return Created ChannelOptions. + * @throws AblyException If something goes wrong. + */ + @Deprecated + public static ChannelOptions fromCipherKey(String base64Key) throws AblyException { + return fromCipherKey(Base64Coder.decode(base64Key)); + } - /** - * Create ChannelOptions with the given cipher key. - * @param key Byte array cipher key. - * @return Created ChannelOptions. - * @throws AblyException If something goes wrong. - */ - public static ChannelOptions withCipherKey(byte[] key) throws AblyException { - ChannelOptions options = new ChannelOptions(); - options.encrypted = true; - options.cipherParams = Crypto.getDefaultParams(key); - options.cipher = Crypto.getCipher(options); - return options; - } + /** + * Create ChannelOptions with the given cipher key. + * @param key Byte array cipher key. + * @return Created ChannelOptions. + * @throws AblyException If something goes wrong. + */ + public static ChannelOptions withCipherKey(byte[] key) throws AblyException { + ChannelOptions options = new ChannelOptions(); + options.encrypted = true; + options.cipherParams = Crypto.getDefaultParams(key); + options.cipher = Crypto.getCipher(options); + return options; + } - /** - * Create ChannelOptions with the given cipher key. - * @param base64Key The cipher key as a base64-encoded String, - * @return Created ChannelOptions. - * @throws AblyException If something goes wrong. - */ - public static ChannelOptions withCipherKey(String base64Key) throws AblyException { - return withCipherKey(Base64Coder.decode(base64Key)); - } + /** + * Create ChannelOptions with the given cipher key. + * @param base64Key The cipher key as a base64-encoded String, + * @return Created ChannelOptions. + * @throws AblyException If something goes wrong. + */ + public static ChannelOptions withCipherKey(String base64Key) throws AblyException { + return withCipherKey(Base64Coder.decode(base64Key)); + } } diff --git a/lib/src/main/java/io/ably/lib/types/ChannelProperties.java b/lib/src/main/java/io/ably/lib/types/ChannelProperties.java index 784cb85b2..cdd03603c 100644 --- a/lib/src/main/java/io/ably/lib/types/ChannelProperties.java +++ b/lib/src/main/java/io/ably/lib/types/ChannelProperties.java @@ -4,12 +4,12 @@ * (RTL15) Channel#properties attribute is a ChannelProperties object representing properties of the channel state */ public class ChannelProperties { - /** - * A message identifier indicating the time of attachment to the channel; - * used when recovering a message history to mesh exactly with messages - * received on this channel subsequent to attachment. - */ - public String attachSerial; + /** + * A message identifier indicating the time of attachment to the channel; + * used when recovering a message history to mesh exactly with messages + * received on this channel subsequent to attachment. + */ + public String attachSerial; - public ChannelProperties() {} + public ChannelProperties() {} } diff --git a/lib/src/main/java/io/ably/lib/types/ClientOptions.java b/lib/src/main/java/io/ably/lib/types/ClientOptions.java index 9d94a20c9..2db22e8c2 100644 --- a/lib/src/main/java/io/ably/lib/types/ClientOptions.java +++ b/lib/src/main/java/io/ably/lib/types/ClientOptions.java @@ -14,185 +14,185 @@ */ public class ClientOptions extends AuthOptions { - /** - * Default constructor - */ - public ClientOptions() {} - - /** - * Construct an options with a single key string. The key string is obtained - * from the application dashboard. - * @param key: the key string - * @throws AblyException if the key is not in a valid format - */ - public ClientOptions(String key) throws AblyException { - super(key); - logLevel = Log.defaultLevel; - } - - /** - * The id of the client represented by this instance. The clientId is relevant - * to presence operations, where the clientId is the principal identifier of the - * client in presence update messages. The clientId is also relevant to - * authentication; a token issued for a specific client may be used to authenticate - * the bearer of that token to the service. - */ - public String clientId; - - /** - * Log level; controls the level of verbosity of log messages from the library. - */ - public int logLevel; - - /** - * Log handler: allows the client to intercept log messages and handle them in a - * client-specific way. - */ - public LogHandler logHandler; - - - /** - * Encrypted transport: if true, TLS will be used for all connections (whether REST/HTTP - * or Realtime WebSocket or Comet connections). - */ - public boolean tls = true; - - /** - * FIXME: unused - */ - public Map headers; - - /** - * For development environments only; allows a non-default Ably host to be specified. - */ - public String restHost; - - /** - * For development environments only; allows a non-default Ably host to be specified for - * websocket connections. - */ - public String realtimeHost; - - /** - * For development environments only; allows a non-default Ably port to be specified. - */ - public int port; - - /** - * For development environments only; allows a non-default Ably TLS port to be specified. - */ - public int tlsPort; - - /** - * If false, suppresses the automatic initiation of a connection when the library is instanced. - */ - public boolean autoConnect = true; - - /** - * If false, forces the library to use the JSON encoding for REST and Realtime operations, - * instead of the default binary msgpack encoding. - */ - public boolean useBinaryProtocol = true; - - /** - * If false, suppresses the default queueing of messages when connection states that - * anticipate imminent connection (connecting and disconnected). Instead, publish and - * presence state changes will fail immediately if not in the connected state. - */ - public boolean queueMessages = true; - - /** - * If false, suppresses messages originating from this connection being echoed back - * on the same connection. - */ - public boolean echoMessages = true; - - /** - * A connection recovery string, specified by a client when initialising the library - * with the intention of inheriting the state of an earlier connection. See the Ably - * Realtime API documentation for further information on connection state recovery. - */ - public String recover; - - /** - * Proxy settings - */ - public ProxyOptions proxy; - - /** - * For development environments only; allows a non-default Ably environment - * to be used such as 'sandbox'. - * Spec: TO3k1 - */ - public String environment; - - /** - * Spec: TO3n - */ - public boolean idempotentRestPublishing = (Defaults.ABLY_VERSION_NUMBER >= 1.2); - - /** - * Spec: TO313 - */ - public int httpOpenTimeout = Defaults.TIMEOUT_HTTP_OPEN; - - /** - * Spec: TO314 - */ - public int httpRequestTimeout = Defaults.TIMEOUT_HTTP_REQUEST; - - /** - * Max number of fallback hosts to use as a fallback when an HTTP request to - * the primary host is unreachable or indicates that it is unserviceable - */ - public int httpMaxRetryCount = Defaults.HTTP_MAX_RETRY_COUNT; - - /** - * Spec: DF1b - */ - public long realtimeRequestTimeout = Defaults.realtimeRequestTimeout; - - /** - * Spec: TO3k6,RSC15a,RSC15b,RTN17b list of custom fallback hosts. - */ - public String[] fallbackHosts; - - /** - * Spec: TO3k7 Set to use default fallbackHosts even when overriding - * environment or restHost/realtimeHost - */ - public boolean fallbackHostsUseDefault; - - /** - * Spec: TO3l10 - */ - public long fallbackRetryTimeout = Defaults.fallbackRetryTimeout; - /** - * When a TokenParams object is provided, it will override - * the client library defaults described in TokenParams - * Spec: TO3j11 - */ - public TokenParams defaultTokenParams = new TokenParams(); - - /** - * Channel reattach timeout - * Spec: RTL13b - */ - public int channelRetryTimeout = Defaults.TIMEOUT_CHANNEL_RETRY; - - /** - * Additional parameters to be sent in the querystring when initiating a realtime connection - */ - public Param[] transportParams; - - /** - * Allows the caller to specify a non-default size for the asyncHttp threadpool - */ - public int asyncHttpThreadpoolSize = Defaults.HTTP_ASYNC_THREADPOOL_SIZE; - - /** - * Whether to tell Ably to wait for push REST requests to fully wait for all their effects - * before responding. - */ - public boolean pushFullWait = false; + /** + * Default constructor + */ + public ClientOptions() {} + + /** + * Construct an options with a single key string. The key string is obtained + * from the application dashboard. + * @param key: the key string + * @throws AblyException if the key is not in a valid format + */ + public ClientOptions(String key) throws AblyException { + super(key); + logLevel = Log.defaultLevel; + } + + /** + * The id of the client represented by this instance. The clientId is relevant + * to presence operations, where the clientId is the principal identifier of the + * client in presence update messages. The clientId is also relevant to + * authentication; a token issued for a specific client may be used to authenticate + * the bearer of that token to the service. + */ + public String clientId; + + /** + * Log level; controls the level of verbosity of log messages from the library. + */ + public int logLevel; + + /** + * Log handler: allows the client to intercept log messages and handle them in a + * client-specific way. + */ + public LogHandler logHandler; + + + /** + * Encrypted transport: if true, TLS will be used for all connections (whether REST/HTTP + * or Realtime WebSocket or Comet connections). + */ + public boolean tls = true; + + /** + * FIXME: unused + */ + public Map headers; + + /** + * For development environments only; allows a non-default Ably host to be specified. + */ + public String restHost; + + /** + * For development environments only; allows a non-default Ably host to be specified for + * websocket connections. + */ + public String realtimeHost; + + /** + * For development environments only; allows a non-default Ably port to be specified. + */ + public int port; + + /** + * For development environments only; allows a non-default Ably TLS port to be specified. + */ + public int tlsPort; + + /** + * If false, suppresses the automatic initiation of a connection when the library is instanced. + */ + public boolean autoConnect = true; + + /** + * If false, forces the library to use the JSON encoding for REST and Realtime operations, + * instead of the default binary msgpack encoding. + */ + public boolean useBinaryProtocol = true; + + /** + * If false, suppresses the default queueing of messages when connection states that + * anticipate imminent connection (connecting and disconnected). Instead, publish and + * presence state changes will fail immediately if not in the connected state. + */ + public boolean queueMessages = true; + + /** + * If false, suppresses messages originating from this connection being echoed back + * on the same connection. + */ + public boolean echoMessages = true; + + /** + * A connection recovery string, specified by a client when initialising the library + * with the intention of inheriting the state of an earlier connection. See the Ably + * Realtime API documentation for further information on connection state recovery. + */ + public String recover; + + /** + * Proxy settings + */ + public ProxyOptions proxy; + + /** + * For development environments only; allows a non-default Ably environment + * to be used such as 'sandbox'. + * Spec: TO3k1 + */ + public String environment; + + /** + * Spec: TO3n + */ + public boolean idempotentRestPublishing = (Defaults.ABLY_VERSION_NUMBER >= 1.2); + + /** + * Spec: TO313 + */ + public int httpOpenTimeout = Defaults.TIMEOUT_HTTP_OPEN; + + /** + * Spec: TO314 + */ + public int httpRequestTimeout = Defaults.TIMEOUT_HTTP_REQUEST; + + /** + * Max number of fallback hosts to use as a fallback when an HTTP request to + * the primary host is unreachable or indicates that it is unserviceable + */ + public int httpMaxRetryCount = Defaults.HTTP_MAX_RETRY_COUNT; + + /** + * Spec: DF1b + */ + public long realtimeRequestTimeout = Defaults.realtimeRequestTimeout; + + /** + * Spec: TO3k6,RSC15a,RSC15b,RTN17b list of custom fallback hosts. + */ + public String[] fallbackHosts; + + /** + * Spec: TO3k7 Set to use default fallbackHosts even when overriding + * environment or restHost/realtimeHost + */ + public boolean fallbackHostsUseDefault; + + /** + * Spec: TO3l10 + */ + public long fallbackRetryTimeout = Defaults.fallbackRetryTimeout; + /** + * When a TokenParams object is provided, it will override + * the client library defaults described in TokenParams + * Spec: TO3j11 + */ + public TokenParams defaultTokenParams = new TokenParams(); + + /** + * Channel reattach timeout + * Spec: RTL13b + */ + public int channelRetryTimeout = Defaults.TIMEOUT_CHANNEL_RETRY; + + /** + * Additional parameters to be sent in the querystring when initiating a realtime connection + */ + public Param[] transportParams; + + /** + * Allows the caller to specify a non-default size for the asyncHttp threadpool + */ + public int asyncHttpThreadpoolSize = Defaults.HTTP_ASYNC_THREADPOOL_SIZE; + + /** + * Whether to tell Ably to wait for push REST requests to fully wait for all their effects + * before responding. + */ + public boolean pushFullWait = false; } diff --git a/lib/src/main/java/io/ably/lib/types/ConnectionDetails.java b/lib/src/main/java/io/ably/lib/types/ConnectionDetails.java index c9a4b4954..5abe0718a 100644 --- a/lib/src/main/java/io/ably/lib/types/ConnectionDetails.java +++ b/lib/src/main/java/io/ably/lib/types/ConnectionDetails.java @@ -9,67 +9,67 @@ import org.msgpack.core.MessageUnpacker; public class ConnectionDetails { - public String clientId; - public String connectionKey; - public String serverId; - public Long maxMessageSize; - public Long maxInboundRate; - public Long maxOutboundRate; - public Long maxFrameSize; - public Long maxIdleInterval; - public Long connectionStateTtl; + public String clientId; + public String connectionKey; + public String serverId; + public Long maxMessageSize; + public Long maxInboundRate; + public Long maxOutboundRate; + public Long maxFrameSize; + public Long maxIdleInterval; + public Long connectionStateTtl; - ConnectionDetails() { - maxIdleInterval = Defaults.maxIdleInterval; - connectionStateTtl = Defaults.connectionStateTtl; - } + ConnectionDetails() { + maxIdleInterval = Defaults.maxIdleInterval; + connectionStateTtl = Defaults.connectionStateTtl; + } - ConnectionDetails readMsgpack(MessageUnpacker unpacker) throws IOException { - int fieldCount = unpacker.unpackMapHeader(); - for(int i = 0; i < fieldCount; i++) { - String fieldName = unpacker.unpackString().intern(); - MessageFormat fieldFormat = unpacker.getNextFormat(); - if(fieldFormat.equals(MessageFormat.NIL)) { unpacker.unpackNil(); continue; } + ConnectionDetails readMsgpack(MessageUnpacker unpacker) throws IOException { + int fieldCount = unpacker.unpackMapHeader(); + for(int i = 0; i < fieldCount; i++) { + String fieldName = unpacker.unpackString().intern(); + MessageFormat fieldFormat = unpacker.getNextFormat(); + if(fieldFormat.equals(MessageFormat.NIL)) { unpacker.unpackNil(); continue; } - switch(fieldName) { - case "clientId": - clientId = unpacker.unpackString(); - break; - case "connectionKey": - connectionKey = unpacker.unpackString(); - break; - case "serverId": - serverId = unpacker.unpackString(); - break; - case "maxMessageSize": - maxMessageSize = unpacker.unpackLong(); - break; - case "maxInboundRate": - maxInboundRate = unpacker.unpackLong(); - break; - case "maxOutboundRate": - maxOutboundRate = unpacker.unpackLong(); - break; - case "maxFrameSize": - maxFrameSize = unpacker.unpackLong(); - break; - case "maxIdleInterval": - maxIdleInterval = unpacker.unpackLong(); - break; - case "connectionStateTtl": - connectionStateTtl = unpacker.unpackLong(); - break; - default: - Log.v(TAG, "Unexpected field: " + fieldName); - unpacker.skipValue(); - } - } - return this; - } + switch(fieldName) { + case "clientId": + clientId = unpacker.unpackString(); + break; + case "connectionKey": + connectionKey = unpacker.unpackString(); + break; + case "serverId": + serverId = unpacker.unpackString(); + break; + case "maxMessageSize": + maxMessageSize = unpacker.unpackLong(); + break; + case "maxInboundRate": + maxInboundRate = unpacker.unpackLong(); + break; + case "maxOutboundRate": + maxOutboundRate = unpacker.unpackLong(); + break; + case "maxFrameSize": + maxFrameSize = unpacker.unpackLong(); + break; + case "maxIdleInterval": + maxIdleInterval = unpacker.unpackLong(); + break; + case "connectionStateTtl": + connectionStateTtl = unpacker.unpackLong(); + break; + default: + Log.v(TAG, "Unexpected field: " + fieldName); + unpacker.skipValue(); + } + } + return this; + } - static ConnectionDetails fromMsgpack(MessageUnpacker unpacker) throws IOException { - return (new ConnectionDetails()).readMsgpack(unpacker); - } + static ConnectionDetails fromMsgpack(MessageUnpacker unpacker) throws IOException { + return (new ConnectionDetails()).readMsgpack(unpacker); + } - private static final String TAG = ConnectionDetails.class.getName(); + private static final String TAG = ConnectionDetails.class.getName(); } diff --git a/lib/src/main/java/io/ably/lib/types/DecodingContext.java b/lib/src/main/java/io/ably/lib/types/DecodingContext.java index 256c5f8d8..c8fe6f311 100644 --- a/lib/src/main/java/io/ably/lib/types/DecodingContext.java +++ b/lib/src/main/java/io/ably/lib/types/DecodingContext.java @@ -4,32 +4,32 @@ public class DecodingContext { - private String lastMessageString; - private byte[] lastMessageBinary; + private String lastMessageString; + private byte[] lastMessageBinary; - public DecodingContext() - { - lastMessageBinary = null; - lastMessageString = null; - } + public DecodingContext() + { + lastMessageBinary = null; + lastMessageString = null; + } - public byte[] getLastMessageData() { - if(lastMessageBinary != null) - return lastMessageBinary; - else if(lastMessageString != null) { - return lastMessageString.getBytes(Charset.forName("UTF-8")); - } - else - return null; - } + public byte[] getLastMessageData() { + if(lastMessageBinary != null) + return lastMessageBinary; + else if(lastMessageString != null) { + return lastMessageString.getBytes(Charset.forName("UTF-8")); + } + else + return null; + } - public void setLastMessageData(String message) { - lastMessageString = message; - lastMessageBinary = null; - } + public void setLastMessageData(String message) { + lastMessageString = message; + lastMessageBinary = null; + } - public void setLastMessageData(byte[] message) { - lastMessageBinary = message; - lastMessageString = null; - } + public void setLastMessageData(byte[] message) { + lastMessageBinary = message; + lastMessageString = null; + } } diff --git a/lib/src/main/java/io/ably/lib/types/DeltaExtras.java b/lib/src/main/java/io/ably/lib/types/DeltaExtras.java index f3615d0da..68d5a4f2a 100644 --- a/lib/src/main/java/io/ably/lib/types/DeltaExtras.java +++ b/lib/src/main/java/io/ably/lib/types/DeltaExtras.java @@ -16,75 +16,75 @@ import java.util.Objects; public final class DeltaExtras { - private static final String TAG = DeltaExtras.class.getName(); - - public static final String FORMAT_VCDIFF = "vcdiff"; - - private static final String FROM = "from"; - private static final String FORMAT = "format"; - - private final String format; - private final String from; - - private DeltaExtras(final String format, final String from) { - if (null == format) { - throw new IllegalArgumentException("format cannot be null."); - } - if (null == from) { - throw new IllegalArgumentException("from cannot be null."); - } - - this.format = format; - this.from = from; - } - - /** - * The delta format. As at API version 1.2, only {@link DeltaExtras.FORMAT_VCDIFF} is supported. - * Will never return null. - */ - public String getFormat() { - return format; - } - - /** - * The id of the message the delta was generated from. - * Will never return null. - */ - public String getFrom() { - return from; - } - - /* package private */ void write(MessagePacker packer) throws IOException { - packer.packMapHeader(2); - - packer.packString(FORMAT); - packer.packString(format); - - packer.packString(FROM); - packer.packString(from); - } - - /* package private */ static DeltaExtras read(final Map map) throws IOException { - final Value format = map.get(ValueFactory.newString(FORMAT)); - final Value from = map.get(ValueFactory.newString(FROM)); - return new DeltaExtras(format.asStringValue().asString(), from.asStringValue().asString()); - } - - /* package private */ static DeltaExtras read(final JsonObject map) { - return new DeltaExtras(map.get(FORMAT).getAsString(), map.get(FROM).getAsString()); - } - - @Override - public boolean equals(Object o) { - if (this == o) return true; - if (o == null || getClass() != o.getClass()) return false; - DeltaExtras that = (DeltaExtras) o; - return format.equals(that.format) && - from.equals(that.from); - } - - @Override - public int hashCode() { - return Objects.hash(format, from); - } + private static final String TAG = DeltaExtras.class.getName(); + + public static final String FORMAT_VCDIFF = "vcdiff"; + + private static final String FROM = "from"; + private static final String FORMAT = "format"; + + private final String format; + private final String from; + + private DeltaExtras(final String format, final String from) { + if (null == format) { + throw new IllegalArgumentException("format cannot be null."); + } + if (null == from) { + throw new IllegalArgumentException("from cannot be null."); + } + + this.format = format; + this.from = from; + } + + /** + * The delta format. As at API version 1.2, only {@link DeltaExtras.FORMAT_VCDIFF} is supported. + * Will never return null. + */ + public String getFormat() { + return format; + } + + /** + * The id of the message the delta was generated from. + * Will never return null. + */ + public String getFrom() { + return from; + } + + /* package private */ void write(MessagePacker packer) throws IOException { + packer.packMapHeader(2); + + packer.packString(FORMAT); + packer.packString(format); + + packer.packString(FROM); + packer.packString(from); + } + + /* package private */ static DeltaExtras read(final Map map) throws IOException { + final Value format = map.get(ValueFactory.newString(FORMAT)); + final Value from = map.get(ValueFactory.newString(FROM)); + return new DeltaExtras(format.asStringValue().asString(), from.asStringValue().asString()); + } + + /* package private */ static DeltaExtras read(final JsonObject map) { + return new DeltaExtras(map.get(FORMAT).getAsString(), map.get(FROM).getAsString()); + } + + @Override + public boolean equals(Object o) { + if (this == o) return true; + if (o == null || getClass() != o.getClass()) return false; + DeltaExtras that = (DeltaExtras) o; + return format.equals(that.format) && + from.equals(that.from); + } + + @Override + public int hashCode() { + return Objects.hash(format, from); + } } diff --git a/lib/src/main/java/io/ably/lib/types/ErrorInfo.java b/lib/src/main/java/io/ably/lib/types/ErrorInfo.java index bc68695d8..b9307ad22 100644 --- a/lib/src/main/java/io/ably/lib/types/ErrorInfo.java +++ b/lib/src/main/java/io/ably/lib/types/ErrorInfo.java @@ -16,174 +16,174 @@ */ public class ErrorInfo { - /** - * Ably error code (see ably-common/protocol/errors.json) - */ - public int code; - - /** - * HTTP Status Code corresponding to this error, where applicable - */ - public int statusCode; - - /** - * Additional message information, where available - */ - public String message; - - /** - * Link to specification detail for this error code, where available. Spec TI4. - */ - public String href; - - /** - * Public no-argument constructor for msgpack - */ - public ErrorInfo() {} - - /** - * Construct an ErrorInfo from message and code - * @param message - * @param code - */ - public ErrorInfo(String message, int code) { - this.code = code; - this.message = message; - } - - /** - * Generic constructor - * @param message - * @param statusCode - * @param code - */ - public ErrorInfo(String message, int statusCode, int code) { - this(message, code); - this.statusCode = statusCode; - if(code > 0) { - this.href = href(code); - } - } - - public String toString() { - StringBuilder result = new StringBuilder("[ErrorInfo"); - result.append(" message=").append(logMessage()); - if(code > 0) { - result.append(" code=").append(code); - } - if(statusCode > 0) { - result.append(" statusCode=").append(statusCode); - } - if(href != null) { - result.append(" href=").append(href); - } - result.append(']'); - return result.toString(); - } - - ErrorInfo readMsgpack(MessageUnpacker unpacker) throws IOException { - int fieldCount = unpacker.unpackMapHeader(); - for(int i = 0; i < fieldCount; i++) { - String fieldName = unpacker.unpackString().intern(); - MessageFormat fieldFormat = unpacker.getNextFormat(); - if(fieldFormat.equals(MessageFormat.NIL)) { unpacker.unpackNil(); continue; } - - switch(fieldName) { - case "message": - message = unpacker.unpackString(); - break; - case "code": - code = unpacker.unpackInt(); - break; - case "statusCode": - statusCode = unpacker.unpackInt(); - break; - case "href": - href = unpacker.unpackString(); - break; - default: - Log.v(TAG, "Unexpected field: " + fieldName); - unpacker.skipValue(); - } - } - return this; - } - - public static ErrorInfo fromMsgpackBody(byte[] msgpack) throws IOException { - MessageUnpacker unpacker = Serialisation.msgpackUnpackerConfig.newUnpacker(msgpack); - return fromMsgpackBody(unpacker); - } - - private static ErrorInfo fromMsgpackBody(MessageUnpacker unpacker) throws IOException { - int fieldCount = unpacker.unpackMapHeader(); - ErrorInfo error = null; - for(int i = 0; i < fieldCount; i++) { - String fieldName = unpacker.unpackString().intern(); - MessageFormat fieldFormat = unpacker.getNextFormat(); - if(fieldFormat.equals(MessageFormat.NIL)) { unpacker.unpackNil(); continue; } - - switch(fieldName) { - case "error": - error = ErrorInfo.fromMsgpack(unpacker); - break; - default: - Log.v(TAG, "Unexpected field: " + fieldName); - unpacker.skipValue(); - } - } - return error; - } - - static ErrorInfo fromMsgpack(MessageUnpacker unpacker) throws IOException { - return (new ErrorInfo()).readMsgpack(unpacker); - } - - public static ErrorInfo fromThrowable(Throwable throwable) { - ErrorInfo errorInfo; - if(throwable instanceof UnknownHostException - || throwable instanceof NoRouteToHostException) { - errorInfo = new ErrorInfo(throwable.getLocalizedMessage(), 500, 50002); - } - else if(throwable instanceof IOException) { - errorInfo = new ErrorInfo(throwable.getLocalizedMessage(), 500, 50000); - } - else { - errorInfo = new ErrorInfo("Unexpected exception: " + throwable.getLocalizedMessage(), 50000, 500); - } - - return errorInfo; - } - - public static ErrorInfo fromResponseStatus(String statusLine, int statusCode) { - return new ErrorInfo(statusLine, statusCode, statusCode * 100); - } - - /* Spec: TI5 */ - private String logMessage() { - String errHref = null, logMessage = message == null ? "" : message; - if(href != null) { - errHref = href; - } else if(code > 0) { - errHref = href(code); - } - if(errHref != null && !logMessage.contains(errHref)) { - logMessage += " (See " + errHref + ")"; - } - return logMessage; - } - - @Override - public boolean equals(Object o) { - if (!(o instanceof ErrorInfo)) { - return false; - } - ErrorInfo other = (ErrorInfo) o; - return code == other.code && - statusCode == other.statusCode && - (message == other.message || (message != null && message.equals(other.message))); - } - - private static String href(int code) { return HREF_BASE + String.valueOf(code); } - private static final String HREF_BASE = "https://help.ably.io/error/"; - private static final String TAG = ErrorInfo.class.getName(); + /** + * Ably error code (see ably-common/protocol/errors.json) + */ + public int code; + + /** + * HTTP Status Code corresponding to this error, where applicable + */ + public int statusCode; + + /** + * Additional message information, where available + */ + public String message; + + /** + * Link to specification detail for this error code, where available. Spec TI4. + */ + public String href; + + /** + * Public no-argument constructor for msgpack + */ + public ErrorInfo() {} + + /** + * Construct an ErrorInfo from message and code + * @param message + * @param code + */ + public ErrorInfo(String message, int code) { + this.code = code; + this.message = message; + } + + /** + * Generic constructor + * @param message + * @param statusCode + * @param code + */ + public ErrorInfo(String message, int statusCode, int code) { + this(message, code); + this.statusCode = statusCode; + if(code > 0) { + this.href = href(code); + } + } + + public String toString() { + StringBuilder result = new StringBuilder("[ErrorInfo"); + result.append(" message=").append(logMessage()); + if(code > 0) { + result.append(" code=").append(code); + } + if(statusCode > 0) { + result.append(" statusCode=").append(statusCode); + } + if(href != null) { + result.append(" href=").append(href); + } + result.append(']'); + return result.toString(); + } + + ErrorInfo readMsgpack(MessageUnpacker unpacker) throws IOException { + int fieldCount = unpacker.unpackMapHeader(); + for(int i = 0; i < fieldCount; i++) { + String fieldName = unpacker.unpackString().intern(); + MessageFormat fieldFormat = unpacker.getNextFormat(); + if(fieldFormat.equals(MessageFormat.NIL)) { unpacker.unpackNil(); continue; } + + switch(fieldName) { + case "message": + message = unpacker.unpackString(); + break; + case "code": + code = unpacker.unpackInt(); + break; + case "statusCode": + statusCode = unpacker.unpackInt(); + break; + case "href": + href = unpacker.unpackString(); + break; + default: + Log.v(TAG, "Unexpected field: " + fieldName); + unpacker.skipValue(); + } + } + return this; + } + + public static ErrorInfo fromMsgpackBody(byte[] msgpack) throws IOException { + MessageUnpacker unpacker = Serialisation.msgpackUnpackerConfig.newUnpacker(msgpack); + return fromMsgpackBody(unpacker); + } + + private static ErrorInfo fromMsgpackBody(MessageUnpacker unpacker) throws IOException { + int fieldCount = unpacker.unpackMapHeader(); + ErrorInfo error = null; + for(int i = 0; i < fieldCount; i++) { + String fieldName = unpacker.unpackString().intern(); + MessageFormat fieldFormat = unpacker.getNextFormat(); + if(fieldFormat.equals(MessageFormat.NIL)) { unpacker.unpackNil(); continue; } + + switch(fieldName) { + case "error": + error = ErrorInfo.fromMsgpack(unpacker); + break; + default: + Log.v(TAG, "Unexpected field: " + fieldName); + unpacker.skipValue(); + } + } + return error; + } + + static ErrorInfo fromMsgpack(MessageUnpacker unpacker) throws IOException { + return (new ErrorInfo()).readMsgpack(unpacker); + } + + public static ErrorInfo fromThrowable(Throwable throwable) { + ErrorInfo errorInfo; + if(throwable instanceof UnknownHostException + || throwable instanceof NoRouteToHostException) { + errorInfo = new ErrorInfo(throwable.getLocalizedMessage(), 500, 50002); + } + else if(throwable instanceof IOException) { + errorInfo = new ErrorInfo(throwable.getLocalizedMessage(), 500, 50000); + } + else { + errorInfo = new ErrorInfo("Unexpected exception: " + throwable.getLocalizedMessage(), 50000, 500); + } + + return errorInfo; + } + + public static ErrorInfo fromResponseStatus(String statusLine, int statusCode) { + return new ErrorInfo(statusLine, statusCode, statusCode * 100); + } + + /* Spec: TI5 */ + private String logMessage() { + String errHref = null, logMessage = message == null ? "" : message; + if(href != null) { + errHref = href; + } else if(code > 0) { + errHref = href(code); + } + if(errHref != null && !logMessage.contains(errHref)) { + logMessage += " (See " + errHref + ")"; + } + return logMessage; + } + + @Override + public boolean equals(Object o) { + if (!(o instanceof ErrorInfo)) { + return false; + } + ErrorInfo other = (ErrorInfo) o; + return code == other.code && + statusCode == other.statusCode && + (message == other.message || (message != null && message.equals(other.message))); + } + + private static String href(int code) { return HREF_BASE + String.valueOf(code); } + private static final String HREF_BASE = "https://help.ably.io/error/"; + private static final String TAG = ErrorInfo.class.getName(); } \ No newline at end of file diff --git a/lib/src/main/java/io/ably/lib/types/ErrorResponse.java b/lib/src/main/java/io/ably/lib/types/ErrorResponse.java index 49965cac2..3b5c2299d 100644 --- a/lib/src/main/java/io/ably/lib/types/ErrorResponse.java +++ b/lib/src/main/java/io/ably/lib/types/ErrorResponse.java @@ -3,16 +3,16 @@ import io.ably.lib.util.Serialisation; public class ErrorResponse { - public ErrorInfo error; + public ErrorInfo error; - /** - * Get an ErrorInfo from a response body with error details - * @param jsonText - * @return - * @throws AblyException - */ - public static ErrorResponse fromJSON(String jsonText) throws AblyException { - ErrorResponse errorResponse = (ErrorResponse)Serialisation.gson.fromJson(jsonText, ErrorResponse.class); - return errorResponse; - } + /** + * Get an ErrorInfo from a response body with error details + * @param jsonText + * @return + * @throws AblyException + */ + public static ErrorResponse fromJSON(String jsonText) throws AblyException { + ErrorResponse errorResponse = (ErrorResponse)Serialisation.gson.fromJson(jsonText, ErrorResponse.class); + return errorResponse; + } } diff --git a/lib/src/main/java/io/ably/lib/types/HttpPaginatedResponse.java b/lib/src/main/java/io/ably/lib/types/HttpPaginatedResponse.java index 55e22108d..20cbbe5ee 100644 --- a/lib/src/main/java/io/ably/lib/types/HttpPaginatedResponse.java +++ b/lib/src/main/java/io/ably/lib/types/HttpPaginatedResponse.java @@ -8,26 +8,26 @@ * indicates the relative queries available. */ public abstract class HttpPaginatedResponse { - public boolean success; - public int statusCode; - public int errorCode; - public String errorMessage; - public Param[] headers; + public boolean success; + public int statusCode; + public int errorCode; + public String errorMessage; + public Param[] headers; - /** - * Get the contents as an array of component type - */ - public abstract JsonElement[] items(); + /** + * Get the contents as an array of component type + */ + public abstract JsonElement[] items(); - /** - * Perform the given relative query - */ - public abstract HttpPaginatedResponse first() throws AblyException; - public abstract HttpPaginatedResponse current() throws AblyException; - public abstract HttpPaginatedResponse next() throws AblyException; + /** + * Perform the given relative query + */ + public abstract HttpPaginatedResponse first() throws AblyException; + public abstract HttpPaginatedResponse current() throws AblyException; + public abstract HttpPaginatedResponse next() throws AblyException; - public abstract boolean hasFirst(); - public abstract boolean hasCurrent(); - public abstract boolean hasNext(); - public abstract boolean isLast(); + public abstract boolean hasFirst(); + public abstract boolean hasCurrent(); + public abstract boolean hasNext(); + public abstract boolean isLast(); } diff --git a/lib/src/main/java/io/ably/lib/types/Message.java b/lib/src/main/java/io/ably/lib/types/Message.java index f76304459..66f1e2409 100644 --- a/lib/src/main/java/io/ably/lib/types/Message.java +++ b/lib/src/main/java/io/ably/lib/types/Message.java @@ -18,274 +18,274 @@ */ public class Message extends BaseMessage { - /** - * The event name, if available - */ - public String name; - - /** - * Extras, if available - */ - public MessageExtras extras; - - private static final String NAME = "name"; - private static final String EXTRAS = "extras"; - - /** - * Default constructor - */ - public Message() { - } - - /** - * Construct a message from event name and data - * @param name - * @param data - */ - public Message(String name, Object data) { - this(name, data, null, null); - } - - - public Message(String name, Object data, String clientId) { - this(name, data, clientId, null); - } - - public Message(String name, Object data, MessageExtras extras) { - this(name, data, null, extras); - } - - /** - * Generic constructor - * @param name - * @param data - * @param clientId - * @param extras - */ - public Message(String name, Object data, String clientId, MessageExtras extras) { - this.name = name; - this.clientId = clientId; - this.data = data; - this.extras = extras; - } - - /** - * Generate a String summary of this Message - * @return string - */ - public String toString() { - StringBuilder result = new StringBuilder("[Message"); - super.getDetails(result); - if(name != null) - result.append(" name=").append(name); - result.append(']'); - return result.toString(); - } - - void writeMsgpack(MessagePacker packer) throws IOException { - int fieldCount = super.countFields(); - if(name != null) ++fieldCount; - if(extras != null) ++fieldCount; - packer.packMapHeader(fieldCount); - super.writeFields(packer); - if(name != null) { - packer.packString("name"); - packer.packString(name); - } - if(extras != null) { - packer.packString("extras"); - extras.write(packer); - } - } - - Message readMsgpack(MessageUnpacker unpacker) throws IOException { - int fieldCount = unpacker.unpackMapHeader(); - for(int i = 0; i < fieldCount; i++) { - String fieldName = unpacker.unpackString().intern(); - MessageFormat fieldFormat = unpacker.getNextFormat(); - if(fieldFormat.equals(MessageFormat.NIL)) { - unpacker.unpackNil(); - continue; - } - - if(super.readField(unpacker, fieldName, fieldFormat)) { - continue; - } - if(fieldName.equals(NAME)) { - name = unpacker.unpackString(); - } else if (fieldName.equals(EXTRAS)) { - extras = MessageExtras.read(unpacker); - } else { - Log.v(TAG, "Unexpected field: " + fieldName); - unpacker.skipValue(); - } - } - return this; - } - - /** - * A specification for a collection of messages to be sent using the batch API - * @author paddy - */ - public static class Batch { - public String[] channels; - public Message[] messages; - - public Batch(String channel, Message[] messages) { - if(channel == null || channel.isEmpty()) throw new IllegalArgumentException("A Batch spec cannot have an empty set of channels"); - if(messages == null || messages.length == 0) throw new IllegalArgumentException("A Batch spec cannot have an empty set of messages"); - this.channels = new String[]{channel}; - this.messages = messages; - } - - public Batch(String[] channels, Message[] messages) { - if(channels == null || channels.length == 0) throw new IllegalArgumentException("A Batch spec cannot have an empty set of channels"); - if(messages == null || messages.length == 0) throw new IllegalArgumentException("A Batch spec cannot have an empty set of messages"); - this.channels = channels; - this.messages = messages; - } - - public Batch(Collection channels, Collection messages) { - this(channels.toArray(new String[channels.size()]), messages.toArray(new Message[messages.size()])); - } - - public void writeMsgpack(MessagePacker packer) throws IOException { - packer.packMapHeader(2); - packer.packString("channels"); - packer.packArrayHeader(channels.length); - for(String ch : channels) packer.packString(ch); - packer.packString("messages"); - MessageSerializer.writeMsgpackArray(messages, packer); - } - } - - static Message fromMsgpack(MessageUnpacker unpacker) throws IOException { - return (new Message()).readMsgpack(unpacker); - } - - /** - * Refer Spec TM3
- * An alternative constructor that take an Message-JSON object and a channelOptions (optional), and return a Message - * @param messageJson - * @param channelOptions - * @return - * @throws MessageDecodeException - */ - public static Message fromEncoded(JsonObject messageJson, ChannelOptions channelOptions) throws MessageDecodeException { - try { - Message message = Serialisation.gson.fromJson(messageJson, Message.class); - message.decode(channelOptions); - return message; - } catch(Exception e) { - Log.e(Message.class.getName(), e.getMessage(), e); - throw MessageDecodeException.fromDescription(e.getMessage()); - } - } - - /** - * Refer Spec TM3
- * An alternative constructor that takes a Stringified Message-JSON and a channelOptions (optional), and return a Message - * @param messageJson - * @param channelOptions - * @return - * @throws MessageDecodeException - */ - public static Message fromEncoded(String messageJson, ChannelOptions channelOptions) throws MessageDecodeException { - try { - JsonObject jsonObject = Serialisation.gson.fromJson(messageJson, JsonObject.class); - return fromEncoded(jsonObject.getAsJsonObject(), channelOptions); - } catch(Exception e) { - Log.e(Message.class.getName(), e.getMessage(), e); - throw MessageDecodeException.fromDescription(e.getMessage()); - } - } - - /** - * Refer Spec TM3
- * An alternative constructor that takes a Messages JsonArray and a channelOptions (optional), and return array of Messages. - * @param messageArray - * @param channelOptions - * @return - * @throws MessageDecodeException - */ - public static Message[] fromEncodedArray(JsonArray messageArray, ChannelOptions channelOptions) throws MessageDecodeException { - try { - Message[] messages = new Message[messageArray.size()]; - for(int index = 0; index < messageArray.size(); index++) { - JsonElement jsonElement = messageArray.get(index); - if(!jsonElement.isJsonObject()) { - throw new JsonParseException("Not all JSON elements are of type JSON Object."); - } - messages[index] = fromEncoded(jsonElement.getAsJsonObject(), channelOptions); - } - return messages; - } catch(Exception e) { - e.printStackTrace(); - throw MessageDecodeException.fromDescription(e.getMessage()); - } - } - - /** - * - * @param messagesArray - * @param channelOptions - * @return - * @throws MessageDecodeException - */ - public static Message[] fromEncodedArray(String messagesArray, ChannelOptions channelOptions) throws MessageDecodeException { - try { - JsonArray jsonArray = Serialisation.gson.fromJson(messagesArray, JsonArray.class); - return fromEncodedArray(jsonArray, channelOptions); - } catch(Exception e) { - e.printStackTrace(); - throw MessageDecodeException.fromDescription(e.getMessage()); - } - } - - @Override - protected void read(final JsonObject map) throws MessageDecodeException { - super.read(map); - - name = readString(map, NAME); - - final JsonElement extrasElement = map.get(EXTRAS); - if (null != extrasElement) { - if (!(extrasElement instanceof JsonObject)) { - throw MessageDecodeException.fromDescription("Message extras is of type \"" + extrasElement.getClass() + "\" when expected a JSON object."); - } - extras = MessageExtras.read((JsonObject) extrasElement); - } - } - - public static class Serializer implements JsonSerializer, JsonDeserializer { - @Override - public JsonElement serialize(Message message, Type typeOfMessage, JsonSerializationContext ctx) { - final JsonObject json = BaseMessage.toJsonObject(message); - if (message.name != null) { - json.addProperty(NAME, message.name); - } - if (message.extras != null) { - json.add(EXTRAS, Serialisation.gson.toJsonTree(message.extras)); - } - return json; - } - - @Override - public Message deserialize(JsonElement json, Type typeOfT, JsonDeserializationContext context) throws JsonParseException { - if (!(json instanceof JsonObject)) { - throw new JsonParseException("Expected an object but got \"" + json.getClass() + "\"."); - } - - final Message message = new Message(); - try { - message.read((JsonObject)json); - } catch (MessageDecodeException e) { - e.printStackTrace(); - throw new JsonParseException("Failed to deserialize Message from JSON.", e); - } - return message; - } - } - - private static final String TAG = Message.class.getName(); + /** + * The event name, if available + */ + public String name; + + /** + * Extras, if available + */ + public MessageExtras extras; + + private static final String NAME = "name"; + private static final String EXTRAS = "extras"; + + /** + * Default constructor + */ + public Message() { + } + + /** + * Construct a message from event name and data + * @param name + * @param data + */ + public Message(String name, Object data) { + this(name, data, null, null); + } + + + public Message(String name, Object data, String clientId) { + this(name, data, clientId, null); + } + + public Message(String name, Object data, MessageExtras extras) { + this(name, data, null, extras); + } + + /** + * Generic constructor + * @param name + * @param data + * @param clientId + * @param extras + */ + public Message(String name, Object data, String clientId, MessageExtras extras) { + this.name = name; + this.clientId = clientId; + this.data = data; + this.extras = extras; + } + + /** + * Generate a String summary of this Message + * @return string + */ + public String toString() { + StringBuilder result = new StringBuilder("[Message"); + super.getDetails(result); + if(name != null) + result.append(" name=").append(name); + result.append(']'); + return result.toString(); + } + + void writeMsgpack(MessagePacker packer) throws IOException { + int fieldCount = super.countFields(); + if(name != null) ++fieldCount; + if(extras != null) ++fieldCount; + packer.packMapHeader(fieldCount); + super.writeFields(packer); + if(name != null) { + packer.packString("name"); + packer.packString(name); + } + if(extras != null) { + packer.packString("extras"); + extras.write(packer); + } + } + + Message readMsgpack(MessageUnpacker unpacker) throws IOException { + int fieldCount = unpacker.unpackMapHeader(); + for(int i = 0; i < fieldCount; i++) { + String fieldName = unpacker.unpackString().intern(); + MessageFormat fieldFormat = unpacker.getNextFormat(); + if(fieldFormat.equals(MessageFormat.NIL)) { + unpacker.unpackNil(); + continue; + } + + if(super.readField(unpacker, fieldName, fieldFormat)) { + continue; + } + if(fieldName.equals(NAME)) { + name = unpacker.unpackString(); + } else if (fieldName.equals(EXTRAS)) { + extras = MessageExtras.read(unpacker); + } else { + Log.v(TAG, "Unexpected field: " + fieldName); + unpacker.skipValue(); + } + } + return this; + } + + /** + * A specification for a collection of messages to be sent using the batch API + * @author paddy + */ + public static class Batch { + public String[] channels; + public Message[] messages; + + public Batch(String channel, Message[] messages) { + if(channel == null || channel.isEmpty()) throw new IllegalArgumentException("A Batch spec cannot have an empty set of channels"); + if(messages == null || messages.length == 0) throw new IllegalArgumentException("A Batch spec cannot have an empty set of messages"); + this.channels = new String[]{channel}; + this.messages = messages; + } + + public Batch(String[] channels, Message[] messages) { + if(channels == null || channels.length == 0) throw new IllegalArgumentException("A Batch spec cannot have an empty set of channels"); + if(messages == null || messages.length == 0) throw new IllegalArgumentException("A Batch spec cannot have an empty set of messages"); + this.channels = channels; + this.messages = messages; + } + + public Batch(Collection channels, Collection messages) { + this(channels.toArray(new String[channels.size()]), messages.toArray(new Message[messages.size()])); + } + + public void writeMsgpack(MessagePacker packer) throws IOException { + packer.packMapHeader(2); + packer.packString("channels"); + packer.packArrayHeader(channels.length); + for(String ch : channels) packer.packString(ch); + packer.packString("messages"); + MessageSerializer.writeMsgpackArray(messages, packer); + } + } + + static Message fromMsgpack(MessageUnpacker unpacker) throws IOException { + return (new Message()).readMsgpack(unpacker); + } + + /** + * Refer Spec TM3
+ * An alternative constructor that take an Message-JSON object and a channelOptions (optional), and return a Message + * @param messageJson + * @param channelOptions + * @return + * @throws MessageDecodeException + */ + public static Message fromEncoded(JsonObject messageJson, ChannelOptions channelOptions) throws MessageDecodeException { + try { + Message message = Serialisation.gson.fromJson(messageJson, Message.class); + message.decode(channelOptions); + return message; + } catch(Exception e) { + Log.e(Message.class.getName(), e.getMessage(), e); + throw MessageDecodeException.fromDescription(e.getMessage()); + } + } + + /** + * Refer Spec TM3
+ * An alternative constructor that takes a Stringified Message-JSON and a channelOptions (optional), and return a Message + * @param messageJson + * @param channelOptions + * @return + * @throws MessageDecodeException + */ + public static Message fromEncoded(String messageJson, ChannelOptions channelOptions) throws MessageDecodeException { + try { + JsonObject jsonObject = Serialisation.gson.fromJson(messageJson, JsonObject.class); + return fromEncoded(jsonObject.getAsJsonObject(), channelOptions); + } catch(Exception e) { + Log.e(Message.class.getName(), e.getMessage(), e); + throw MessageDecodeException.fromDescription(e.getMessage()); + } + } + + /** + * Refer Spec TM3
+ * An alternative constructor that takes a Messages JsonArray and a channelOptions (optional), and return array of Messages. + * @param messageArray + * @param channelOptions + * @return + * @throws MessageDecodeException + */ + public static Message[] fromEncodedArray(JsonArray messageArray, ChannelOptions channelOptions) throws MessageDecodeException { + try { + Message[] messages = new Message[messageArray.size()]; + for(int index = 0; index < messageArray.size(); index++) { + JsonElement jsonElement = messageArray.get(index); + if(!jsonElement.isJsonObject()) { + throw new JsonParseException("Not all JSON elements are of type JSON Object."); + } + messages[index] = fromEncoded(jsonElement.getAsJsonObject(), channelOptions); + } + return messages; + } catch(Exception e) { + e.printStackTrace(); + throw MessageDecodeException.fromDescription(e.getMessage()); + } + } + + /** + * + * @param messagesArray + * @param channelOptions + * @return + * @throws MessageDecodeException + */ + public static Message[] fromEncodedArray(String messagesArray, ChannelOptions channelOptions) throws MessageDecodeException { + try { + JsonArray jsonArray = Serialisation.gson.fromJson(messagesArray, JsonArray.class); + return fromEncodedArray(jsonArray, channelOptions); + } catch(Exception e) { + e.printStackTrace(); + throw MessageDecodeException.fromDescription(e.getMessage()); + } + } + + @Override + protected void read(final JsonObject map) throws MessageDecodeException { + super.read(map); + + name = readString(map, NAME); + + final JsonElement extrasElement = map.get(EXTRAS); + if (null != extrasElement) { + if (!(extrasElement instanceof JsonObject)) { + throw MessageDecodeException.fromDescription("Message extras is of type \"" + extrasElement.getClass() + "\" when expected a JSON object."); + } + extras = MessageExtras.read((JsonObject) extrasElement); + } + } + + public static class Serializer implements JsonSerializer, JsonDeserializer { + @Override + public JsonElement serialize(Message message, Type typeOfMessage, JsonSerializationContext ctx) { + final JsonObject json = BaseMessage.toJsonObject(message); + if (message.name != null) { + json.addProperty(NAME, message.name); + } + if (message.extras != null) { + json.add(EXTRAS, Serialisation.gson.toJsonTree(message.extras)); + } + return json; + } + + @Override + public Message deserialize(JsonElement json, Type typeOfT, JsonDeserializationContext context) throws JsonParseException { + if (!(json instanceof JsonObject)) { + throw new JsonParseException("Expected an object but got \"" + json.getClass() + "\"."); + } + + final Message message = new Message(); + try { + message.read((JsonObject)json); + } catch (MessageDecodeException e) { + e.printStackTrace(); + throw new JsonParseException("Failed to deserialize Message from JSON.", e); + } + return message; + } + } + + private static final String TAG = Message.class.getName(); } diff --git a/lib/src/main/java/io/ably/lib/types/MessageDecodeException.java b/lib/src/main/java/io/ably/lib/types/MessageDecodeException.java index b9f278a75..6d89d63a8 100644 --- a/lib/src/main/java/io/ably/lib/types/MessageDecodeException.java +++ b/lib/src/main/java/io/ably/lib/types/MessageDecodeException.java @@ -4,19 +4,19 @@ * Special AblyException for message decoding problems */ public class MessageDecodeException extends AblyException { - private static final long serialVersionUID = 1L; + private static final long serialVersionUID = 1L; - private MessageDecodeException(Throwable e, ErrorInfo errorInfo) { - super(e, errorInfo); - } + private MessageDecodeException(Throwable e, ErrorInfo errorInfo) { + super(e, errorInfo); + } - public static MessageDecodeException fromDescription(String description) { - return new MessageDecodeException( - new Exception(description), - new ErrorInfo(description, 91200)); - } + public static MessageDecodeException fromDescription(String description) { + return new MessageDecodeException( + new Exception(description), + new ErrorInfo(description, 91200)); + } - public static MessageDecodeException fromThrowableAndErrorInfo(Throwable e, ErrorInfo errorInfo) { - return new MessageDecodeException(e, errorInfo); - } + public static MessageDecodeException fromThrowableAndErrorInfo(Throwable e, ErrorInfo errorInfo) { + return new MessageDecodeException(e, errorInfo); + } } diff --git a/lib/src/main/java/io/ably/lib/types/MessageExtras.java b/lib/src/main/java/io/ably/lib/types/MessageExtras.java index 8a921a8b6..32156e236 100644 --- a/lib/src/main/java/io/ably/lib/types/MessageExtras.java +++ b/lib/src/main/java/io/ably/lib/types/MessageExtras.java @@ -18,132 +18,132 @@ import java.util.Objects; public final class MessageExtras { - private static final String TAG = MessageExtras.class.getName(); - - private static final String DELTA = "delta"; - - private final DeltaExtras delta; // may be null - private final JsonObject jsonObject; // never null - - /** - * Creates a MessageExtras instance to be sent as extra with a Message to Ably's servers. - * - * @see Channel-based push notification example - * - * @since 1.2.1 - */ - public MessageExtras(final JsonObject jsonObject) { - this(jsonObject, null); - } - - private MessageExtras(final JsonObject jsonObject, final DeltaExtras delta) { - if (null == jsonObject) { - throw new NullPointerException("jsonObject cannot be null."); - } - - this.jsonObject = jsonObject; - this.delta = delta; - } - - public DeltaExtras getDelta() { - return delta; - } - - public JsonObject asJsonObject() { - return jsonObject; - } - - /* package private */ void write(MessagePacker packer) throws IOException { - if (null == jsonObject) { - // raw is null, so delta is not null - packer.packMapHeader(1); - packer.packString(DELTA); - delta.write(packer); - } else { - // raw is not null, so delta can be ignored - Serialisation.gsonToMsgpack(jsonObject, packer); - } - } - - /* package private */ static MessageExtras read(MessageUnpacker unpacker) throws IOException { - DeltaExtras delta = null; - - final ImmutableValue value = unpacker.unpackValue(); - if (value instanceof ImmutableMapValue) { - final Map map = ((ImmutableMapValue) value).map(); - final Value deltaValue = map.get(ValueFactory.newString(DELTA)); - if (null != deltaValue) { - if (!(deltaValue instanceof ImmutableMapValue)) { - // There's a delta key but the value at that key is not a map. - throw new IOException("The delta extras unpacked to the wrong type \"" + deltaValue.getClass() + "\" when expected a map."); - } - final Map deltaMap = ((ImmutableMapValue)deltaValue).map(); - delta = DeltaExtras.read(deltaMap); - } - } - - final JsonElement element = Serialisation.msgpackToGson(value); - if (!(element instanceof JsonObject)) { - // The root thing that we unpacked was not a map. - throw new IOException("The extras unpacked to the wrong type \"" + element.getClass() + "\" when expected a JsonObject."); - } - final JsonObject raw = (JsonObject)element; - - return new MessageExtras(raw, delta); - } - - /* package private */ static MessageExtras read(final JsonObject raw) throws MessageDecodeException { - DeltaExtras delta = null; - - final JsonElement deltaElement = raw.get(DELTA); - if (deltaElement instanceof JsonObject) { - delta = DeltaExtras.read((JsonObject)deltaElement); - } else { - if (null != deltaElement) { - throw MessageDecodeException.fromDescription("The value under the delta key is of the wrong type \"" + deltaElement.getClass() + "\" when expected a map."); - } - } - - return new MessageExtras(raw, delta); - } - - @Override - public boolean equals(Object o) { - if (this == o) return true; - if (o == null || getClass() != o.getClass()) return false; - MessageExtras that = (MessageExtras) o; - return (null == jsonObject) ? - Objects.equals(delta, that.delta) : - Objects.equals(jsonObject, that.jsonObject); - } - - @Override - public int hashCode() { - return (null == jsonObject) ? Objects.hashCode(delta) : Objects.hashCode(jsonObject); - } - - @Override - public String toString() { - return "MessageExtras{" + - DELTA + "=" + delta + - ", raw=" + jsonObject + - '}'; - } - - public static class Serializer implements JsonSerializer { - @Override - public JsonElement serialize(final MessageExtras src, final Type typeOfSrc, final JsonSerializationContext context) { - return (null != src.jsonObject) ? src.jsonObject : wrapDelta(src.getDelta()); - } - - public static JsonObject wrapDelta(final DeltaExtras delta) { - if (null == delta) { - throw new NullPointerException("delta cannot be null."); - } - - final JsonObject json = new JsonObject(); - json.add(DELTA, Serialisation.gson.toJsonTree(delta)); - return json; - } - } + private static final String TAG = MessageExtras.class.getName(); + + private static final String DELTA = "delta"; + + private final DeltaExtras delta; // may be null + private final JsonObject jsonObject; // never null + + /** + * Creates a MessageExtras instance to be sent as extra with a Message to Ably's servers. + * + * @see Channel-based push notification example + * + * @since 1.2.1 + */ + public MessageExtras(final JsonObject jsonObject) { + this(jsonObject, null); + } + + private MessageExtras(final JsonObject jsonObject, final DeltaExtras delta) { + if (null == jsonObject) { + throw new NullPointerException("jsonObject cannot be null."); + } + + this.jsonObject = jsonObject; + this.delta = delta; + } + + public DeltaExtras getDelta() { + return delta; + } + + public JsonObject asJsonObject() { + return jsonObject; + } + + /* package private */ void write(MessagePacker packer) throws IOException { + if (null == jsonObject) { + // raw is null, so delta is not null + packer.packMapHeader(1); + packer.packString(DELTA); + delta.write(packer); + } else { + // raw is not null, so delta can be ignored + Serialisation.gsonToMsgpack(jsonObject, packer); + } + } + + /* package private */ static MessageExtras read(MessageUnpacker unpacker) throws IOException { + DeltaExtras delta = null; + + final ImmutableValue value = unpacker.unpackValue(); + if (value instanceof ImmutableMapValue) { + final Map map = ((ImmutableMapValue) value).map(); + final Value deltaValue = map.get(ValueFactory.newString(DELTA)); + if (null != deltaValue) { + if (!(deltaValue instanceof ImmutableMapValue)) { + // There's a delta key but the value at that key is not a map. + throw new IOException("The delta extras unpacked to the wrong type \"" + deltaValue.getClass() + "\" when expected a map."); + } + final Map deltaMap = ((ImmutableMapValue)deltaValue).map(); + delta = DeltaExtras.read(deltaMap); + } + } + + final JsonElement element = Serialisation.msgpackToGson(value); + if (!(element instanceof JsonObject)) { + // The root thing that we unpacked was not a map. + throw new IOException("The extras unpacked to the wrong type \"" + element.getClass() + "\" when expected a JsonObject."); + } + final JsonObject raw = (JsonObject)element; + + return new MessageExtras(raw, delta); + } + + /* package private */ static MessageExtras read(final JsonObject raw) throws MessageDecodeException { + DeltaExtras delta = null; + + final JsonElement deltaElement = raw.get(DELTA); + if (deltaElement instanceof JsonObject) { + delta = DeltaExtras.read((JsonObject)deltaElement); + } else { + if (null != deltaElement) { + throw MessageDecodeException.fromDescription("The value under the delta key is of the wrong type \"" + deltaElement.getClass() + "\" when expected a map."); + } + } + + return new MessageExtras(raw, delta); + } + + @Override + public boolean equals(Object o) { + if (this == o) return true; + if (o == null || getClass() != o.getClass()) return false; + MessageExtras that = (MessageExtras) o; + return (null == jsonObject) ? + Objects.equals(delta, that.delta) : + Objects.equals(jsonObject, that.jsonObject); + } + + @Override + public int hashCode() { + return (null == jsonObject) ? Objects.hashCode(delta) : Objects.hashCode(jsonObject); + } + + @Override + public String toString() { + return "MessageExtras{" + + DELTA + "=" + delta + + ", raw=" + jsonObject + + '}'; + } + + public static class Serializer implements JsonSerializer { + @Override + public JsonElement serialize(final MessageExtras src, final Type typeOfSrc, final JsonSerializationContext context) { + return (null != src.jsonObject) ? src.jsonObject : wrapDelta(src.getDelta()); + } + + public static JsonObject wrapDelta(final DeltaExtras delta) { + if (null == delta) { + throw new NullPointerException("delta cannot be null."); + } + + final JsonObject json = new JsonObject(); + json.add(DELTA, Serialisation.gson.toJsonTree(delta)); + return json; + } + } } diff --git a/lib/src/main/java/io/ably/lib/types/MessageSerializer.java b/lib/src/main/java/io/ably/lib/types/MessageSerializer.java index 092462368..3e2db0e2c 100644 --- a/lib/src/main/java/io/ably/lib/types/MessageSerializer.java +++ b/lib/src/main/java/io/ably/lib/types/MessageSerializer.java @@ -31,168 +31,168 @@ */ public class MessageSerializer { - /**************************************** - * Msgpack decode - ****************************************/ - - public static Message[] readMsgpackArray(MessageUnpacker unpacker) throws IOException { - int count = unpacker.unpackArrayHeader(); - Message[] result = new Message[count]; - for(int i = 0; i < count; i++) - result[i] = Message.fromMsgpack(unpacker); - return result; - } - - public static Message[] readMsgpack(byte[] packed) throws AblyException { - try { - MessageUnpacker unpacker = Serialisation.msgpackUnpackerConfig.newUnpacker(packed); - return readMsgpackArray(unpacker); - } catch(IOException ioe) { - throw AblyException.fromThrowable(ioe); - } - } - - /**************************************** - * Msgpack encode - ****************************************/ - - public static HttpCore.RequestBody asMsgpackRequest(Message message) throws AblyException { - return asMsgpackRequest(new Message[] { message }); - } - - public static HttpCore.RequestBody asMsgpackRequest(Message[] messages) { - return new HttpUtils.ByteArrayRequestBody(writeMsgpackArray(messages), "application/x-msgpack"); - } - - public static byte[] writeMsgpackArray(Message[] messages) { - try { - ByteArrayOutputStream out = new ByteArrayOutputStream(); - MessagePacker packer = Serialisation.msgpackPackerConfig.newPacker(out); - writeMsgpackArray(messages, packer); - packer.flush(); - return out.toByteArray(); - } catch(IOException e) { return null; } - } - - public static void writeMsgpackArray(Message[] messages, MessagePacker packer) { - try { - int count = messages.length; - packer.packArrayHeader(count); - for(Message message : messages) - message.writeMsgpack(packer); - } catch(IOException e) {} - } - - public static void write(final Map map, final MessagePacker packer) throws IOException { - packer.packMapHeader(map.size()); - for (final Map.Entry entry : map.entrySet()) { - packer.packString(entry.getKey()); - packer.packString(entry.getValue()); - } - } - - public static Map readStringMap(final MessageUnpacker unpacker) throws IOException { - final Map map = new HashMap<>(); - final int fieldCount = unpacker.unpackMapHeader(); - for(int i = 0; i < fieldCount; i++) { - final String fieldName = unpacker.unpackString(); - final MessageFormat fieldFormat = unpacker.getNextFormat(); - - // TODO is this required? It seems to be saying that if we have a null value - // then nothing should be added to the map, despite the fact that the key - // was potentially viable. - if(fieldFormat.equals(MessageFormat.NIL)) { unpacker.unpackNil(); continue; } - - map.put(fieldName, unpacker.unpackString()); - } - return map; - } - - public static HttpCore.RequestBody asMsgpackRequest(Message.Batch[] pubSpecs) { - return new HttpUtils.ByteArrayRequestBody(writeMsgpackArray(pubSpecs), "application/x-msgpack"); - } - - static byte[] writeMsgpackArray(Message.Batch[] pubSpecs) { - try { - ByteArrayOutputStream out = new ByteArrayOutputStream(); - MessagePacker packer = Serialisation.msgpackPackerConfig.newPacker(out); - writeMsgpackArray(pubSpecs, packer); - packer.flush(); - return out.toByteArray(); - } catch(IOException e) { return null; } - } - - static void writeMsgpackArray(Message.Batch[] pubSpecs, MessagePacker packer) throws IOException { - try { - int count = pubSpecs.length; - packer.packArrayHeader(count); - for(Message.Batch spec : pubSpecs) - spec.writeMsgpack(packer); - } catch(IOException e) {} - } - - /**************************************** - * JSON decode - ****************************************/ - - public static Message[] readMessagesFromJson(byte[] packed) throws MessageDecodeException { - return Serialisation.gson.fromJson(new String(packed), Message[].class); - } - - /**************************************** - * JSON encode - ****************************************/ - - public static HttpCore.RequestBody asJsonRequest(Message message) throws AblyException { - return asJsonRequest(new Message[] { message }); - } - - public static HttpCore.RequestBody asJsonRequest(Message[] messages) { - return new HttpUtils.JsonRequestBody(Serialisation.gson.toJson(messages)); - } - - public static HttpCore.RequestBody asJSONRequest(Message.Batch[] pubSpecs) { - return new HttpUtils.JsonRequestBody(Serialisation.gson.toJson(pubSpecs)); - } - - /**************************************** - * BodyHandler - ****************************************/ - - public static HttpCore.BodyHandler getMessageResponseHandler(ChannelOptions opts) { - return opts == null ? messageResponseHandler : new MessageBodyHandler(opts); - } - - private static class MessageBodyHandler implements HttpCore.BodyHandler { - - MessageBodyHandler(ChannelOptions opts) { this.opts = opts; } - - @Override - public Message[] handleResponseBody(String contentType, byte[] body) throws AblyException { - try { - Message[] messages = null; - if("application/json".equals(contentType)) - messages = readMessagesFromJson(body); - else if("application/x-msgpack".equals(contentType)) - messages = readMsgpack(body); - if(messages != null) { - for (Message message : messages) { - try { - message.decode(opts); - } catch (MessageDecodeException e) { - Log.e(TAG, e.errorInfo.message); - } - } - } - return messages; - } catch (MessageDecodeException e) { - throw AblyException.fromThrowable(e); - } - } - - private ChannelOptions opts; - } - - private static HttpCore.BodyHandler messageResponseHandler = new MessageBodyHandler(null); - private static final String TAG = MessageSerializer.class.getName(); + /**************************************** + * Msgpack decode + ****************************************/ + + public static Message[] readMsgpackArray(MessageUnpacker unpacker) throws IOException { + int count = unpacker.unpackArrayHeader(); + Message[] result = new Message[count]; + for(int i = 0; i < count; i++) + result[i] = Message.fromMsgpack(unpacker); + return result; + } + + public static Message[] readMsgpack(byte[] packed) throws AblyException { + try { + MessageUnpacker unpacker = Serialisation.msgpackUnpackerConfig.newUnpacker(packed); + return readMsgpackArray(unpacker); + } catch(IOException ioe) { + throw AblyException.fromThrowable(ioe); + } + } + + /**************************************** + * Msgpack encode + ****************************************/ + + public static HttpCore.RequestBody asMsgpackRequest(Message message) throws AblyException { + return asMsgpackRequest(new Message[] { message }); + } + + public static HttpCore.RequestBody asMsgpackRequest(Message[] messages) { + return new HttpUtils.ByteArrayRequestBody(writeMsgpackArray(messages), "application/x-msgpack"); + } + + public static byte[] writeMsgpackArray(Message[] messages) { + try { + ByteArrayOutputStream out = new ByteArrayOutputStream(); + MessagePacker packer = Serialisation.msgpackPackerConfig.newPacker(out); + writeMsgpackArray(messages, packer); + packer.flush(); + return out.toByteArray(); + } catch(IOException e) { return null; } + } + + public static void writeMsgpackArray(Message[] messages, MessagePacker packer) { + try { + int count = messages.length; + packer.packArrayHeader(count); + for(Message message : messages) + message.writeMsgpack(packer); + } catch(IOException e) {} + } + + public static void write(final Map map, final MessagePacker packer) throws IOException { + packer.packMapHeader(map.size()); + for (final Map.Entry entry : map.entrySet()) { + packer.packString(entry.getKey()); + packer.packString(entry.getValue()); + } + } + + public static Map readStringMap(final MessageUnpacker unpacker) throws IOException { + final Map map = new HashMap<>(); + final int fieldCount = unpacker.unpackMapHeader(); + for(int i = 0; i < fieldCount; i++) { + final String fieldName = unpacker.unpackString(); + final MessageFormat fieldFormat = unpacker.getNextFormat(); + + // TODO is this required? It seems to be saying that if we have a null value + // then nothing should be added to the map, despite the fact that the key + // was potentially viable. + if(fieldFormat.equals(MessageFormat.NIL)) { unpacker.unpackNil(); continue; } + + map.put(fieldName, unpacker.unpackString()); + } + return map; + } + + public static HttpCore.RequestBody asMsgpackRequest(Message.Batch[] pubSpecs) { + return new HttpUtils.ByteArrayRequestBody(writeMsgpackArray(pubSpecs), "application/x-msgpack"); + } + + static byte[] writeMsgpackArray(Message.Batch[] pubSpecs) { + try { + ByteArrayOutputStream out = new ByteArrayOutputStream(); + MessagePacker packer = Serialisation.msgpackPackerConfig.newPacker(out); + writeMsgpackArray(pubSpecs, packer); + packer.flush(); + return out.toByteArray(); + } catch(IOException e) { return null; } + } + + static void writeMsgpackArray(Message.Batch[] pubSpecs, MessagePacker packer) throws IOException { + try { + int count = pubSpecs.length; + packer.packArrayHeader(count); + for(Message.Batch spec : pubSpecs) + spec.writeMsgpack(packer); + } catch(IOException e) {} + } + + /**************************************** + * JSON decode + ****************************************/ + + public static Message[] readMessagesFromJson(byte[] packed) throws MessageDecodeException { + return Serialisation.gson.fromJson(new String(packed), Message[].class); + } + + /**************************************** + * JSON encode + ****************************************/ + + public static HttpCore.RequestBody asJsonRequest(Message message) throws AblyException { + return asJsonRequest(new Message[] { message }); + } + + public static HttpCore.RequestBody asJsonRequest(Message[] messages) { + return new HttpUtils.JsonRequestBody(Serialisation.gson.toJson(messages)); + } + + public static HttpCore.RequestBody asJSONRequest(Message.Batch[] pubSpecs) { + return new HttpUtils.JsonRequestBody(Serialisation.gson.toJson(pubSpecs)); + } + + /**************************************** + * BodyHandler + ****************************************/ + + public static HttpCore.BodyHandler getMessageResponseHandler(ChannelOptions opts) { + return opts == null ? messageResponseHandler : new MessageBodyHandler(opts); + } + + private static class MessageBodyHandler implements HttpCore.BodyHandler { + + MessageBodyHandler(ChannelOptions opts) { this.opts = opts; } + + @Override + public Message[] handleResponseBody(String contentType, byte[] body) throws AblyException { + try { + Message[] messages = null; + if("application/json".equals(contentType)) + messages = readMessagesFromJson(body); + else if("application/x-msgpack".equals(contentType)) + messages = readMsgpack(body); + if(messages != null) { + for (Message message : messages) { + try { + message.decode(opts); + } catch (MessageDecodeException e) { + Log.e(TAG, e.errorInfo.message); + } + } + } + return messages; + } catch (MessageDecodeException e) { + throw AblyException.fromThrowable(e); + } + } + + private ChannelOptions opts; + } + + private static HttpCore.BodyHandler messageResponseHandler = new MessageBodyHandler(null); + private static final String TAG = MessageSerializer.class.getName(); } diff --git a/lib/src/main/java/io/ably/lib/types/PaginatedResult.java b/lib/src/main/java/io/ably/lib/types/PaginatedResult.java index f2cb5bde5..76e69d3f5 100644 --- a/lib/src/main/java/io/ably/lib/types/PaginatedResult.java +++ b/lib/src/main/java/io/ably/lib/types/PaginatedResult.java @@ -9,21 +9,21 @@ */ public interface PaginatedResult { - /** - * Get the contents as an array of component type - */ - T[] items(); + /** + * Get the contents as an array of component type + */ + T[] items(); - /** - * Perform the given relative query - */ - PaginatedResult first() throws AblyException; - PaginatedResult current() throws AblyException; - PaginatedResult next() throws AblyException; + /** + * Perform the given relative query + */ + PaginatedResult first() throws AblyException; + PaginatedResult current() throws AblyException; + PaginatedResult next() throws AblyException; - boolean hasFirst(); - boolean hasCurrent(); - boolean hasNext(); + boolean hasFirst(); + boolean hasCurrent(); + boolean hasNext(); - boolean isLast(); + boolean isLast(); } diff --git a/lib/src/main/java/io/ably/lib/types/Param.java b/lib/src/main/java/io/ably/lib/types/Param.java index 126f14614..aca9ffc85 100644 --- a/lib/src/main/java/io/ably/lib/types/Param.java +++ b/lib/src/main/java/io/ably/lib/types/Param.java @@ -5,80 +5,80 @@ */ public class Param { - public Param(String key, String value) { this.key = key; this.value = value; } - public Param(String key, Object value) { this(key, value.toString()); } - public String key; - public String value; - - public static Param[] push(Param[] params, Param val) { - if (params == null) { - return new Param[] { val }; - } - - int len = params.length; - Param[] result = new Param[len + 1]; - System.arraycopy(params, 0, result, 0, len); - result[len] = val; - return result; - } - - public static Param[] push(Param[] params, String key, String value) { - return push(params, new Param(key, value)); - } - - public static Param[] set(Param[] params, Param val) { - if (params == null) { - return new Param[] { val }; - } - - for (int i = 0; i < params.length; i++) { - if (params[i].key.equals(val.key)) { - params[i] = val; - return params; - } - } - - return push(params, val); - } - - public static Param[] set(Param[] params, String key, String value) { - return set(params, new Param(key, value)); - } - - public static boolean containsKey(Param[] params, String key) { - return getFirst(params, key) != null; - } - - public static String getFirst(Param[] params, String key) { - if(params == null) - return null; - for(Param param : params) - if(param.key.equals(key)) - return param.value; - return null; - } - - @Override - public boolean equals(Object o) { - if (this == o) return true; - if (o == null || getClass() != o.getClass()) return false; - - Param param = (Param) o; - - if (key != null ? !key.equals(param.key) : param.key != null) return false; - return value != null ? value.equals(param.value) : param.value == null; - - } - - @Override - public int hashCode() { - int result = key != null ? key.hashCode() : 0; - result = 31 * result + (value != null ? value.hashCode() : 0); - return result; - } - - @Override - public String toString() { - return key + ":" + value; - } + public Param(String key, String value) { this.key = key; this.value = value; } + public Param(String key, Object value) { this(key, value.toString()); } + public String key; + public String value; + + public static Param[] push(Param[] params, Param val) { + if (params == null) { + return new Param[] { val }; + } + + int len = params.length; + Param[] result = new Param[len + 1]; + System.arraycopy(params, 0, result, 0, len); + result[len] = val; + return result; + } + + public static Param[] push(Param[] params, String key, String value) { + return push(params, new Param(key, value)); + } + + public static Param[] set(Param[] params, Param val) { + if (params == null) { + return new Param[] { val }; + } + + for (int i = 0; i < params.length; i++) { + if (params[i].key.equals(val.key)) { + params[i] = val; + return params; + } + } + + return push(params, val); + } + + public static Param[] set(Param[] params, String key, String value) { + return set(params, new Param(key, value)); + } + + public static boolean containsKey(Param[] params, String key) { + return getFirst(params, key) != null; + } + + public static String getFirst(Param[] params, String key) { + if(params == null) + return null; + for(Param param : params) + if(param.key.equals(key)) + return param.value; + return null; + } + + @Override + public boolean equals(Object o) { + if (this == o) return true; + if (o == null || getClass() != o.getClass()) return false; + + Param param = (Param) o; + + if (key != null ? !key.equals(param.key) : param.key != null) return false; + return value != null ? value.equals(param.value) : param.value == null; + + } + + @Override + public int hashCode() { + int result = key != null ? key.hashCode() : 0; + result = 31 * result + (value != null ? value.hashCode() : 0); + return result; + } + + @Override + public String toString() { + return key + ":" + value; + } } diff --git a/lib/src/main/java/io/ably/lib/types/PresenceMessage.java b/lib/src/main/java/io/ably/lib/types/PresenceMessage.java index 4c86bbe90..feb6540e6 100644 --- a/lib/src/main/java/io/ably/lib/types/PresenceMessage.java +++ b/lib/src/main/java/io/ably/lib/types/PresenceMessage.java @@ -17,211 +17,211 @@ */ public class PresenceMessage extends BaseMessage implements Cloneable { - /** - * Presence Action: the event signified by a PresenceMessage - */ - public enum Action { - absent, - present, - enter, - leave, - update; - - public int getValue() { return ordinal(); } - public static Action findByValue(int value) { return values()[value]; } - } - - public Action action; - - /** - * Default constructor - */ - public PresenceMessage() {} - - /** - * Construct a PresenceMessage from an Action and clientId - * @param action - * @param clientId - */ - public PresenceMessage(Action action, String clientId) { - this(action, clientId, null); - } - - /** - * Generic constructor - * @param action - * @param clientId - * @param data - */ - public PresenceMessage(Action action, String clientId, Object data) { - this.action = action; - this.clientId = clientId; - this.data = data; - } - - /** - * Generate a String summary of this PresenceMessage - * @return string - */ - public String toString() { - StringBuilder result = new StringBuilder("[PresenceMessage"); - super.getDetails(result); - result.append(" action=").append(action.name()); - result.append(']'); - return result.toString(); - } - - @Override - public Object clone() { - PresenceMessage result = new PresenceMessage(); - result.id = id; - result.timestamp = timestamp; - result.clientId = clientId; - result.connectionId = connectionId; - result.encoding = encoding; - result.data = data; - result.action = action; - return result; - } - - void writeMsgpack(MessagePacker packer) throws IOException { - int fieldCount = super.countFields(); - ++fieldCount; - packer.packMapHeader(fieldCount); - super.writeFields(packer); - packer.packString("action"); - packer.packInt(action.getValue()); - } - - PresenceMessage readMsgpack(MessageUnpacker unpacker) throws IOException { - int fieldCount = unpacker.unpackMapHeader(); - for(int i = 0; i < fieldCount; i++) { - String fieldName = unpacker.unpackString().intern(); - MessageFormat fieldFormat = unpacker.getNextFormat(); - if(fieldFormat.equals(MessageFormat.NIL)) { unpacker.unpackNil(); continue; } - - if(super.readField(unpacker, fieldName, fieldFormat)) { continue; } - if(fieldName.equals("action")) { - action = Action.findByValue(unpacker.unpackInt()); - } else { - Log.v(TAG, "Unexpected field: " + fieldName); - unpacker.skipValue(); - } - } - return this; - } - - static PresenceMessage fromMsgpack(MessageUnpacker unpacker) throws IOException { - return (new PresenceMessage()).readMsgpack(unpacker); - } - - /** - * Refer Spec TP4
- * An alternative constructor that take an PresenceMessage-JSON object and a channelOptions (optional), and return a PresenceMessage - * @param messageJsonObject - * @param channelOptions - * @return - * @throws MessageDecodeException - */ - public static PresenceMessage fromEncoded(JsonObject messageJsonObject, ChannelOptions channelOptions) throws MessageDecodeException { - try { - PresenceMessage presenceMessage = Serialisation.gson.fromJson(messageJsonObject, PresenceMessage.class); - presenceMessage.decode(channelOptions); - if(presenceMessage.action == null){ - throw MessageDecodeException.fromDescription("Action cannot be null/empty"); - } - return presenceMessage; - } catch(Exception e) { - Log.e(PresenceMessage.class.getName(), e.getMessage(), e); - throw MessageDecodeException.fromDescription(e.getMessage()); - } - } - - /** - * Refer Spec TP4
- * An alternative constructor that takes a Stringified PresenceMessage-JSON and a channelOptions (optional), and return a PresenceMessage - * @param messageJson - * @param channelOptions - * @return - * @throws MessageDecodeException - */ - public static PresenceMessage fromEncoded(String messageJson, ChannelOptions channelOptions) throws MessageDecodeException { - try { - JsonObject jsonObject = Serialisation.gson.fromJson(messageJson, JsonObject.class); - return fromEncoded(jsonObject, channelOptions); - } catch(Exception e) { - Log.e(PresenceMessage.class.getName(), e.getMessage(), e); - throw MessageDecodeException.fromDescription(e.getMessage()); - } - } - - /** - * Refer Spec TP4
- * An alternative constructor that takes a PresenceMessage JsonArray and a channelOptions (optional), and return array of PresenceMessages. - * @param presenceMsgArray - * @param channelOptions - * @return - * @throws MessageDecodeException - */ - public static PresenceMessage[] fromEncodedArray(JsonArray presenceMsgArray, ChannelOptions channelOptions) throws MessageDecodeException { - try { - PresenceMessage[] messages = new PresenceMessage[presenceMsgArray.size()]; - for(int index = 0; index < presenceMsgArray.size(); index++) { - JsonElement jsonElement = presenceMsgArray.get(index); - if(!jsonElement.isJsonObject()) { - throw new JsonParseException("Not all JSON elements are of type JSON Object."); - } - messages[index] = fromEncoded(jsonElement.getAsJsonObject(), channelOptions); - } - return messages; - } catch(Exception e) { - Log.e(PresenceMessage.class.getName(), e.getMessage(), e); - throw MessageDecodeException.fromDescription(e.getMessage()); - } - } - - /** - * Refer Spec TP4
- * An alternative constructor that takes a Stringified PresenceMessages Array and a channelOptions (optional), and return array of PresenceMessages. - * @param presenceMsgArray - * @param channelOptions - * @return - * @throws MessageDecodeException - */ - public static PresenceMessage[] fromEncodedArray(String presenceMsgArray, ChannelOptions channelOptions) throws MessageDecodeException { - try { - JsonArray jsonArray = Serialisation.gson.fromJson(presenceMsgArray, JsonArray.class); - return fromEncodedArray(jsonArray, channelOptions); - } catch(Exception e) { - Log.e(PresenceMessage.class.getName(), e.getMessage(), e); - throw MessageDecodeException.fromDescription(e.getMessage()); - } - } - - public static class ActionSerializer implements JsonDeserializer { - @Override - public Action deserialize(JsonElement json, Type t, JsonDeserializationContext ctx) - throws JsonParseException { - return Action.findByValue(json.getAsInt()); - } - } - - public static class Serializer implements JsonSerializer { - @Override - public JsonElement serialize(PresenceMessage message, Type typeOfMessage, JsonSerializationContext ctx) { - final JsonObject json = BaseMessage.toJsonObject(message); - if(message.action != null) json.addProperty("action", message.action.getValue()); - return json; - } - } - - /** - * Get the member key for the PresenceMessage. - * @return - */ - public String memberKey() { - return connectionId + ':' + clientId; - } - - private static final String TAG = PresenceMessage.class.getName(); + /** + * Presence Action: the event signified by a PresenceMessage + */ + public enum Action { + absent, + present, + enter, + leave, + update; + + public int getValue() { return ordinal(); } + public static Action findByValue(int value) { return values()[value]; } + } + + public Action action; + + /** + * Default constructor + */ + public PresenceMessage() {} + + /** + * Construct a PresenceMessage from an Action and clientId + * @param action + * @param clientId + */ + public PresenceMessage(Action action, String clientId) { + this(action, clientId, null); + } + + /** + * Generic constructor + * @param action + * @param clientId + * @param data + */ + public PresenceMessage(Action action, String clientId, Object data) { + this.action = action; + this.clientId = clientId; + this.data = data; + } + + /** + * Generate a String summary of this PresenceMessage + * @return string + */ + public String toString() { + StringBuilder result = new StringBuilder("[PresenceMessage"); + super.getDetails(result); + result.append(" action=").append(action.name()); + result.append(']'); + return result.toString(); + } + + @Override + public Object clone() { + PresenceMessage result = new PresenceMessage(); + result.id = id; + result.timestamp = timestamp; + result.clientId = clientId; + result.connectionId = connectionId; + result.encoding = encoding; + result.data = data; + result.action = action; + return result; + } + + void writeMsgpack(MessagePacker packer) throws IOException { + int fieldCount = super.countFields(); + ++fieldCount; + packer.packMapHeader(fieldCount); + super.writeFields(packer); + packer.packString("action"); + packer.packInt(action.getValue()); + } + + PresenceMessage readMsgpack(MessageUnpacker unpacker) throws IOException { + int fieldCount = unpacker.unpackMapHeader(); + for(int i = 0; i < fieldCount; i++) { + String fieldName = unpacker.unpackString().intern(); + MessageFormat fieldFormat = unpacker.getNextFormat(); + if(fieldFormat.equals(MessageFormat.NIL)) { unpacker.unpackNil(); continue; } + + if(super.readField(unpacker, fieldName, fieldFormat)) { continue; } + if(fieldName.equals("action")) { + action = Action.findByValue(unpacker.unpackInt()); + } else { + Log.v(TAG, "Unexpected field: " + fieldName); + unpacker.skipValue(); + } + } + return this; + } + + static PresenceMessage fromMsgpack(MessageUnpacker unpacker) throws IOException { + return (new PresenceMessage()).readMsgpack(unpacker); + } + + /** + * Refer Spec TP4
+ * An alternative constructor that take an PresenceMessage-JSON object and a channelOptions (optional), and return a PresenceMessage + * @param messageJsonObject + * @param channelOptions + * @return + * @throws MessageDecodeException + */ + public static PresenceMessage fromEncoded(JsonObject messageJsonObject, ChannelOptions channelOptions) throws MessageDecodeException { + try { + PresenceMessage presenceMessage = Serialisation.gson.fromJson(messageJsonObject, PresenceMessage.class); + presenceMessage.decode(channelOptions); + if(presenceMessage.action == null){ + throw MessageDecodeException.fromDescription("Action cannot be null/empty"); + } + return presenceMessage; + } catch(Exception e) { + Log.e(PresenceMessage.class.getName(), e.getMessage(), e); + throw MessageDecodeException.fromDescription(e.getMessage()); + } + } + + /** + * Refer Spec TP4
+ * An alternative constructor that takes a Stringified PresenceMessage-JSON and a channelOptions (optional), and return a PresenceMessage + * @param messageJson + * @param channelOptions + * @return + * @throws MessageDecodeException + */ + public static PresenceMessage fromEncoded(String messageJson, ChannelOptions channelOptions) throws MessageDecodeException { + try { + JsonObject jsonObject = Serialisation.gson.fromJson(messageJson, JsonObject.class); + return fromEncoded(jsonObject, channelOptions); + } catch(Exception e) { + Log.e(PresenceMessage.class.getName(), e.getMessage(), e); + throw MessageDecodeException.fromDescription(e.getMessage()); + } + } + + /** + * Refer Spec TP4
+ * An alternative constructor that takes a PresenceMessage JsonArray and a channelOptions (optional), and return array of PresenceMessages. + * @param presenceMsgArray + * @param channelOptions + * @return + * @throws MessageDecodeException + */ + public static PresenceMessage[] fromEncodedArray(JsonArray presenceMsgArray, ChannelOptions channelOptions) throws MessageDecodeException { + try { + PresenceMessage[] messages = new PresenceMessage[presenceMsgArray.size()]; + for(int index = 0; index < presenceMsgArray.size(); index++) { + JsonElement jsonElement = presenceMsgArray.get(index); + if(!jsonElement.isJsonObject()) { + throw new JsonParseException("Not all JSON elements are of type JSON Object."); + } + messages[index] = fromEncoded(jsonElement.getAsJsonObject(), channelOptions); + } + return messages; + } catch(Exception e) { + Log.e(PresenceMessage.class.getName(), e.getMessage(), e); + throw MessageDecodeException.fromDescription(e.getMessage()); + } + } + + /** + * Refer Spec TP4
+ * An alternative constructor that takes a Stringified PresenceMessages Array and a channelOptions (optional), and return array of PresenceMessages. + * @param presenceMsgArray + * @param channelOptions + * @return + * @throws MessageDecodeException + */ + public static PresenceMessage[] fromEncodedArray(String presenceMsgArray, ChannelOptions channelOptions) throws MessageDecodeException { + try { + JsonArray jsonArray = Serialisation.gson.fromJson(presenceMsgArray, JsonArray.class); + return fromEncodedArray(jsonArray, channelOptions); + } catch(Exception e) { + Log.e(PresenceMessage.class.getName(), e.getMessage(), e); + throw MessageDecodeException.fromDescription(e.getMessage()); + } + } + + public static class ActionSerializer implements JsonDeserializer { + @Override + public Action deserialize(JsonElement json, Type t, JsonDeserializationContext ctx) + throws JsonParseException { + return Action.findByValue(json.getAsInt()); + } + } + + public static class Serializer implements JsonSerializer { + @Override + public JsonElement serialize(PresenceMessage message, Type typeOfMessage, JsonSerializationContext ctx) { + final JsonObject json = BaseMessage.toJsonObject(message); + if(message.action != null) json.addProperty("action", message.action.getValue()); + return json; + } + } + + /** + * Get the member key for the PresenceMessage. + * @return + */ + public String memberKey() { + return connectionId + ':' + clientId; + } + + private static final String TAG = PresenceMessage.class.getName(); } diff --git a/lib/src/main/java/io/ably/lib/types/PresenceSerializer.java b/lib/src/main/java/io/ably/lib/types/PresenceSerializer.java index 9c0a70486..9919010e0 100644 --- a/lib/src/main/java/io/ably/lib/types/PresenceSerializer.java +++ b/lib/src/main/java/io/ably/lib/types/PresenceSerializer.java @@ -18,109 +18,109 @@ */ public class PresenceSerializer { - /**************************************** - * Msgpack decode - ****************************************/ - - public static PresenceMessage[] readMsgpackArray(MessageUnpacker unpacker) throws IOException { - int count = unpacker.unpackArrayHeader(); - PresenceMessage[] result = new PresenceMessage[count]; - for(int i = 0; i < count; i++) - result[i] = PresenceMessage.fromMsgpack(unpacker); - return result; - } - - public static PresenceMessage[] readMsgpack(byte[] packed) throws AblyException { - try { - MessageUnpacker unpacker = Serialisation.msgpackUnpackerConfig.newUnpacker(packed); - return readMsgpackArray(unpacker); - } catch(IOException ioe) { - throw AblyException.fromThrowable(ioe); - } - } - - /**************************************** - * Msgpack encode - ****************************************/ - - public static byte[] writeMsgpackArray(PresenceMessage[] messages) { - try { - ByteArrayOutputStream out = new ByteArrayOutputStream(); - MessagePacker packer = Serialisation.msgpackPackerConfig.newPacker(out); - writeMsgpackArray(messages, packer); - packer.flush(); - return out.toByteArray(); - } catch(IOException e) { return null; } - } - - public static void writeMsgpackArray(PresenceMessage[] messages, MessagePacker packer) { - try { - int count = messages.length; - packer.packArrayHeader(count); - for(PresenceMessage message : messages) - message.writeMsgpack(packer); - } catch(IOException e) {} - } - - /**************************************** - * JSON decode - ****************************************/ - - private static PresenceMessage[] readJson(byte[] packed) throws IOException { - return Serialisation.gson.fromJson(new String(packed), PresenceMessage[].class); - } - - /**************************************** - * JSON encode - ****************************************/ - - public static HttpCore.RequestBody asJsonRequest(PresenceMessage message) throws AblyException { - return asJsonRequest(new PresenceMessage[] { message }); - } - - public static HttpCore.RequestBody asJsonRequest(PresenceMessage[] messages) { - return new HttpUtils.JsonRequestBody(Serialisation.gson.toJson(messages)); - } - - /**************************************** - * BodyHandler - ****************************************/ - - public static HttpCore.BodyHandler getPresenceResponseHandler(ChannelOptions opts) { - return opts == null ? presenceResponseHandler : new PresenceBodyHandler(opts); - } - - private static class PresenceBodyHandler implements HttpCore.BodyHandler { - - PresenceBodyHandler(ChannelOptions opts) { this.opts = opts; } - - @Override - public PresenceMessage[] handleResponseBody(String contentType, byte[] body) throws AblyException { - try { - PresenceMessage[] messages = null; - if("application/json".equals(contentType)) - messages = readJson(body); - else if("application/x-msgpack".equals(contentType)) - messages = readMsgpack(body); - if(messages != null) { - for (PresenceMessage message : messages) { - try { - message.decode(opts); - } catch (MessageDecodeException e) { - Log.e(TAG, e.errorInfo.message); - } - } - } - return messages; - } catch(IOException e) { - throw AblyException.fromThrowable(e); - } - } - - private ChannelOptions opts; - } - - private static HttpCore.BodyHandler presenceResponseHandler = new PresenceBodyHandler(null); - - private static final String TAG = PresenceSerializer.class.getName(); + /**************************************** + * Msgpack decode + ****************************************/ + + public static PresenceMessage[] readMsgpackArray(MessageUnpacker unpacker) throws IOException { + int count = unpacker.unpackArrayHeader(); + PresenceMessage[] result = new PresenceMessage[count]; + for(int i = 0; i < count; i++) + result[i] = PresenceMessage.fromMsgpack(unpacker); + return result; + } + + public static PresenceMessage[] readMsgpack(byte[] packed) throws AblyException { + try { + MessageUnpacker unpacker = Serialisation.msgpackUnpackerConfig.newUnpacker(packed); + return readMsgpackArray(unpacker); + } catch(IOException ioe) { + throw AblyException.fromThrowable(ioe); + } + } + + /**************************************** + * Msgpack encode + ****************************************/ + + public static byte[] writeMsgpackArray(PresenceMessage[] messages) { + try { + ByteArrayOutputStream out = new ByteArrayOutputStream(); + MessagePacker packer = Serialisation.msgpackPackerConfig.newPacker(out); + writeMsgpackArray(messages, packer); + packer.flush(); + return out.toByteArray(); + } catch(IOException e) { return null; } + } + + public static void writeMsgpackArray(PresenceMessage[] messages, MessagePacker packer) { + try { + int count = messages.length; + packer.packArrayHeader(count); + for(PresenceMessage message : messages) + message.writeMsgpack(packer); + } catch(IOException e) {} + } + + /**************************************** + * JSON decode + ****************************************/ + + private static PresenceMessage[] readJson(byte[] packed) throws IOException { + return Serialisation.gson.fromJson(new String(packed), PresenceMessage[].class); + } + + /**************************************** + * JSON encode + ****************************************/ + + public static HttpCore.RequestBody asJsonRequest(PresenceMessage message) throws AblyException { + return asJsonRequest(new PresenceMessage[] { message }); + } + + public static HttpCore.RequestBody asJsonRequest(PresenceMessage[] messages) { + return new HttpUtils.JsonRequestBody(Serialisation.gson.toJson(messages)); + } + + /**************************************** + * BodyHandler + ****************************************/ + + public static HttpCore.BodyHandler getPresenceResponseHandler(ChannelOptions opts) { + return opts == null ? presenceResponseHandler : new PresenceBodyHandler(opts); + } + + private static class PresenceBodyHandler implements HttpCore.BodyHandler { + + PresenceBodyHandler(ChannelOptions opts) { this.opts = opts; } + + @Override + public PresenceMessage[] handleResponseBody(String contentType, byte[] body) throws AblyException { + try { + PresenceMessage[] messages = null; + if("application/json".equals(contentType)) + messages = readJson(body); + else if("application/x-msgpack".equals(contentType)) + messages = readMsgpack(body); + if(messages != null) { + for (PresenceMessage message : messages) { + try { + message.decode(opts); + } catch (MessageDecodeException e) { + Log.e(TAG, e.errorInfo.message); + } + } + } + return messages; + } catch(IOException e) { + throw AblyException.fromThrowable(e); + } + } + + private ChannelOptions opts; + } + + private static HttpCore.BodyHandler presenceResponseHandler = new PresenceBodyHandler(null); + + private static final String TAG = PresenceSerializer.class.getName(); } diff --git a/lib/src/main/java/io/ably/lib/types/ProtocolMessage.java b/lib/src/main/java/io/ably/lib/types/ProtocolMessage.java index de8744cf6..9a513dff6 100644 --- a/lib/src/main/java/io/ably/lib/types/ProtocolMessage.java +++ b/lib/src/main/java/io/ably/lib/types/ProtocolMessage.java @@ -27,269 +27,269 @@ * details on the members of a ProtocolMessage. */ public class ProtocolMessage { - public enum Action { - heartbeat, - ack, - nack, - connect, - connected, - disconnect, - disconnected, - close, - closed, - error, - attach, - attached, - detach, - detached, - presence, - message, - sync, - auth; - - public int getValue() { return ordinal(); } - public static Action findByValue(int value) { return values()[value]; } - } - - public enum Flag { - /* Channel attach state flags */ - has_presence(0), - has_backlog(1), - resumed(2), - attach_resume(5), - - /* Channel mode flags */ - presence(16), - publish(17), - subscribe(18), - presence_subscribe(19); - - private final int mask; - - Flag(int offset) { - this.mask = 1 << offset; - } - - public int getMask() { - return this.mask; - } - } - - public static boolean ackRequired(ProtocolMessage msg) { - return (msg.action == Action.message || msg.action == Action.presence); - } - - public ProtocolMessage() {} - - public ProtocolMessage(Action action) { - this.action = action; - } - - public ProtocolMessage(Action action, String channel) { - this.action = action; - this.channel = channel; - } - - public Action action; - public int flags; - public int count; - public ErrorInfo error; - public String id; - public String channel; - public String channelSerial; - public String connectionId; - public Long connectionSerial; - public Long msgSerial; - public long timestamp; - public Message[] messages; - public PresenceMessage[] presence; - public ConnectionDetails connectionDetails; - public AuthDetails auth; - public Map params; - - public boolean hasFlag(final Flag flag) { - return (flags & flag.getMask()) == flag.getMask(); - } - - public void setFlag(final Flag flag) { - flags |= flag.getMask(); - } - - public void setFlags(final int flags) { - this.flags |= flags; - } - - void writeMsgpack(MessagePacker packer) throws IOException { - int fieldCount = 1; //action - if(channel != null) ++fieldCount; - if(msgSerial != null) ++fieldCount; - if(messages != null) ++fieldCount; - if(presence != null) ++fieldCount; - if(auth != null) ++fieldCount; - if(flags != 0) ++fieldCount; - if(params != null) ++fieldCount; - if(channelSerial != null) ++fieldCount; - packer.packMapHeader(fieldCount); - packer.packString("action"); - packer.packInt(action.getValue()); - if(channel != null) { - packer.packString("channel"); - packer.packString(channel); - } - if(msgSerial != null) { - packer.packString("msgSerial"); - packer.packLong(msgSerial.longValue()); - } - if(messages != null) { - packer.packString("messages"); - MessageSerializer.writeMsgpackArray(messages, packer); - } - if(presence != null) { - packer.packString("presence"); - PresenceSerializer.writeMsgpackArray(presence, packer); - } - if(auth != null) { - packer.packString("auth"); - auth.writeMsgpack(packer); - } - if(flags != 0) { - packer.packString("flags"); - packer.packInt(flags); - } - if(params != null) { - packer.packString("params"); - MessageSerializer.write(params, packer); - } - if(channelSerial != null) { - packer.packString("channelSerial"); - packer.packString(channelSerial); - } - } - - ProtocolMessage readMsgpack(MessageUnpacker unpacker) throws IOException { - int fieldCount = unpacker.unpackMapHeader(); - for(int i = 0; i < fieldCount; i++) { - String fieldName = unpacker.unpackString().intern(); - MessageFormat fieldFormat = unpacker.getNextFormat(); - if(fieldFormat.equals(MessageFormat.NIL)) { unpacker.unpackNil(); continue; } - - switch(fieldName) { - case "action": - action = Action.findByValue(unpacker.unpackInt()); - break; - case "flags": - flags = unpacker.unpackInt(); - break; - case "count": - count = unpacker.unpackInt(); - break; - case "error": - error = ErrorInfo.fromMsgpack(unpacker); - break; - case "id": - id = unpacker.unpackString(); - break; - case "channel": - channel = unpacker.unpackString(); - break; - case "channelSerial": - channelSerial = unpacker.unpackString(); - break; - case "connectionId": - connectionId = unpacker.unpackString(); - break; - case "connectionSerial": - connectionSerial = Long.valueOf(unpacker.unpackLong()); - break; - case "msgSerial": - msgSerial = Long.valueOf(unpacker.unpackLong()); - break; - case "timestamp": - timestamp = unpacker.unpackLong(); - break; - case "messages": - messages = MessageSerializer.readMsgpackArray(unpacker); - break; - case "presence": - presence = PresenceSerializer.readMsgpackArray(unpacker); - break; - case "connectionDetails": - connectionDetails = ConnectionDetails.fromMsgpack(unpacker); - break; - case "auth": - auth = AuthDetails.fromMsgpack(unpacker); - break; - case "connectionKey": - /* deprecated; ignore */ - unpacker.unpackString(); - break; - case "params": - params = MessageSerializer.readStringMap(unpacker); - break; - default: - Log.v(TAG, "Unexpected field: " + fieldName); - unpacker.skipValue(); - } - } - return this; - } - - static ProtocolMessage fromMsgpack(MessageUnpacker unpacker) throws IOException { - return (new ProtocolMessage()).readMsgpack(unpacker); - } - - public static class ActionSerializer implements JsonSerializer, JsonDeserializer { - @Override - public Action deserialize(JsonElement json, Type t, JsonDeserializationContext ctx) - throws JsonParseException { - return Action.findByValue(json.getAsInt()); - } - - @Override - public JsonElement serialize(Action action, Type t, JsonSerializationContext ctx) { - return new JsonPrimitive(action.getValue()); - } - } - - public static class AuthDetails { - public String accessToken; - - private AuthDetails() { } - public AuthDetails(String s) { accessToken = s; } - - AuthDetails readMsgpack(MessageUnpacker unpacker) throws IOException { - int fieldCount = unpacker.unpackMapHeader(); - for(int i = 0; i < fieldCount; i++) { - String fieldName = unpacker.unpackString().intern(); - MessageFormat fieldFormat = unpacker.getNextFormat(); - if(fieldFormat.equals(MessageFormat.NIL)) { unpacker.unpackNil(); continue; } - - switch(fieldName) { - case "accessToken": - accessToken = unpacker.unpackString(); - break; - default: - Log.v(TAG, "Unexpected field: " + fieldName); - unpacker.skipValue(); - } - } - return this; - } - - static AuthDetails fromMsgpack(MessageUnpacker unpacker) throws IOException { - return (new AuthDetails()).readMsgpack(unpacker); - } - - void writeMsgpack(MessagePacker packer) throws IOException { - int fieldCount = 0; - if(accessToken != null) ++fieldCount; - packer.packMapHeader(fieldCount); - if(accessToken != null) { - packer.packString("accessToken"); - packer.packString(accessToken); - } - } - } - - private static final String TAG = ProtocolMessage.class.getName(); + public enum Action { + heartbeat, + ack, + nack, + connect, + connected, + disconnect, + disconnected, + close, + closed, + error, + attach, + attached, + detach, + detached, + presence, + message, + sync, + auth; + + public int getValue() { return ordinal(); } + public static Action findByValue(int value) { return values()[value]; } + } + + public enum Flag { + /* Channel attach state flags */ + has_presence(0), + has_backlog(1), + resumed(2), + attach_resume(5), + + /* Channel mode flags */ + presence(16), + publish(17), + subscribe(18), + presence_subscribe(19); + + private final int mask; + + Flag(int offset) { + this.mask = 1 << offset; + } + + public int getMask() { + return this.mask; + } + } + + public static boolean ackRequired(ProtocolMessage msg) { + return (msg.action == Action.message || msg.action == Action.presence); + } + + public ProtocolMessage() {} + + public ProtocolMessage(Action action) { + this.action = action; + } + + public ProtocolMessage(Action action, String channel) { + this.action = action; + this.channel = channel; + } + + public Action action; + public int flags; + public int count; + public ErrorInfo error; + public String id; + public String channel; + public String channelSerial; + public String connectionId; + public Long connectionSerial; + public Long msgSerial; + public long timestamp; + public Message[] messages; + public PresenceMessage[] presence; + public ConnectionDetails connectionDetails; + public AuthDetails auth; + public Map params; + + public boolean hasFlag(final Flag flag) { + return (flags & flag.getMask()) == flag.getMask(); + } + + public void setFlag(final Flag flag) { + flags |= flag.getMask(); + } + + public void setFlags(final int flags) { + this.flags |= flags; + } + + void writeMsgpack(MessagePacker packer) throws IOException { + int fieldCount = 1; //action + if(channel != null) ++fieldCount; + if(msgSerial != null) ++fieldCount; + if(messages != null) ++fieldCount; + if(presence != null) ++fieldCount; + if(auth != null) ++fieldCount; + if(flags != 0) ++fieldCount; + if(params != null) ++fieldCount; + if(channelSerial != null) ++fieldCount; + packer.packMapHeader(fieldCount); + packer.packString("action"); + packer.packInt(action.getValue()); + if(channel != null) { + packer.packString("channel"); + packer.packString(channel); + } + if(msgSerial != null) { + packer.packString("msgSerial"); + packer.packLong(msgSerial.longValue()); + } + if(messages != null) { + packer.packString("messages"); + MessageSerializer.writeMsgpackArray(messages, packer); + } + if(presence != null) { + packer.packString("presence"); + PresenceSerializer.writeMsgpackArray(presence, packer); + } + if(auth != null) { + packer.packString("auth"); + auth.writeMsgpack(packer); + } + if(flags != 0) { + packer.packString("flags"); + packer.packInt(flags); + } + if(params != null) { + packer.packString("params"); + MessageSerializer.write(params, packer); + } + if(channelSerial != null) { + packer.packString("channelSerial"); + packer.packString(channelSerial); + } + } + + ProtocolMessage readMsgpack(MessageUnpacker unpacker) throws IOException { + int fieldCount = unpacker.unpackMapHeader(); + for(int i = 0; i < fieldCount; i++) { + String fieldName = unpacker.unpackString().intern(); + MessageFormat fieldFormat = unpacker.getNextFormat(); + if(fieldFormat.equals(MessageFormat.NIL)) { unpacker.unpackNil(); continue; } + + switch(fieldName) { + case "action": + action = Action.findByValue(unpacker.unpackInt()); + break; + case "flags": + flags = unpacker.unpackInt(); + break; + case "count": + count = unpacker.unpackInt(); + break; + case "error": + error = ErrorInfo.fromMsgpack(unpacker); + break; + case "id": + id = unpacker.unpackString(); + break; + case "channel": + channel = unpacker.unpackString(); + break; + case "channelSerial": + channelSerial = unpacker.unpackString(); + break; + case "connectionId": + connectionId = unpacker.unpackString(); + break; + case "connectionSerial": + connectionSerial = Long.valueOf(unpacker.unpackLong()); + break; + case "msgSerial": + msgSerial = Long.valueOf(unpacker.unpackLong()); + break; + case "timestamp": + timestamp = unpacker.unpackLong(); + break; + case "messages": + messages = MessageSerializer.readMsgpackArray(unpacker); + break; + case "presence": + presence = PresenceSerializer.readMsgpackArray(unpacker); + break; + case "connectionDetails": + connectionDetails = ConnectionDetails.fromMsgpack(unpacker); + break; + case "auth": + auth = AuthDetails.fromMsgpack(unpacker); + break; + case "connectionKey": + /* deprecated; ignore */ + unpacker.unpackString(); + break; + case "params": + params = MessageSerializer.readStringMap(unpacker); + break; + default: + Log.v(TAG, "Unexpected field: " + fieldName); + unpacker.skipValue(); + } + } + return this; + } + + static ProtocolMessage fromMsgpack(MessageUnpacker unpacker) throws IOException { + return (new ProtocolMessage()).readMsgpack(unpacker); + } + + public static class ActionSerializer implements JsonSerializer, JsonDeserializer { + @Override + public Action deserialize(JsonElement json, Type t, JsonDeserializationContext ctx) + throws JsonParseException { + return Action.findByValue(json.getAsInt()); + } + + @Override + public JsonElement serialize(Action action, Type t, JsonSerializationContext ctx) { + return new JsonPrimitive(action.getValue()); + } + } + + public static class AuthDetails { + public String accessToken; + + private AuthDetails() { } + public AuthDetails(String s) { accessToken = s; } + + AuthDetails readMsgpack(MessageUnpacker unpacker) throws IOException { + int fieldCount = unpacker.unpackMapHeader(); + for(int i = 0; i < fieldCount; i++) { + String fieldName = unpacker.unpackString().intern(); + MessageFormat fieldFormat = unpacker.getNextFormat(); + if(fieldFormat.equals(MessageFormat.NIL)) { unpacker.unpackNil(); continue; } + + switch(fieldName) { + case "accessToken": + accessToken = unpacker.unpackString(); + break; + default: + Log.v(TAG, "Unexpected field: " + fieldName); + unpacker.skipValue(); + } + } + return this; + } + + static AuthDetails fromMsgpack(MessageUnpacker unpacker) throws IOException { + return (new AuthDetails()).readMsgpack(unpacker); + } + + void writeMsgpack(MessagePacker packer) throws IOException { + int fieldCount = 0; + if(accessToken != null) ++fieldCount; + packer.packMapHeader(fieldCount); + if(accessToken != null) { + packer.packString("accessToken"); + packer.packString(accessToken); + } + } + } + + private static final String TAG = ProtocolMessage.class.getName(); } diff --git a/lib/src/main/java/io/ably/lib/types/ProtocolSerializer.java b/lib/src/main/java/io/ably/lib/types/ProtocolSerializer.java index 9bd2770ad..97e5fc80b 100644 --- a/lib/src/main/java/io/ably/lib/types/ProtocolSerializer.java +++ b/lib/src/main/java/io/ably/lib/types/ProtocolSerializer.java @@ -11,47 +11,47 @@ public class ProtocolSerializer { - /**************************************** - * Msgpack decode - ****************************************/ - - public static ProtocolMessage readMsgpack(byte[] packed) throws AblyException { - try { - MessageUnpacker unpacker = Serialisation.msgpackUnpackerConfig.newUnpacker(packed); - return ProtocolMessage.fromMsgpack(unpacker); - } catch (IOException ioe) { - throw AblyException.fromThrowable(ioe); - } - } - - /**************************************** - * Msgpack encode - ****************************************/ - - public static byte[] writeMsgpack(ProtocolMessage message) { - ByteArrayOutputStream out = new ByteArrayOutputStream(); - MessagePacker packer = Serialisation.msgpackPackerConfig.newPacker(out); - try { - message.writeMsgpack(packer); - - packer.flush(); - return out.toByteArray(); - } catch(IOException e) { return null; } - } - - /**************************************** - * JSON decode - ****************************************/ - - public static ProtocolMessage fromJSON(String packed) throws AblyException { - return Serialisation.gson.fromJson(packed, ProtocolMessage.class); - } - - /**************************************** - * JSON encode - ****************************************/ - - public static byte[] writeJSON(ProtocolMessage message) throws AblyException { - return Serialisation.gson.toJson(message).getBytes(Charset.forName("UTF-8")); - } + /**************************************** + * Msgpack decode + ****************************************/ + + public static ProtocolMessage readMsgpack(byte[] packed) throws AblyException { + try { + MessageUnpacker unpacker = Serialisation.msgpackUnpackerConfig.newUnpacker(packed); + return ProtocolMessage.fromMsgpack(unpacker); + } catch (IOException ioe) { + throw AblyException.fromThrowable(ioe); + } + } + + /**************************************** + * Msgpack encode + ****************************************/ + + public static byte[] writeMsgpack(ProtocolMessage message) { + ByteArrayOutputStream out = new ByteArrayOutputStream(); + MessagePacker packer = Serialisation.msgpackPackerConfig.newPacker(out); + try { + message.writeMsgpack(packer); + + packer.flush(); + return out.toByteArray(); + } catch(IOException e) { return null; } + } + + /**************************************** + * JSON decode + ****************************************/ + + public static ProtocolMessage fromJSON(String packed) throws AblyException { + return Serialisation.gson.fromJson(packed, ProtocolMessage.class); + } + + /**************************************** + * JSON encode + ****************************************/ + + public static byte[] writeJSON(ProtocolMessage message) throws AblyException { + return Serialisation.gson.toJson(message).getBytes(Charset.forName("UTF-8")); + } } diff --git a/lib/src/main/java/io/ably/lib/types/ProxyOptions.java b/lib/src/main/java/io/ably/lib/types/ProxyOptions.java index 071a85551..11465f8c1 100644 --- a/lib/src/main/java/io/ably/lib/types/ProxyOptions.java +++ b/lib/src/main/java/io/ably/lib/types/ProxyOptions.java @@ -3,10 +3,10 @@ import io.ably.lib.http.HttpAuth; public class ProxyOptions { - public String host; - public int port; - public String username; - public String password; - public String[] nonProxyHosts; - public HttpAuth.Type prefAuthType = HttpAuth.Type.BASIC; + public String host; + public int port; + public String username; + public String password; + public String[] nonProxyHosts; + public HttpAuth.Type prefAuthType = HttpAuth.Type.BASIC; } diff --git a/lib/src/main/java/io/ably/lib/types/PublishResponse.java b/lib/src/main/java/io/ably/lib/types/PublishResponse.java index fe2866bc3..7d720d2df 100644 --- a/lib/src/main/java/io/ably/lib/types/PublishResponse.java +++ b/lib/src/main/java/io/ably/lib/types/PublishResponse.java @@ -14,140 +14,140 @@ ****************************************/ public class PublishResponse { - public ErrorInfo error; - @SerializedName("channel") - public String channelId; - public String messageId; - - private static PublishResponse[] fromJSONArray(byte[] json) { - return Serialisation.gson.fromJson(new String(json), PublishResponse[].class); - } - - private static PublishResponse fromMsgpack(MessageUnpacker unpacker) throws IOException { - return (new PublishResponse()).readMsgpack(unpacker); - } - - private static PublishResponse[] fromMsgpackArray(byte[] msgpack) throws IOException { - MessageUnpacker unpacker = Serialisation.msgpackUnpackerConfig.newUnpacker(msgpack); - return fromMsgpackArray(unpacker); - } - - private static PublishResponse[] fromMsgpackArray(MessageUnpacker unpacker) throws IOException { - int count = unpacker.unpackArrayHeader(); - PublishResponse[] result = new PublishResponse[count]; - for(int j = 0; j < count; j++) { - result[j] = PublishResponse.fromMsgpack(unpacker); - } - return result; - } - - private PublishResponse readMsgpack(MessageUnpacker unpacker) throws IOException { - int fieldCount = unpacker.unpackMapHeader(); - for(int i = 0; i < fieldCount; i++) { - String fieldName = unpacker.unpackString().intern(); - MessageFormat fieldFormat = unpacker.getNextFormat(); - if(fieldFormat.equals(MessageFormat.NIL)) { unpacker.unpackNil(); continue; } - - switch(fieldName) { - case "error": - error = ErrorInfo.fromMsgpack(unpacker); - break; - case "channel": - case "channelId": - channelId = unpacker.unpackString(); - break; - case "messageId": - messageId = unpacker.unpackString(); - break; - default: - Log.v(TAG, "Unexpected field: " + fieldName); - unpacker.skipValue(); - } - } - return this; - } - - public static HttpCore.BodyHandler getBulkPublishResponseHandler(int statusCode) { - return (statusCode < 300) ? bulkResponseBodyHandler : batchErrorBodyHandler; - } - - private static class BatchErrorResponse { - public ErrorInfo error; - public PublishResponse[] batchResponse; - - static BatchErrorResponse readJSON(byte[] json) { - return Serialisation.gson.fromJson(new String(json), BatchErrorResponse.class); - } - - static BatchErrorResponse readMsgpack(byte[] msgpack) throws IOException { - MessageUnpacker unpacker = Serialisation.msgpackUnpackerConfig.newUnpacker(msgpack); - return (new BatchErrorResponse()).readMsgpack(unpacker); - } - - BatchErrorResponse readMsgpack(MessageUnpacker unpacker) throws IOException { - int fieldCount = unpacker.unpackMapHeader(); - for(int i = 0; i < fieldCount; i++) { - String fieldName = unpacker.unpackString().intern(); - MessageFormat fieldFormat = unpacker.getNextFormat(); - if(fieldFormat.equals(MessageFormat.NIL)) { unpacker.unpackNil(); continue; } - - switch(fieldName) { - case "error": - error = ErrorInfo.fromMsgpack(unpacker); - break; - case "batchResponse": - batchResponse = PublishResponse.fromMsgpackArray(unpacker); - break; - default: - Log.v(TAG, "Unexpected field: " + fieldName); - unpacker.skipValue(); - } - } - return this; - } - } - - private static class BulkResponseBodyHandler implements HttpCore.BodyHandler { - @Override - public PublishResponse[] handleResponseBody(String contentType, byte[] body) throws AblyException { - try { - if("application/json".equals(contentType)) { - return PublishResponse.fromJSONArray(body); - } else if("application/x-msgpack".equals(contentType)) { - return PublishResponse.fromMsgpackArray(body); - } - return null; - } catch(IOException e) { - throw AblyException.fromThrowable(e); - } - } - } - - private static class BatchErrorBodyHandler implements HttpCore.BodyHandler { - @Override - public PublishResponse[] handleResponseBody(String contentType, byte[] body) throws AblyException { - try { - BatchErrorResponse response = null; - if("application/json".equals(contentType)) { - response = BatchErrorResponse.readJSON(body); - } else if("application/x-msgpack".equals(contentType)) { - response = BatchErrorResponse.readMsgpack(body); - } - if(response == null) { - return null; - } - if(response.error != null && response.error.code != 40020) { - throw AblyException.fromErrorInfo(response.error); - } - return response.batchResponse; - } catch(IOException e) { - throw AblyException.fromThrowable(e); - } - } - } - - private static HttpCore.BodyHandler batchErrorBodyHandler = new BatchErrorBodyHandler(); - private static HttpCore.BodyHandler bulkResponseBodyHandler = new BulkResponseBodyHandler(); - - private static final String TAG = MessageSerializer.class.getName(); + public ErrorInfo error; + @SerializedName("channel") + public String channelId; + public String messageId; + + private static PublishResponse[] fromJSONArray(byte[] json) { + return Serialisation.gson.fromJson(new String(json), PublishResponse[].class); + } + + private static PublishResponse fromMsgpack(MessageUnpacker unpacker) throws IOException { + return (new PublishResponse()).readMsgpack(unpacker); + } + + private static PublishResponse[] fromMsgpackArray(byte[] msgpack) throws IOException { + MessageUnpacker unpacker = Serialisation.msgpackUnpackerConfig.newUnpacker(msgpack); + return fromMsgpackArray(unpacker); + } + + private static PublishResponse[] fromMsgpackArray(MessageUnpacker unpacker) throws IOException { + int count = unpacker.unpackArrayHeader(); + PublishResponse[] result = new PublishResponse[count]; + for(int j = 0; j < count; j++) { + result[j] = PublishResponse.fromMsgpack(unpacker); + } + return result; + } + + private PublishResponse readMsgpack(MessageUnpacker unpacker) throws IOException { + int fieldCount = unpacker.unpackMapHeader(); + for(int i = 0; i < fieldCount; i++) { + String fieldName = unpacker.unpackString().intern(); + MessageFormat fieldFormat = unpacker.getNextFormat(); + if(fieldFormat.equals(MessageFormat.NIL)) { unpacker.unpackNil(); continue; } + + switch(fieldName) { + case "error": + error = ErrorInfo.fromMsgpack(unpacker); + break; + case "channel": + case "channelId": + channelId = unpacker.unpackString(); + break; + case "messageId": + messageId = unpacker.unpackString(); + break; + default: + Log.v(TAG, "Unexpected field: " + fieldName); + unpacker.skipValue(); + } + } + return this; + } + + public static HttpCore.BodyHandler getBulkPublishResponseHandler(int statusCode) { + return (statusCode < 300) ? bulkResponseBodyHandler : batchErrorBodyHandler; + } + + private static class BatchErrorResponse { + public ErrorInfo error; + public PublishResponse[] batchResponse; + + static BatchErrorResponse readJSON(byte[] json) { + return Serialisation.gson.fromJson(new String(json), BatchErrorResponse.class); + } + + static BatchErrorResponse readMsgpack(byte[] msgpack) throws IOException { + MessageUnpacker unpacker = Serialisation.msgpackUnpackerConfig.newUnpacker(msgpack); + return (new BatchErrorResponse()).readMsgpack(unpacker); + } + + BatchErrorResponse readMsgpack(MessageUnpacker unpacker) throws IOException { + int fieldCount = unpacker.unpackMapHeader(); + for(int i = 0; i < fieldCount; i++) { + String fieldName = unpacker.unpackString().intern(); + MessageFormat fieldFormat = unpacker.getNextFormat(); + if(fieldFormat.equals(MessageFormat.NIL)) { unpacker.unpackNil(); continue; } + + switch(fieldName) { + case "error": + error = ErrorInfo.fromMsgpack(unpacker); + break; + case "batchResponse": + batchResponse = PublishResponse.fromMsgpackArray(unpacker); + break; + default: + Log.v(TAG, "Unexpected field: " + fieldName); + unpacker.skipValue(); + } + } + return this; + } + } + + private static class BulkResponseBodyHandler implements HttpCore.BodyHandler { + @Override + public PublishResponse[] handleResponseBody(String contentType, byte[] body) throws AblyException { + try { + if("application/json".equals(contentType)) { + return PublishResponse.fromJSONArray(body); + } else if("application/x-msgpack".equals(contentType)) { + return PublishResponse.fromMsgpackArray(body); + } + return null; + } catch(IOException e) { + throw AblyException.fromThrowable(e); + } + } + } + + private static class BatchErrorBodyHandler implements HttpCore.BodyHandler { + @Override + public PublishResponse[] handleResponseBody(String contentType, byte[] body) throws AblyException { + try { + BatchErrorResponse response = null; + if("application/json".equals(contentType)) { + response = BatchErrorResponse.readJSON(body); + } else if("application/x-msgpack".equals(contentType)) { + response = BatchErrorResponse.readMsgpack(body); + } + if(response == null) { + return null; + } + if(response.error != null && response.error.code != 40020) { + throw AblyException.fromErrorInfo(response.error); + } + return response.batchResponse; + } catch(IOException e) { + throw AblyException.fromThrowable(e); + } + } + } + + private static HttpCore.BodyHandler batchErrorBodyHandler = new BatchErrorBodyHandler(); + private static HttpCore.BodyHandler bulkResponseBodyHandler = new BulkResponseBodyHandler(); + + private static final String TAG = MessageSerializer.class.getName(); } diff --git a/lib/src/main/java/io/ably/lib/types/Stats.java b/lib/src/main/java/io/ably/lib/types/Stats.java index f4b56ccd1..9d7a127ce 100644 --- a/lib/src/main/java/io/ably/lib/types/Stats.java +++ b/lib/src/main/java/io/ably/lib/types/Stats.java @@ -16,114 +16,114 @@ */ public class Stats { - /** - * A breakdown of summary stats data for different (tls vs non-tls) - * connection types. - */ - public static class ConnectionTypes { - public ResourceCount all; - public ResourceCount plain; - public ResourceCount tls; - } - - /** - * A datapoint for message volume (number of messages plus aggregate data size) - */ - public static class MessageCount { - public double count; - public double data; - public double uncompressedData; - } - - public static class MessageCategory extends MessageCount { - public Map category; - } - - /** - * A breakdown of summary stats data for different (message vs presence) - * message types. - */ - public static class MessageTypes { - public MessageCategory all; - public MessageCategory messages; - public MessageCategory presence; - } - - /** - * A breakdown of summary stats data for traffic over various transport types. - */ - public static class MessageTraffic { - public MessageTypes all; - public MessageTypes realtime; - public MessageTypes rest; - public MessageTypes webhook; - } - - /** - * Aggregate data for numbers of requests in a specific scope. - */ - public static class RequestCount { - public double succeeded; - public double failed; - public double refused; - } - - /** - * Aggregate data for usage of a resource in a specific scope. - */ - public static class ResourceCount { - public double opened; - public double peak; - public double mean; - public double min; - public double refused; - } - - public static class ProcessedCount { - public double succeeded; - public double skipped; - public double failed; - } - - public static class ProcessedMessages { - public Map delta; - } - - public enum Granularity { - minute, - hour, - day, - month - } - - private static String[] intervalFormatString = new String[] { - "yyyy-MM-dd:hh:mm", - "yyyy-MM-dd:hh", - "yyyy-MM-dd", - "yyyy-MM" - }; - - public static String toIntervalId(long timestamp, Granularity granularity) { - String formatString = intervalFormatString[granularity.ordinal()]; - return new SimpleDateFormat(formatString).format(new Date(timestamp)); - } - - public static long fromIntervalId(String intervalId) { - try { - String formatString = intervalFormatString[0].substring(0, intervalId.length()); - return new SimpleDateFormat(formatString).parse(intervalId).getTime(); - } catch (ParseException e) { return 0; } - } - - public String intervalId; - public String unit; - public MessageTypes all; - public MessageTraffic inbound; - public MessageTraffic outbound; - public MessageTypes persisted; - public ConnectionTypes connections; - public ResourceCount channels; - public RequestCount apiRequests; - public RequestCount tokenRequests; - public ProcessedMessages processed; + /** + * A breakdown of summary stats data for different (tls vs non-tls) + * connection types. + */ + public static class ConnectionTypes { + public ResourceCount all; + public ResourceCount plain; + public ResourceCount tls; + } + + /** + * A datapoint for message volume (number of messages plus aggregate data size) + */ + public static class MessageCount { + public double count; + public double data; + public double uncompressedData; + } + + public static class MessageCategory extends MessageCount { + public Map category; + } + + /** + * A breakdown of summary stats data for different (message vs presence) + * message types. + */ + public static class MessageTypes { + public MessageCategory all; + public MessageCategory messages; + public MessageCategory presence; + } + + /** + * A breakdown of summary stats data for traffic over various transport types. + */ + public static class MessageTraffic { + public MessageTypes all; + public MessageTypes realtime; + public MessageTypes rest; + public MessageTypes webhook; + } + + /** + * Aggregate data for numbers of requests in a specific scope. + */ + public static class RequestCount { + public double succeeded; + public double failed; + public double refused; + } + + /** + * Aggregate data for usage of a resource in a specific scope. + */ + public static class ResourceCount { + public double opened; + public double peak; + public double mean; + public double min; + public double refused; + } + + public static class ProcessedCount { + public double succeeded; + public double skipped; + public double failed; + } + + public static class ProcessedMessages { + public Map delta; + } + + public enum Granularity { + minute, + hour, + day, + month + } + + private static String[] intervalFormatString = new String[] { + "yyyy-MM-dd:hh:mm", + "yyyy-MM-dd:hh", + "yyyy-MM-dd", + "yyyy-MM" + }; + + public static String toIntervalId(long timestamp, Granularity granularity) { + String formatString = intervalFormatString[granularity.ordinal()]; + return new SimpleDateFormat(formatString).format(new Date(timestamp)); + } + + public static long fromIntervalId(String intervalId) { + try { + String formatString = intervalFormatString[0].substring(0, intervalId.length()); + return new SimpleDateFormat(formatString).parse(intervalId).getTime(); + } catch (ParseException e) { return 0; } + } + + public String intervalId; + public String unit; + public MessageTypes all; + public MessageTraffic inbound; + public MessageTraffic outbound; + public MessageTypes persisted; + public ConnectionTypes connections; + public ResourceCount channels; + public RequestCount apiRequests; + public RequestCount tokenRequests; + public ProcessedMessages processed; } diff --git a/lib/src/main/java/io/ably/lib/types/StatsReader.java b/lib/src/main/java/io/ably/lib/types/StatsReader.java index 66b8cafb8..21456b3e1 100644 --- a/lib/src/main/java/io/ably/lib/types/StatsReader.java +++ b/lib/src/main/java/io/ably/lib/types/StatsReader.java @@ -11,24 +11,24 @@ */ public class StatsReader { - public static Stats[] readJson(byte[] jsonBytes) throws AblyException { - try { - return readJson(new String(jsonBytes, "UTF-8")); - } catch (UnsupportedEncodingException e) { - throw AblyException.fromThrowable(e); - } - } + public static Stats[] readJson(byte[] jsonBytes) throws AblyException { + try { + return readJson(new String(jsonBytes, "UTF-8")); + } catch (UnsupportedEncodingException e) { + throw AblyException.fromThrowable(e); + } + } - public static Stats[] readJson(String packed) throws AblyException { - return Serialisation.gson.fromJson(packed, Stats[].class); - } + public static Stats[] readJson(String packed) throws AblyException { + return Serialisation.gson.fromJson(packed, Stats[].class); + } - public static HttpCore.BodyHandler statsResponseHandler = new HttpCore.BodyHandler() { - @Override - public Stats[] handleResponseBody(String contentType, byte[] body) throws AblyException { - if("application/json".equals(contentType)) - return readJson(body); - return null; - } - }; + public static HttpCore.BodyHandler statsResponseHandler = new HttpCore.BodyHandler() { + @Override + public Stats[] handleResponseBody(String contentType, byte[] body) throws AblyException { + if("application/json".equals(contentType)) + return readJson(body); + return null; + } + }; } diff --git a/lib/src/main/java/io/ably/lib/util/CollectionUtils.java b/lib/src/main/java/io/ably/lib/util/CollectionUtils.java index 5799f6b0e..fe91ca934 100644 --- a/lib/src/main/java/io/ably/lib/util/CollectionUtils.java +++ b/lib/src/main/java/io/ably/lib/util/CollectionUtils.java @@ -4,19 +4,19 @@ import java.util.Map; public final class CollectionUtils { - private CollectionUtils() { } - - /** - * Creates a shallow copy. - * - * @param Key type. - * @param Value type. - * @param map The map to be copied. - * @return A new map. - */ - public static Map copy(final Map map) { - final Map copy = new HashMap<>(map.size()); - copy.putAll(map); - return copy; - } + private CollectionUtils() { } + + /** + * Creates a shallow copy. + * + * @param Key type. + * @param Value type. + * @param map The map to be copied. + * @return A new map. + */ + public static Map copy(final Map map) { + final Map copy = new HashMap<>(map.size()); + copy.putAll(map); + return copy; + } } diff --git a/lib/src/main/java/io/ably/lib/util/Crypto.java b/lib/src/main/java/io/ably/lib/util/Crypto.java index 0996cfc4d..b20915b02 100644 --- a/lib/src/main/java/io/ably/lib/util/Crypto.java +++ b/lib/src/main/java/io/ably/lib/util/Crypto.java @@ -36,311 +36,311 @@ */ public class Crypto { - public static final String DEFAULT_ALGORITHM = "aes"; - public static final int DEFAULT_KEYLENGTH = is256BitsSupported() ? 256 : 128; // bits - public static final int DEFAULT_BLOCKLENGTH = 16; // bytes - - /** - * A class encapsulating the client-specifiable parameters for - * the cipher. - * - * algorithm is the name of the algorithm in the default system provider, - * or the lower-cased version of it; eg "aes" or "AES". - * - * Clients may instance a CipherParams directly and populate it, or may - * query the implementation to obtain a default system CipherParams. - */ - public static class CipherParams { - private final String algorithm; - private final int keyLength; - private final SecretKeySpec keySpec; - private final IvParameterSpec ivSpec; - - CipherParams(String algorithm, byte[] key, byte[] iv) throws NoSuchAlgorithmException { - this.algorithm = (null == algorithm) ? DEFAULT_ALGORITHM : algorithm; - keyLength = key.length * 8; - keySpec = new SecretKeySpec(key, this.algorithm.toUpperCase()); - ivSpec = new IvParameterSpec(iv); - } - - /** - * Returns the length of the key in bits (e.g. 256 for a 32 byte key). - * - * This method is package scoped as it is exposed for unit testing purposes. - */ - int getKeyLength() { - return keyLength; - } - - /** - * Returns the algorithm in the case that it was supplied on construction. - * - * Package scoped for unit testing purposes. - */ - String getAlgorithm() { - return algorithm; - } - } - - /** - * Obtain a default CipherParams. This uses default algorithm, mode and - * padding and key length. A key and IV are generated using the default - * system SecureRandom; the key may be obtained from the returned CipherParams - * for out-of-band distribution to other clients. - * @return the CipherParams - */ - public static CipherParams getDefaultParams() { - return getParams(DEFAULT_ALGORITHM, DEFAULT_KEYLENGTH); - } - - /** - * Obtain a default CipherParams. This uses default algorithm, mode and - * padding and initialises a key based on the given key data. The cipher - * key length is derived from the length of the given key data. An IV is - * generated using the default system SecureRandom. - * - * Use this method of constructing CipherParams if initialising a Channel - * with a client-provided key, or to obtain a system-generated key of a - * non-default key length. - * @return the CipherParams - */ - public static CipherParams getDefaultParams(byte[] key) { - try { - return getParams(DEFAULT_ALGORITHM, key); - } catch (NoSuchAlgorithmException e) { return null; } - } - - /** - * Package scoped method for unit testing purposes. - */ - static CipherParams getDefaultParams(byte[] key, byte[] iv) throws NoSuchAlgorithmException { - return new CipherParams(DEFAULT_ALGORITHM, key, iv); - } - - /** - * Obtain a default CipherParams using Base64-encoded key. Same as above, throws - * IllegalArgumentException if base64Key is invalid - * - * @param base64Key - * @return - */ - public static CipherParams getDefaultParams(String base64Key) { - return getDefaultParams(Base64Coder.decode(base64Key)); - } - - /** - * Package scoped method for unit testing purposes. - */ - static CipherParams getDefaultParams(String base64Key, byte[] iv) throws NoSuchAlgorithmException { - return new CipherParams(null, Base64Coder.decode(base64Key), iv); - } - - public static CipherParams getParams(String algorithm, int keyLength) { - if(algorithm == null) algorithm = DEFAULT_ALGORITHM; - try { - KeyGenerator keygen = KeyGenerator.getInstance(algorithm.toUpperCase()); - keygen.init(keyLength); - byte[] key = keygen.generateKey().getEncoded(); - return getParams(algorithm, key); - } catch(NoSuchAlgorithmException e) { return null; } - - } - - public static CipherParams getParams(String algorithm, byte[] key) throws NoSuchAlgorithmException { - byte[] ivBytes = new byte[DEFAULT_BLOCKLENGTH]; - secureRandom.nextBytes(ivBytes); - return getParams(algorithm, key, ivBytes); - } - - public static CipherParams getParams(String algorithm, byte[] key, byte[] iv) throws NoSuchAlgorithmException { - return new CipherParams(algorithm, key, iv); - } - - public static byte[] generateRandomKey(int keyLength) { - byte[] result = new byte[(keyLength + 7)/8]; - secureRandom.nextBytes(result); - return result; - } - - public static byte[] generateRandomKey() { - return generateRandomKey(DEFAULT_KEYLENGTH); - } - - /** - * Interface for a ChannelCipher instance that may be associated with a Channel. - * - */ - public interface ChannelCipher { - byte[] encrypt(byte[] plaintext) throws AblyException; - byte[] decrypt(byte[] ciphertext) throws AblyException; - String getAlgorithm(); - } - - /** - * Internal; get a ChannelCipher instance based on the given ChannelOptions - * @param opts - * @return - * @throws AblyException - */ - public static ChannelCipher getCipher(final ChannelOptions opts) throws AblyException { - final Object opaqueCipherParams = opts.cipherParams; - final CipherParams cipherParams; - if(null == opaqueCipherParams) - cipherParams = Crypto.getDefaultParams(); - else if(opts.cipherParams instanceof CipherParams) - cipherParams = (CipherParams)opts.cipherParams; - else - throw AblyException.fromErrorInfo(new ErrorInfo("ChannelOptions not supported", 400, 40000)); - - return new CBCCipher(cipherParams); - } - - /** - * Internal: a class that implements a CBC mode ChannelCipher. - * A single block of secure random data is provided for an initial IV. - * Consecutive messages are chained in a manner that allows each to be - * emitted with an IV, allowing each to be deciphered independently, - * whilst avoiding having to obtain further entropy for IVs, and reinit - * the cipher, between successive messages. - * - */ - private static class CBCCipher implements ChannelCipher { - private final SecretKeySpec keySpec; - private final Cipher encryptCipher; - private final Cipher decryptCipher; - private final String algorithm; - private final int blockLength; - private byte[] iv; - - private CBCCipher(CipherParams params) throws AblyException { - final String cipherAlgorithm = params.getAlgorithm(); - String transformation = cipherAlgorithm.toUpperCase() + "/CBC/PKCS5Padding"; - try { - algorithm = cipherAlgorithm + '-' + params.getKeyLength() + "-cbc"; - keySpec = params.keySpec; - encryptCipher = Cipher.getInstance(transformation); - encryptCipher.init(Cipher.ENCRYPT_MODE, params.keySpec, params.ivSpec); - decryptCipher = Cipher.getInstance(transformation); - iv = params.ivSpec.getIV(); - blockLength = iv.length; - } - catch (NoSuchAlgorithmException|NoSuchPaddingException|InvalidAlgorithmParameterException|InvalidKeyException e) { - throw AblyException.fromThrowable(e); - } - } - - @Override - public byte[] encrypt(byte[] plaintext) { - if(plaintext == null) return null; - int plaintextLength = plaintext.length; - int paddedLength = getPaddedLength(plaintextLength); - byte[] cipherIn = new byte[paddedLength]; - byte[] ciphertext = new byte[paddedLength + blockLength]; - int padding = paddedLength - plaintextLength; - System.arraycopy(plaintext, 0, cipherIn, 0, plaintextLength); - System.arraycopy(pkcs5Padding[padding], 0, cipherIn, plaintextLength, padding); - System.arraycopy(getIv(), 0, ciphertext, 0, blockLength); - byte[] cipherOut = encryptCipher.update(cipherIn); - System.arraycopy(cipherOut, 0, ciphertext, blockLength, paddedLength); - return ciphertext; - } - - @Override - public byte[] decrypt(byte[] ciphertext) throws AblyException { - if(ciphertext == null) return null; - byte[] plaintext = null; - try { - decryptCipher.init(Cipher.DECRYPT_MODE, keySpec, new IvParameterSpec(ciphertext, 0, blockLength)); - plaintext = decryptCipher.doFinal(ciphertext, blockLength, ciphertext.length - blockLength); - } - catch (InvalidKeyException|InvalidAlgorithmParameterException|IllegalBlockSizeException|BadPaddingException e) { - Log.e(TAG, "decrypt()", e); - throw AblyException.fromThrowable(e); - } - return plaintext; - } - - @Override - public String getAlgorithm() { - return algorithm; - } - - /** - * Internal: get an IV for the next message. - * Returns either the IV that was used to initialise the ChannelCipher, - * or generates an IV based on the current cipher state. - */ - private byte[] getIv() { - if(iv == null) - return encryptCipher.update(emptyBlock); - - final byte[] result = iv; - iv = null; - return result; - } - - /** - * Internal: calculate the padded length of a given plaintext - * using PKCS5. - * @param plaintextLength - * @return - */ - private static int getPaddedLength(int plaintextLength) { - return (plaintextLength + DEFAULT_BLOCKLENGTH) & -DEFAULT_BLOCKLENGTH; - } - - /** - * Internal: a block containing zeros - */ - private static final byte[] emptyBlock = new byte[DEFAULT_BLOCKLENGTH]; - - /** - * Internal: obtain the pkcs5 padding string for a given padded length; - */ - private static final byte[][] pkcs5Padding = new byte[][] { - new byte[] {16,16,16,16,16,16,16,16,16,16,16,16,16,16,16,16}, - new byte[] {1}, - new byte[] {2,2}, - new byte[] {3,3,3}, - new byte[] {4,4,4,4}, - new byte[] {5,5,5,5,5}, - new byte[] {6,6,6,6,6,6}, - new byte[] {7,7,7,7,7,7,7}, - new byte[] {8,8,8,8,8,8,8,8}, - new byte[] {9,9,9,9,9,9,9,9,9}, - new byte[] {10,10,10,10,10,10,10,10,10,10}, - new byte[] {11,11,11,11,11,11,11,11,11,11,11}, - new byte[] {12,12,12,12,12,12,12,12,12,12,12,12}, - new byte[] {13,13,13,13,13,13,13,13,13,13,13,13,13}, - new byte[] {14,14,14,14,14,14,14,14,14,14,14,14,14,14}, - new byte[] {15,15,15,15,15,15,15,15,15,15,15,15,15,15,15}, - new byte[] {16,16,16,16,16,16,16,16,16,16,16,16,16,16,16,16} - }; - } - - public static String getRandomMessageId() { - byte[] entropy = new byte[9]; - secureRandom.nextBytes(entropy); - return Base64Coder.encode(entropy).toString(); - } - - /** - * Determine whether or not 256-bit AES is supported. (If this determines that - * it is not supported, install the JCE unlimited strength JCE extensions). - * @return - */ - private static boolean is256BitsSupported() { - try { - return Cipher.getMaxAllowedKeyLength(DEFAULT_ALGORITHM) >= 256; - } catch (NoSuchAlgorithmException e) { - return false; - } - } - - /** - * The default system SecureRandom - */ - private static final SecureRandom secureRandom = new SecureRandom(); - - private static final String TAG = Crypto.class.getName(); + public static final String DEFAULT_ALGORITHM = "aes"; + public static final int DEFAULT_KEYLENGTH = is256BitsSupported() ? 256 : 128; // bits + public static final int DEFAULT_BLOCKLENGTH = 16; // bytes + + /** + * A class encapsulating the client-specifiable parameters for + * the cipher. + * + * algorithm is the name of the algorithm in the default system provider, + * or the lower-cased version of it; eg "aes" or "AES". + * + * Clients may instance a CipherParams directly and populate it, or may + * query the implementation to obtain a default system CipherParams. + */ + public static class CipherParams { + private final String algorithm; + private final int keyLength; + private final SecretKeySpec keySpec; + private final IvParameterSpec ivSpec; + + CipherParams(String algorithm, byte[] key, byte[] iv) throws NoSuchAlgorithmException { + this.algorithm = (null == algorithm) ? DEFAULT_ALGORITHM : algorithm; + keyLength = key.length * 8; + keySpec = new SecretKeySpec(key, this.algorithm.toUpperCase()); + ivSpec = new IvParameterSpec(iv); + } + + /** + * Returns the length of the key in bits (e.g. 256 for a 32 byte key). + * + * This method is package scoped as it is exposed for unit testing purposes. + */ + int getKeyLength() { + return keyLength; + } + + /** + * Returns the algorithm in the case that it was supplied on construction. + * + * Package scoped for unit testing purposes. + */ + String getAlgorithm() { + return algorithm; + } + } + + /** + * Obtain a default CipherParams. This uses default algorithm, mode and + * padding and key length. A key and IV are generated using the default + * system SecureRandom; the key may be obtained from the returned CipherParams + * for out-of-band distribution to other clients. + * @return the CipherParams + */ + public static CipherParams getDefaultParams() { + return getParams(DEFAULT_ALGORITHM, DEFAULT_KEYLENGTH); + } + + /** + * Obtain a default CipherParams. This uses default algorithm, mode and + * padding and initialises a key based on the given key data. The cipher + * key length is derived from the length of the given key data. An IV is + * generated using the default system SecureRandom. + * + * Use this method of constructing CipherParams if initialising a Channel + * with a client-provided key, or to obtain a system-generated key of a + * non-default key length. + * @return the CipherParams + */ + public static CipherParams getDefaultParams(byte[] key) { + try { + return getParams(DEFAULT_ALGORITHM, key); + } catch (NoSuchAlgorithmException e) { return null; } + } + + /** + * Package scoped method for unit testing purposes. + */ + static CipherParams getDefaultParams(byte[] key, byte[] iv) throws NoSuchAlgorithmException { + return new CipherParams(DEFAULT_ALGORITHM, key, iv); + } + + /** + * Obtain a default CipherParams using Base64-encoded key. Same as above, throws + * IllegalArgumentException if base64Key is invalid + * + * @param base64Key + * @return + */ + public static CipherParams getDefaultParams(String base64Key) { + return getDefaultParams(Base64Coder.decode(base64Key)); + } + + /** + * Package scoped method for unit testing purposes. + */ + static CipherParams getDefaultParams(String base64Key, byte[] iv) throws NoSuchAlgorithmException { + return new CipherParams(null, Base64Coder.decode(base64Key), iv); + } + + public static CipherParams getParams(String algorithm, int keyLength) { + if(algorithm == null) algorithm = DEFAULT_ALGORITHM; + try { + KeyGenerator keygen = KeyGenerator.getInstance(algorithm.toUpperCase()); + keygen.init(keyLength); + byte[] key = keygen.generateKey().getEncoded(); + return getParams(algorithm, key); + } catch(NoSuchAlgorithmException e) { return null; } + + } + + public static CipherParams getParams(String algorithm, byte[] key) throws NoSuchAlgorithmException { + byte[] ivBytes = new byte[DEFAULT_BLOCKLENGTH]; + secureRandom.nextBytes(ivBytes); + return getParams(algorithm, key, ivBytes); + } + + public static CipherParams getParams(String algorithm, byte[] key, byte[] iv) throws NoSuchAlgorithmException { + return new CipherParams(algorithm, key, iv); + } + + public static byte[] generateRandomKey(int keyLength) { + byte[] result = new byte[(keyLength + 7)/8]; + secureRandom.nextBytes(result); + return result; + } + + public static byte[] generateRandomKey() { + return generateRandomKey(DEFAULT_KEYLENGTH); + } + + /** + * Interface for a ChannelCipher instance that may be associated with a Channel. + * + */ + public interface ChannelCipher { + byte[] encrypt(byte[] plaintext) throws AblyException; + byte[] decrypt(byte[] ciphertext) throws AblyException; + String getAlgorithm(); + } + + /** + * Internal; get a ChannelCipher instance based on the given ChannelOptions + * @param opts + * @return + * @throws AblyException + */ + public static ChannelCipher getCipher(final ChannelOptions opts) throws AblyException { + final Object opaqueCipherParams = opts.cipherParams; + final CipherParams cipherParams; + if(null == opaqueCipherParams) + cipherParams = Crypto.getDefaultParams(); + else if(opts.cipherParams instanceof CipherParams) + cipherParams = (CipherParams)opts.cipherParams; + else + throw AblyException.fromErrorInfo(new ErrorInfo("ChannelOptions not supported", 400, 40000)); + + return new CBCCipher(cipherParams); + } + + /** + * Internal: a class that implements a CBC mode ChannelCipher. + * A single block of secure random data is provided for an initial IV. + * Consecutive messages are chained in a manner that allows each to be + * emitted with an IV, allowing each to be deciphered independently, + * whilst avoiding having to obtain further entropy for IVs, and reinit + * the cipher, between successive messages. + * + */ + private static class CBCCipher implements ChannelCipher { + private final SecretKeySpec keySpec; + private final Cipher encryptCipher; + private final Cipher decryptCipher; + private final String algorithm; + private final int blockLength; + private byte[] iv; + + private CBCCipher(CipherParams params) throws AblyException { + final String cipherAlgorithm = params.getAlgorithm(); + String transformation = cipherAlgorithm.toUpperCase() + "/CBC/PKCS5Padding"; + try { + algorithm = cipherAlgorithm + '-' + params.getKeyLength() + "-cbc"; + keySpec = params.keySpec; + encryptCipher = Cipher.getInstance(transformation); + encryptCipher.init(Cipher.ENCRYPT_MODE, params.keySpec, params.ivSpec); + decryptCipher = Cipher.getInstance(transformation); + iv = params.ivSpec.getIV(); + blockLength = iv.length; + } + catch (NoSuchAlgorithmException|NoSuchPaddingException|InvalidAlgorithmParameterException|InvalidKeyException e) { + throw AblyException.fromThrowable(e); + } + } + + @Override + public byte[] encrypt(byte[] plaintext) { + if(plaintext == null) return null; + int plaintextLength = plaintext.length; + int paddedLength = getPaddedLength(plaintextLength); + byte[] cipherIn = new byte[paddedLength]; + byte[] ciphertext = new byte[paddedLength + blockLength]; + int padding = paddedLength - plaintextLength; + System.arraycopy(plaintext, 0, cipherIn, 0, plaintextLength); + System.arraycopy(pkcs5Padding[padding], 0, cipherIn, plaintextLength, padding); + System.arraycopy(getIv(), 0, ciphertext, 0, blockLength); + byte[] cipherOut = encryptCipher.update(cipherIn); + System.arraycopy(cipherOut, 0, ciphertext, blockLength, paddedLength); + return ciphertext; + } + + @Override + public byte[] decrypt(byte[] ciphertext) throws AblyException { + if(ciphertext == null) return null; + byte[] plaintext = null; + try { + decryptCipher.init(Cipher.DECRYPT_MODE, keySpec, new IvParameterSpec(ciphertext, 0, blockLength)); + plaintext = decryptCipher.doFinal(ciphertext, blockLength, ciphertext.length - blockLength); + } + catch (InvalidKeyException|InvalidAlgorithmParameterException|IllegalBlockSizeException|BadPaddingException e) { + Log.e(TAG, "decrypt()", e); + throw AblyException.fromThrowable(e); + } + return plaintext; + } + + @Override + public String getAlgorithm() { + return algorithm; + } + + /** + * Internal: get an IV for the next message. + * Returns either the IV that was used to initialise the ChannelCipher, + * or generates an IV based on the current cipher state. + */ + private byte[] getIv() { + if(iv == null) + return encryptCipher.update(emptyBlock); + + final byte[] result = iv; + iv = null; + return result; + } + + /** + * Internal: calculate the padded length of a given plaintext + * using PKCS5. + * @param plaintextLength + * @return + */ + private static int getPaddedLength(int plaintextLength) { + return (plaintextLength + DEFAULT_BLOCKLENGTH) & -DEFAULT_BLOCKLENGTH; + } + + /** + * Internal: a block containing zeros + */ + private static final byte[] emptyBlock = new byte[DEFAULT_BLOCKLENGTH]; + + /** + * Internal: obtain the pkcs5 padding string for a given padded length; + */ + private static final byte[][] pkcs5Padding = new byte[][] { + new byte[] {16,16,16,16,16,16,16,16,16,16,16,16,16,16,16,16}, + new byte[] {1}, + new byte[] {2,2}, + new byte[] {3,3,3}, + new byte[] {4,4,4,4}, + new byte[] {5,5,5,5,5}, + new byte[] {6,6,6,6,6,6}, + new byte[] {7,7,7,7,7,7,7}, + new byte[] {8,8,8,8,8,8,8,8}, + new byte[] {9,9,9,9,9,9,9,9,9}, + new byte[] {10,10,10,10,10,10,10,10,10,10}, + new byte[] {11,11,11,11,11,11,11,11,11,11,11}, + new byte[] {12,12,12,12,12,12,12,12,12,12,12,12}, + new byte[] {13,13,13,13,13,13,13,13,13,13,13,13,13}, + new byte[] {14,14,14,14,14,14,14,14,14,14,14,14,14,14}, + new byte[] {15,15,15,15,15,15,15,15,15,15,15,15,15,15,15}, + new byte[] {16,16,16,16,16,16,16,16,16,16,16,16,16,16,16,16} + }; + } + + public static String getRandomMessageId() { + byte[] entropy = new byte[9]; + secureRandom.nextBytes(entropy); + return Base64Coder.encode(entropy).toString(); + } + + /** + * Determine whether or not 256-bit AES is supported. (If this determines that + * it is not supported, install the JCE unlimited strength JCE extensions). + * @return + */ + private static boolean is256BitsSupported() { + try { + return Cipher.getMaxAllowedKeyLength(DEFAULT_ALGORITHM) >= 256; + } catch (NoSuchAlgorithmException e) { + return false; + } + } + + /** + * The default system SecureRandom + */ + private static final SecureRandom secureRandom = new SecureRandom(); + + private static final String TAG = Crypto.class.getName(); } diff --git a/lib/src/main/java/io/ably/lib/util/CurrentThreadExecutor.java b/lib/src/main/java/io/ably/lib/util/CurrentThreadExecutor.java index e49da94b8..26ecb2a31 100644 --- a/lib/src/main/java/io/ably/lib/util/CurrentThreadExecutor.java +++ b/lib/src/main/java/io/ably/lib/util/CurrentThreadExecutor.java @@ -3,10 +3,10 @@ import java.util.concurrent.Executor; public class CurrentThreadExecutor implements Executor { - public static CurrentThreadExecutor INSTANCE = new CurrentThreadExecutor(); + public static CurrentThreadExecutor INSTANCE = new CurrentThreadExecutor(); - @Override - public void execute(Runnable runnable) { - runnable.run(); - } + @Override + public void execute(Runnable runnable) { + runnable.run(); + } } diff --git a/lib/src/main/java/io/ably/lib/util/EventEmitter.java b/lib/src/main/java/io/ably/lib/util/EventEmitter.java index bc5fe78cc..7e277e495 100644 --- a/lib/src/main/java/io/ably/lib/util/EventEmitter.java +++ b/lib/src/main/java/io/ably/lib/util/EventEmitter.java @@ -15,109 +15,109 @@ */ public abstract class EventEmitter { - /** - * Remove all registered listeners irrespective of type - */ - public synchronized void off() { - listeners.clear(); - filters.clear(); - } + /** + * Remove all registered listeners irrespective of type + */ + public synchronized void off() { + listeners.clear(); + filters.clear(); + } - /** - * Register the given listener for all events - * @param listener - */ - public synchronized void on(Listener listener) { - if(!listeners.contains(listener)) - listeners.add(listener); - } + /** + * Register the given listener for all events + * @param listener + */ + public synchronized void on(Listener listener) { + if(!listeners.contains(listener)) + listeners.add(listener); + } - /** - * Register the given listener for a single occurrence of any event - * @param listener - */ - public synchronized void once(Listener listener) { - filters.put(listener, new Filter(null, listener, true)); - } + /** + * Register the given listener for a single occurrence of any event + * @param listener + */ + public synchronized void once(Listener listener) { + filters.put(listener, new Filter(null, listener, true)); + } - /** - * Remove a previously registered listener irrespective of type - * @param listener - */ - public synchronized void off(Listener listener) { - listeners.remove(listener); - filters.remove(listener); - } + /** + * Remove a previously registered listener irrespective of type + * @param listener + */ + public synchronized void off(Listener listener) { + listeners.remove(listener); + filters.remove(listener); + } - /** - * Register the given listener for a specific event - * @param listener - */ - public synchronized void on(Event event, Listener listener) { - filters.put(listener, new Filter(event, listener, false)); - } + /** + * Register the given listener for a specific event + * @param listener + */ + public synchronized void on(Event event, Listener listener) { + filters.put(listener, new Filter(event, listener, false)); + } - /** - * Register the given listener for a single occurrence of a specific event - * @param listener - */ - public synchronized void once(Event event, Listener listener) { - filters.put(listener, new Filter(event, listener, true)); - } + /** + * Register the given listener for a single occurrence of a specific event + * @param listener + */ + public synchronized void once(Event event, Listener listener) { + filters.put(listener, new Filter(event, listener, true)); + } - /** - * Remove a previously registered event-specific listener - * @param listener - * @param event - */ - public synchronized void off(Event event, Listener listener) { - Filter filter = filters.get(listener); - if(filter != null && filter.event == event) - filters.remove(listener); - } + /** + * Remove a previously registered event-specific listener + * @param listener + * @param event + */ + public synchronized void off(Event event, Listener listener) { + Filter filter = filters.get(listener); + if(filter != null && filter.event == event) + filters.remove(listener); + } - /** - * Emit the given event (broadcasting to registered listeners) - * @param event the Event - * @param args the arguments to pass to listeners - */ - public synchronized void emit(Event event, Object... args) { - /* - * The set of listeners called by emit must not change over the course of the emit - * Refer RTE6a part of Spec for more details. - * To address this issue, we clone the listeners before calling emit. - */ - List clonedListeners = new ArrayList<>(listeners); + /** + * Emit the given event (broadcasting to registered listeners) + * @param event the Event + * @param args the arguments to pass to listeners + */ + public synchronized void emit(Event event, Object... args) { + /* + * The set of listeners called by emit must not change over the course of the emit + * Refer RTE6a part of Spec for more details. + * To address this issue, we clone the listeners before calling emit. + */ + List clonedListeners = new ArrayList<>(listeners); - for (Listener listener : clonedListeners) { - apply(listener, event, args); - } + for (Listener listener : clonedListeners) { + apply(listener, event, args); + } - Map clonedFilters = new HashMap<>(filters); - for (Iterator> it = clonedFilters.entrySet().iterator(); it.hasNext(); ) { - Map.Entry entry = it.next(); - if (entry.getValue().apply(event, args)) { - filters.remove(entry.getKey()); - } - } - } + Map clonedFilters = new HashMap<>(filters); + for (Iterator> it = clonedFilters.entrySet().iterator(); it.hasNext(); ) { + Map.Entry entry = it.next(); + if (entry.getValue().apply(event, args)) { + filters.remove(entry.getKey()); + } + } + } - protected abstract void apply(Listener listener, Event event, Object... args); + protected abstract void apply(Listener listener, Event event, Object... args); - protected class Filter { - Filter(Event event, Listener listener, boolean once) { this.event = event; this.listener = listener; this.once = once; } - private Event event; - private Listener listener; - private boolean once; - protected boolean apply(Event event, Object... args) { - if(this.event == event || this.event == null) { - EventEmitter.this.apply(listener, event, args); - return once; - } - return false; - } - } + protected class Filter { + Filter(Event event, Listener listener, boolean once) { this.event = event; this.listener = listener; this.once = once; } + private Event event; + private Listener listener; + private boolean once; + protected boolean apply(Event event, Object... args) { + if(this.event == event || this.event == null) { + EventEmitter.this.apply(listener, event, args); + return once; + } + return false; + } + } - Map filters = new HashMap(); - List listeners = new ArrayList(); + Map filters = new HashMap(); + List listeners = new ArrayList(); } diff --git a/lib/src/main/java/io/ably/lib/util/JsonUtils.java b/lib/src/main/java/io/ably/lib/util/JsonUtils.java index 545a68512..5f028ecb4 100644 --- a/lib/src/main/java/io/ably/lib/util/JsonUtils.java +++ b/lib/src/main/java/io/ably/lib/util/JsonUtils.java @@ -4,38 +4,38 @@ import com.google.gson.JsonObject; public class JsonUtils { - public static JsonUtilsObject object() { - return new JsonUtilsObject(); - } + public static JsonUtilsObject object() { + return new JsonUtilsObject(); + } - public static class JsonUtilsObject { - private final JsonObject json; + public static class JsonUtilsObject { + private final JsonObject json; - JsonUtilsObject() { - json = new JsonObject(); - } + JsonUtilsObject() { + json = new JsonObject(); + } - public JsonUtilsObject add(String key, Object value) { - if (value == null) { - json.add(key, null); - } else if (value instanceof JsonElement) { - json.add(key, (JsonElement) value); - } else if (value instanceof String) { - json.addProperty(key, (String) value); - } else if (value instanceof Boolean) { - json.addProperty(key, (Boolean) value); - } else if (value instanceof Character) { - json.addProperty(key, (Character) value); - } else if (value instanceof Number) { - json.addProperty(key, (Number) value); - } else if (value instanceof JsonUtilsObject) { - json.add(key, ((JsonUtilsObject) value).toJson()); - } - return this; - } + public JsonUtilsObject add(String key, Object value) { + if (value == null) { + json.add(key, null); + } else if (value instanceof JsonElement) { + json.add(key, (JsonElement) value); + } else if (value instanceof String) { + json.addProperty(key, (String) value); + } else if (value instanceof Boolean) { + json.addProperty(key, (Boolean) value); + } else if (value instanceof Character) { + json.addProperty(key, (Character) value); + } else if (value instanceof Number) { + json.addProperty(key, (Number) value); + } else if (value instanceof JsonUtilsObject) { + json.add(key, ((JsonUtilsObject) value).toJson()); + } + return this; + } - public JsonObject toJson() { - return json; - } - } + public JsonObject toJson() { + return json; + } + } } diff --git a/lib/src/main/java/io/ably/lib/util/Log.java b/lib/src/main/java/io/ably/lib/util/Log.java index 1db4e9d15..ecd673ed2 100644 --- a/lib/src/main/java/io/ably/lib/util/Log.java +++ b/lib/src/main/java/io/ably/lib/util/Log.java @@ -20,138 +20,138 @@ public class Log { - public interface LogHandler { - void println(int severity, String tag, String msg, Throwable tr); - } - - /** - * Default log handler class that sends output to System.out. - * This is public as a convenience to allow simple subclasses - * to output to other PrintStrems. - */ - public static class DefaultHandler implements LogHandler { - @Override - public void println(int severity, String tag, String msg, Throwable tr) { - println(System.out, severity, tag, msg, tr); - } - - protected void println(PrintStream stream, int severity, String tag, String msg, Throwable tr) { - stream.print("(" + severities[severity] + "): "); - if (tag != null && tag.length() != 0) - stream.print(tag + ": "); - if (msg != null && msg.length() != 0) - stream.print(msg); - stream.println(); - if (tr != null) { - tr.printStackTrace(stream); - } - } - } - - /** - * Priority constant to suppress all logging. - */ - public static final int NONE = 99; - - /** - * Priority constant; use Log.v. - */ - public static final int VERBOSE = 2; - - /** - * Priority constant; use Log.d. - */ - public static final int DEBUG = 3; - - /** - * Priority constant; use Log.i. - */ - public static final int INFO = 4; - - /** - * Priority constant; use Log.w. - */ - public static final int WARN = 5; - - /** - * Priority constant; use Log.e. - */ - public static final int ERROR = 6; - - public static int v(String tag, String msg) { - print(VERBOSE, tag, msg, null); - return 0; - } - - public static int v(String tag, String msg, Throwable tr) { - print(VERBOSE, tag, msg, tr); - return 0; - } - - public static int d(String tag, String msg) { - print(DEBUG, tag, msg, null); - return 0; - } - - public static int d(String tag, String msg, Throwable tr) { - print(DEBUG, tag, msg, tr); - return 0; - } - - public static int i(String tag, String msg) { - print(INFO, tag, msg, null); - return 0; - } - - public static int i(String tag, String msg, Throwable tr) { - print(INFO, tag, msg, tr); - return 0; - } - - public static int w(String tag, String msg) { - print(WARN, tag, msg, null); - return 0; - } - - public static int w(String tag, String msg, Throwable tr) { - print(WARN, tag, msg, tr); - return 0; - } - - public static int w(String tag, Throwable tr) { - print(WARN, tag, null, tr); - return 0; - } - - public static int e(String tag, String msg) { - print(ERROR, tag, msg, null); - return 0; - } - - public static int e(String tag, String msg, Throwable tr) { - print(ERROR, tag, msg, tr); - return 0; - } - - public static void setLevel(int level) { - Log.level = (level != 0) ? level : defaultLevel; - } - - public static final int defaultLevel = WARN; - public static int level = defaultLevel; - - public static void setHandler(LogHandler handler) { - Log.handler = (handler != null) ? handler : defaultHandler; - } - - public static final LogHandler defaultHandler = new DefaultHandler(); - public static LogHandler handler = defaultHandler; - - private static String[] severities = new String[]{"", "", "VERBOSE", "DEBUG", "INFO", "WARN", "ERROR", "ASSERT"}; - - private static void print(int severity, String tag, String msg, Throwable tr) { - if (severity >= level) { - handler.println(severity, tag, msg, tr); - } - } + public interface LogHandler { + void println(int severity, String tag, String msg, Throwable tr); + } + + /** + * Default log handler class that sends output to System.out. + * This is public as a convenience to allow simple subclasses + * to output to other PrintStrems. + */ + public static class DefaultHandler implements LogHandler { + @Override + public void println(int severity, String tag, String msg, Throwable tr) { + println(System.out, severity, tag, msg, tr); + } + + protected void println(PrintStream stream, int severity, String tag, String msg, Throwable tr) { + stream.print("(" + severities[severity] + "): "); + if (tag != null && tag.length() != 0) + stream.print(tag + ": "); + if (msg != null && msg.length() != 0) + stream.print(msg); + stream.println(); + if (tr != null) { + tr.printStackTrace(stream); + } + } + } + + /** + * Priority constant to suppress all logging. + */ + public static final int NONE = 99; + + /** + * Priority constant; use Log.v. + */ + public static final int VERBOSE = 2; + + /** + * Priority constant; use Log.d. + */ + public static final int DEBUG = 3; + + /** + * Priority constant; use Log.i. + */ + public static final int INFO = 4; + + /** + * Priority constant; use Log.w. + */ + public static final int WARN = 5; + + /** + * Priority constant; use Log.e. + */ + public static final int ERROR = 6; + + public static int v(String tag, String msg) { + print(VERBOSE, tag, msg, null); + return 0; + } + + public static int v(String tag, String msg, Throwable tr) { + print(VERBOSE, tag, msg, tr); + return 0; + } + + public static int d(String tag, String msg) { + print(DEBUG, tag, msg, null); + return 0; + } + + public static int d(String tag, String msg, Throwable tr) { + print(DEBUG, tag, msg, tr); + return 0; + } + + public static int i(String tag, String msg) { + print(INFO, tag, msg, null); + return 0; + } + + public static int i(String tag, String msg, Throwable tr) { + print(INFO, tag, msg, tr); + return 0; + } + + public static int w(String tag, String msg) { + print(WARN, tag, msg, null); + return 0; + } + + public static int w(String tag, String msg, Throwable tr) { + print(WARN, tag, msg, tr); + return 0; + } + + public static int w(String tag, Throwable tr) { + print(WARN, tag, null, tr); + return 0; + } + + public static int e(String tag, String msg) { + print(ERROR, tag, msg, null); + return 0; + } + + public static int e(String tag, String msg, Throwable tr) { + print(ERROR, tag, msg, tr); + return 0; + } + + public static void setLevel(int level) { + Log.level = (level != 0) ? level : defaultLevel; + } + + public static final int defaultLevel = WARN; + public static int level = defaultLevel; + + public static void setHandler(LogHandler handler) { + Log.handler = (handler != null) ? handler : defaultHandler; + } + + public static final LogHandler defaultHandler = new DefaultHandler(); + public static LogHandler handler = defaultHandler; + + private static String[] severities = new String[]{"", "", "VERBOSE", "DEBUG", "INFO", "WARN", "ERROR", "ASSERT"}; + + private static void print(int severity, String tag, String msg, Throwable tr) { + if (severity >= level) { + handler.println(severity, tag, msg, tr); + } + } } diff --git a/lib/src/main/java/io/ably/lib/util/Multicaster.java b/lib/src/main/java/io/ably/lib/util/Multicaster.java index 948b60cca..be9fb0464 100644 --- a/lib/src/main/java/io/ably/lib/util/Multicaster.java +++ b/lib/src/main/java/io/ably/lib/util/Multicaster.java @@ -6,14 +6,14 @@ public abstract class Multicaster { - protected final List members = new ArrayList(); + protected final List members = new ArrayList(); - public Multicaster(T... members) { for(T m : members) this.members.add(m); } - - public void add(T member) { members.add(member); } - public void remove(T member) { members.remove(member); } - public void clear() { members.clear(); } - public boolean isEmpty() { return members.isEmpty(); } - public int size() { return members.size(); } - public Iterator iterator() { return members.iterator(); } + public Multicaster(T... members) { for(T m : members) this.members.add(m); } + + public void add(T member) { members.add(member); } + public void remove(T member) { members.remove(member); } + public void clear() { members.clear(); } + public boolean isEmpty() { return members.isEmpty(); } + public int size() { return members.size(); } + public Iterator iterator() { return members.iterator(); } } diff --git a/lib/src/main/java/io/ably/lib/util/Serialisation.java b/lib/src/main/java/io/ably/lib/util/Serialisation.java index de832e5c6..feeeb188f 100644 --- a/lib/src/main/java/io/ably/lib/util/Serialisation.java +++ b/lib/src/main/java/io/ably/lib/util/Serialisation.java @@ -34,257 +34,257 @@ import java.util.Set; public class Serialisation { - public static final JsonParser gsonParser; - public static final GsonBuilder gsonBuilder; - public static final Gson gson; + public static final JsonParser gsonParser; + public static final GsonBuilder gsonBuilder; + public static final Gson gson; - public static final PackerConfig msgpackPackerConfig; - public static final UnpackerConfig msgpackUnpackerConfig; + public static final PackerConfig msgpackPackerConfig; + public static final UnpackerConfig msgpackUnpackerConfig; - static { - gsonParser = new JsonParser(); - gsonBuilder = new GsonBuilder(); - gsonBuilder.registerTypeAdapter(Message.class, new Message.Serializer()); - gsonBuilder.registerTypeAdapter(MessageExtras.class, new MessageExtras.Serializer()); - gsonBuilder.registerTypeAdapter(PresenceMessage.class, new PresenceMessage.Serializer()); - gsonBuilder.registerTypeAdapter(PresenceMessage.Action.class, new PresenceMessage.ActionSerializer()); - gsonBuilder.registerTypeAdapter(ProtocolMessage.Action.class, new ProtocolMessage.ActionSerializer()); - gson = gsonBuilder.create(); + static { + gsonParser = new JsonParser(); + gsonBuilder = new GsonBuilder(); + gsonBuilder.registerTypeAdapter(Message.class, new Message.Serializer()); + gsonBuilder.registerTypeAdapter(MessageExtras.class, new MessageExtras.Serializer()); + gsonBuilder.registerTypeAdapter(PresenceMessage.class, new PresenceMessage.Serializer()); + gsonBuilder.registerTypeAdapter(PresenceMessage.Action.class, new PresenceMessage.ActionSerializer()); + gsonBuilder.registerTypeAdapter(ProtocolMessage.Action.class, new ProtocolMessage.ActionSerializer()); + gson = gsonBuilder.create(); - msgpackPackerConfig = Platform.name.equals("android") ? - new PackerConfig().withSmallStringOptimizationThreshold(Integer.MAX_VALUE) : - MessagePack.DEFAULT_PACKER_CONFIG; + msgpackPackerConfig = Platform.name.equals("android") ? + new PackerConfig().withSmallStringOptimizationThreshold(Integer.MAX_VALUE) : + MessagePack.DEFAULT_PACKER_CONFIG; - msgpackUnpackerConfig = MessagePack.DEFAULT_UNPACKER_CONFIG; - } + msgpackUnpackerConfig = MessagePack.DEFAULT_UNPACKER_CONFIG; + } - public static byte[] gsonToMsgpack(JsonElement json) { - try { - ByteArrayOutputStream out = new ByteArrayOutputStream(); - MessagePacker packer = msgpackPackerConfig.newPacker(out); - gsonToMsgpack(json, packer); - packer.flush(); - return out.toByteArray(); - } catch(IOException e) { return null; } - } + public static byte[] gsonToMsgpack(JsonElement json) { + try { + ByteArrayOutputStream out = new ByteArrayOutputStream(); + MessagePacker packer = msgpackPackerConfig.newPacker(out); + gsonToMsgpack(json, packer); + packer.flush(); + return out.toByteArray(); + } catch(IOException e) { return null; } + } - public static JsonElement msgpackToGson(byte[] bytes) { - MessageUnpacker unpacker = msgpackUnpackerConfig.newUnpacker(bytes); - try { - return msgpackToGson(unpacker.unpackValue()); - } catch (IOException e) { - return null; - } - } + public static JsonElement msgpackToGson(byte[] bytes) { + MessageUnpacker unpacker = msgpackUnpackerConfig.newUnpacker(bytes); + try { + return msgpackToGson(unpacker.unpackValue()); + } catch (IOException e) { + return null; + } + } - public interface FromJsonElement { - T fromJsonElement(JsonElement e); - } + public interface FromJsonElement { + T fromJsonElement(JsonElement e); + } - public static class HttpResponseHandler implements HttpCore.ResponseHandler { - private final Class klass; - private final FromJsonElement converter; + public static class HttpResponseHandler implements HttpCore.ResponseHandler { + private final Class klass; + private final FromJsonElement converter; - public HttpResponseHandler(Class klass, FromJsonElement converter) { - this.klass = klass; - this.converter = converter; - } + public HttpResponseHandler(Class klass, FromJsonElement converter) { + this.klass = klass; + this.converter = converter; + } - /** - * If the target type extends JsonElement, we don't need to convert JsonElements to - * it. We can just force-cast. - */ - public HttpResponseHandler() { - this(null, null); - } + /** + * If the target type extends JsonElement, we don't need to convert JsonElements to + * it. We can just force-cast. + */ + public HttpResponseHandler() { + this(null, null); + } - @Override - public T handleResponse(HttpCore.Response response, ErrorInfo error) throws AblyException { - if (error != null) { - throw AblyException.fromErrorInfo(error); - } - if ("application/json".equals(response.contentType)) { - if (klass != null) { - return jsonBytesToGson(response.body, klass); - } - // only if target type extends JsonElement - return (T) jsonBytesToGson(response.body); - } else if("application/x-msgpack".equals(response.contentType)) { - if (converter != null) { - return converter.fromJsonElement(msgpackToGson(response.body)); - } - // only if target type extends JsonElement - return (T) msgpackToGson(response.body); - } else { - throw AblyException.fromThrowable(new Exception("unknown content type " + response.contentType)); - } - } - } + @Override + public T handleResponse(HttpCore.Response response, ErrorInfo error) throws AblyException { + if (error != null) { + throw AblyException.fromErrorInfo(error); + } + if ("application/json".equals(response.contentType)) { + if (klass != null) { + return jsonBytesToGson(response.body, klass); + } + // only if target type extends JsonElement + return (T) jsonBytesToGson(response.body); + } else if("application/x-msgpack".equals(response.contentType)) { + if (converter != null) { + return converter.fromJsonElement(msgpackToGson(response.body)); + } + // only if target type extends JsonElement + return (T) msgpackToGson(response.body); + } else { + throw AblyException.fromThrowable(new Exception("unknown content type " + response.contentType)); + } + } + } - public static HttpResponseHandler httpResponseHandler = new HttpResponseHandler(); + public static HttpResponseHandler httpResponseHandler = new HttpResponseHandler(); - public static class HttpBodyHandler implements HttpCore.BodyHandler { - private final Class klass; - private final FromJsonElement converter; + public static class HttpBodyHandler implements HttpCore.BodyHandler { + private final Class klass; + private final FromJsonElement converter; - public HttpBodyHandler(Class klass, FromJsonElement converter) { - this.klass = klass; - this.converter = converter; - } + public HttpBodyHandler(Class klass, FromJsonElement converter) { + this.klass = klass; + this.converter = converter; + } - /** - * If the target type extends JsonElement, we don't need to convert JsonElements to - * it. We can just force-cast. - */ - public HttpBodyHandler() { - this(null, null); - } + /** + * If the target type extends JsonElement, we don't need to convert JsonElements to + * it. We can just force-cast. + */ + public HttpBodyHandler() { + this(null, null); + } - @Override - public T[] handleResponseBody(String contentType, byte[] body) throws AblyException { - JsonArray jsonArray; - if ("application/json".equals(contentType)) { - if (klass != null) { - return jsonBytesToGson(body, klass); - } - // only if target type extends JsonElement - jsonArray = jsonBytesToGson(body, JsonArray.class); - } else if("application/x-msgpack".equals(contentType)) { - jsonArray = (JsonArray)msgpackToGson(body); - } else { - throw AblyException.fromThrowable(new Exception("unknown content type " + contentType)); - } + @Override + public T[] handleResponseBody(String contentType, byte[] body) throws AblyException { + JsonArray jsonArray; + if ("application/json".equals(contentType)) { + if (klass != null) { + return jsonBytesToGson(body, klass); + } + // only if target type extends JsonElement + jsonArray = jsonBytesToGson(body, JsonArray.class); + } else if("application/x-msgpack".equals(contentType)) { + jsonArray = (JsonArray)msgpackToGson(body); + } else { + throw AblyException.fromThrowable(new Exception("unknown content type " + contentType)); + } - T[] array = klass.cast(Array.newInstance(klass.getComponentType(), jsonArray.size())); - int i = 0; - for (JsonElement elem : jsonArray) { - if (converter != null) { - array[i] = converter.fromJsonElement(elem); - } else { - // only if target type extends JsonElement - array[i] = (T) elem; - } - i++; - } + T[] array = klass.cast(Array.newInstance(klass.getComponentType(), jsonArray.size())); + int i = 0; + for (JsonElement elem : jsonArray) { + if (converter != null) { + array[i] = converter.fromJsonElement(elem); + } else { + // only if target type extends JsonElement + array[i] = (T) elem; + } + i++; + } - return array; - } - } + return array; + } + } - public static HttpBodyHandler httpBodyHandler = new HttpBodyHandler(); + public static HttpBodyHandler httpBodyHandler = new HttpBodyHandler(); - public static JsonElement jsonBytesToGson(byte[] bytes) { - return jsonBytesToGson(bytes, JsonElement.class); - } + public static JsonElement jsonBytesToGson(byte[] bytes) { + return jsonBytesToGson(bytes, JsonElement.class); + } - public static T jsonBytesToGson(byte[] bytes, Class klass) { - try { - return gson.fromJson(new String(bytes, "UTF-8"), klass); - } catch (UnsupportedEncodingException e) { - return null; - } - } + public static T jsonBytesToGson(byte[] bytes, Class klass) { + try { + return gson.fromJson(new String(bytes, "UTF-8"), klass); + } catch (UnsupportedEncodingException e) { + return null; + } + } - public static void gsonToMsgpack(JsonElement json, MessagePacker packer) { - if (json.isJsonArray()) { - gsonToMsgpack((JsonArray)json, packer); - } else if (json.isJsonObject()) { - gsonToMsgpack((JsonObject)json, packer); - } else if (json.isJsonNull()) { - gsonToMsgpack((JsonNull)json, packer); - } else if (json.isJsonPrimitive()) { - gsonToMsgpack((JsonPrimitive)json, packer); - } else { - throw new RuntimeException("unreachable"); - } - } + public static void gsonToMsgpack(JsonElement json, MessagePacker packer) { + if (json.isJsonArray()) { + gsonToMsgpack((JsonArray)json, packer); + } else if (json.isJsonObject()) { + gsonToMsgpack((JsonObject)json, packer); + } else if (json.isJsonNull()) { + gsonToMsgpack((JsonNull)json, packer); + } else if (json.isJsonPrimitive()) { + gsonToMsgpack((JsonPrimitive)json, packer); + } else { + throw new RuntimeException("unreachable"); + } + } - private static void gsonToMsgpack(JsonArray array, MessagePacker packer) { - try { - packer.packArrayHeader(array.size()); - for (JsonElement elem : array) { - gsonToMsgpack(elem, packer); - } - } catch(IOException e) {} - } + private static void gsonToMsgpack(JsonArray array, MessagePacker packer) { + try { + packer.packArrayHeader(array.size()); + for (JsonElement elem : array) { + gsonToMsgpack(elem, packer); + } + } catch(IOException e) {} + } - private static void gsonToMsgpack(JsonObject object, MessagePacker packer) { - try { - Set> entries = object.entrySet(); - packer.packMapHeader(entries.size()); - for (Map.Entry entry : entries) { - packer.packString(entry.getKey()); - gsonToMsgpack(entry.getValue(), packer); - } - } catch(IOException e) {} - } + private static void gsonToMsgpack(JsonObject object, MessagePacker packer) { + try { + Set> entries = object.entrySet(); + packer.packMapHeader(entries.size()); + for (Map.Entry entry : entries) { + packer.packString(entry.getKey()); + gsonToMsgpack(entry.getValue(), packer); + } + } catch(IOException e) {} + } - private static void gsonToMsgpack(JsonNull n, MessagePacker packer) { - try { - packer.packNil(); - } catch(IOException e) {} - } + private static void gsonToMsgpack(JsonNull n, MessagePacker packer) { + try { + packer.packNil(); + } catch(IOException e) {} + } - private static void gsonToMsgpack(JsonPrimitive primitive, MessagePacker packer) { - try { - if (primitive.isBoolean()) { - packer.packBoolean(primitive.getAsBoolean()); - } else if (primitive.isNumber()) { - Number number = primitive.getAsNumber(); - if (number instanceof BigDecimal || number instanceof Double) { - packer.packDouble(number.doubleValue()); - } else if (number instanceof Float) { - packer.packFloat(number.floatValue()); - } else if (number instanceof BigInteger || number instanceof Long) { - packer.packLong(number.longValue()); - } else if (number instanceof Integer) { - packer.packInt(number.intValue()); - } else if (number instanceof Short) { - packer.packShort(number.shortValue()); - } else if (number instanceof Byte) { - packer.packByte(number.byteValue()); - } else { - packer.packString(primitive.getAsString()); - } - } else { - packer.packString(primitive.getAsString()); - } - } catch(IOException e) {} - } + private static void gsonToMsgpack(JsonPrimitive primitive, MessagePacker packer) { + try { + if (primitive.isBoolean()) { + packer.packBoolean(primitive.getAsBoolean()); + } else if (primitive.isNumber()) { + Number number = primitive.getAsNumber(); + if (number instanceof BigDecimal || number instanceof Double) { + packer.packDouble(number.doubleValue()); + } else if (number instanceof Float) { + packer.packFloat(number.floatValue()); + } else if (number instanceof BigInteger || number instanceof Long) { + packer.packLong(number.longValue()); + } else if (number instanceof Integer) { + packer.packInt(number.intValue()); + } else if (number instanceof Short) { + packer.packShort(number.shortValue()); + } else if (number instanceof Byte) { + packer.packByte(number.byteValue()); + } else { + packer.packString(primitive.getAsString()); + } + } else { + packer.packString(primitive.getAsString()); + } + } catch(IOException e) {} + } - public static JsonElement msgpackToGson(Value value) { - switch (value.getValueType()) { - case NIL: - return JsonNull.INSTANCE; - case BOOLEAN: - return new JsonPrimitive(value.asBooleanValue().getBoolean()); - case INTEGER: - return new JsonPrimitive(value.asIntegerValue().asLong()); - case FLOAT: - return new JsonPrimitive(value.asFloatValue().toDouble()); - case STRING: - return new JsonPrimitive(value.asStringValue().asString()); - case BINARY: - return new JsonPrimitive(Base64Coder.encodeToString(value.asBinaryValue().asByteArray())); - case ARRAY: - JsonArray array = new JsonArray(); - for (Value element : value.asArrayValue()) { - array.add(msgpackToGson(element)); - } - return array; - case MAP: - JsonObject object = new JsonObject(); - for (Map.Entry entry : value.asMapValue().entrySet()) { - object.add( - entry.getKey().asStringValue().asString(), - msgpackToGson(entry.getValue()) - ); - } - return object; - case EXTENSION: - return null; - default: - return null; - } - } + public static JsonElement msgpackToGson(Value value) { + switch (value.getValueType()) { + case NIL: + return JsonNull.INSTANCE; + case BOOLEAN: + return new JsonPrimitive(value.asBooleanValue().getBoolean()); + case INTEGER: + return new JsonPrimitive(value.asIntegerValue().asLong()); + case FLOAT: + return new JsonPrimitive(value.asFloatValue().toDouble()); + case STRING: + return new JsonPrimitive(value.asStringValue().asString()); + case BINARY: + return new JsonPrimitive(Base64Coder.encodeToString(value.asBinaryValue().asByteArray())); + case ARRAY: + JsonArray array = new JsonArray(); + for (Value element : value.asArrayValue()) { + array.add(msgpackToGson(element)); + } + return array; + case MAP: + JsonObject object = new JsonObject(); + for (Map.Entry entry : value.asMapValue().entrySet()) { + object.add( + entry.getKey().asStringValue().asString(), + msgpackToGson(entry.getValue()) + ); + } + return object; + case EXTENSION: + return null; + default: + return null; + } + } } diff --git a/lib/src/main/java/io/ably/lib/util/StringUtils.java b/lib/src/main/java/io/ably/lib/util/StringUtils.java index 1ddf8bfd7..97f876b4a 100644 --- a/lib/src/main/java/io/ably/lib/util/StringUtils.java +++ b/lib/src/main/java/io/ably/lib/util/StringUtils.java @@ -4,14 +4,14 @@ import io.ably.lib.http.HttpCore; public class StringUtils { - public static Serialisation.FromJsonElement fromJsonElement = new Serialisation.FromJsonElement() { - @Override - public String fromJsonElement(JsonElement e) { - return e.getAsJsonPrimitive().getAsString(); - } - }; + public static Serialisation.FromJsonElement fromJsonElement = new Serialisation.FromJsonElement() { + @Override + public String fromJsonElement(JsonElement e) { + return e.getAsJsonPrimitive().getAsString(); + } + }; - public static HttpCore.ResponseHandler httpResponseHandler = new Serialisation.HttpResponseHandler(String.class, fromJsonElement); + public static HttpCore.ResponseHandler httpResponseHandler = new Serialisation.HttpResponseHandler(String.class, fromJsonElement); - public static HttpCore.BodyHandler httpBodyHandler = new Serialisation.HttpBodyHandler(String[].class, fromJsonElement); + public static HttpCore.BodyHandler httpBodyHandler = new Serialisation.HttpBodyHandler(String[].class, fromJsonElement); } diff --git a/lib/src/test/java/io/ably/lib/test/common/Helpers.java b/lib/src/test/java/io/ably/lib/test/common/Helpers.java index 518387556..8eb1853de 100644 --- a/lib/src/test/java/io/ably/lib/test/common/Helpers.java +++ b/lib/src/test/java/io/ably/lib/test/common/Helpers.java @@ -45,955 +45,955 @@ public class Helpers { - public static void assertArrayUnorderedEquals(T[] expected, T[] got) { - Set expectedSet = new CopyOnWriteArraySet(Arrays.asList(expected)); - Set gotSet = new CopyOnWriteArraySet(Arrays.asList(got)); - assertEquals(expectedSet, gotSet); - } - - public static T expectedError(AblyFunction f, String expectedError) { - return expectedError(f, expectedError, 0); - } - - public static T expectedError(AblyFunction f, String expectedError, int expectedStatusCode) { - return expectedError(f, expectedError, expectedStatusCode, 0); - } - - public static T expectedError(AblyFunction f, String expectedError, int expectedStatusCode, int expectedCode) { - try { - T result = f.apply(null); - assertEquals(null, expectedError); - return result; - } catch (AblyException e) { - try { - assertNotNull(String.format("got error \"%s\", none expected", e.errorInfo.message), expectedError); - assertEquals(String.format("expected to match \"%s\", got \"%s\"", expectedError, e.errorInfo.message), true, Pattern.compile(expectedError).matcher(e.errorInfo.message).find()); - if (expectedCode > 0) { - assertEquals(expectedCode, e.errorInfo.code); - } - if (expectedStatusCode > 0) { - assertEquals(expectedStatusCode, e.errorInfo.statusCode); - } - } catch(AssertionError ae) { - e.printStackTrace(); - throw ae; - } - } - return null; - } - - public static void assertInstanceOf(Class c, Object o) { - assertTrue(String.format("expected object of class %s to be instance of %s", o.getClass().getName(), c.getName()), c.isInstance(o)); - } - - public static void assertSize(int expected, Collection c) { - int size = c.size(); - assertEquals(String.format("expected collection to have size %d, got %d: %s", expected, size, c), expected, size); - } - - public static void assertSize(int expected, T[] c) { - int size = c.length; - assertEquals(String.format("expected array to have size %d, got %d: %s", expected, size, c), expected, size); - } - - public static HttpCore.Response httpResponseFromErrorInfo(final ErrorInfo errorInfo) { - HttpCore.Response response = new HttpCore.Response(); - response.contentType = "application/json"; - response.statusCode = errorInfo.statusCode > 0 ? errorInfo.statusCode : 400; - response.body = Serialisation.gson.toJson(new ErrorResponse() {{ - error = errorInfo; - }}, ErrorResponse.class).getBytes(); - return response; - } - - public static String tokenFromAuthHeader(String authHeader) { - if (!authHeader.startsWith("Bearer ")) { - return null; - } - - String token64 = authHeader.substring("Bearer ".length()); - String token = Base64Coder.decodeString(token64); - - return token; - } - - /** - * Trivial container for an int as counter. - * @author paddy - * - */ - private static class Counter { - public int value; - public int incr() { return ++value; } - } - - /** - * A class that may be passed as a listener for completion - * of an async operation. - * @author paddy - * - */ - public static class CompletionWaiter implements CompletionListener { - public boolean success; - public int successCount; - public ErrorInfo error; - - /** - * Public API - */ - public CompletionWaiter() { - reset(); - } - - public void reset() { - success = false; - successCount = 0; - error = null; - } - - public synchronized ErrorInfo waitFor(int count) { - while(successCount= count; - return error; - } - - public synchronized ErrorInfo waitFor() { - return waitFor(1); - } - - /** - * CompletionListener methods - */ - @Override - public void onSuccess() { - synchronized(this) { - successCount++; - notifyAll(); - } - } - @Override - public void onError(ErrorInfo reason) { - synchronized(this) { - error = reason; - notifyAll(); - } - } - } - - /** - * A class that subscribes to a channel and tracks messages received. - * @author paddy - * - */ - public static class MessageWaiter implements MessageListener { - public List receivedMessages; - - /** - * Public API - */ - - /** - * Track all messages on a channel. - * @param channel - */ - public MessageWaiter(Channel channel) { - reset(); - try { - channel.subscribe(this); - } catch(AblyException e) {} - } - - /** - * Track messages on a channel with a given event name - * @param channel - * @param event - */ - public MessageWaiter(Channel channel, String event) { - reset(); - try { - channel.subscribe(event, this); - } catch(AblyException e) {} - } - - /** - * Wait for a given number of messages - * @param count - */ - public synchronized void waitFor(int count) { - while(receivedMessages.size() < count) - try { wait(); } catch(InterruptedException e) {} - } - - /** - * Wait for a given interval for a number of messages - * @param count - */ - public synchronized void waitFor(int count, long time) { - long targetTime = System.currentTimeMillis() + time; - long remaining = time; - while(receivedMessages.size() < count && remaining > 0) { - try { wait(remaining); } catch(InterruptedException e) {} - remaining = targetTime - System.currentTimeMillis(); - } - } - - /** - * Reset the counter. Waiters will continue to - * wait, and will be unblocked when the revised count - * meets their requirements. - */ - public synchronized void reset() { - receivedMessages = new ArrayList(); - } - - /** - * MessageListener interface - */ - @Override - public void onMessage(Message message) { - synchronized(this) { - receivedMessages.add(message); - notify(); - } - } - } - - /** - * A class that tracks presence events on a channel - * @author paddy - * - */ - public static class PresenceWaiter implements PresenceListener { - public List receivedMessages; - - /** - * Public API - * @param channel - */ - public PresenceWaiter(Channel channel) { - reset(); - try { - channel.presence.subscribe(this); - } catch(AblyException e) {} - } - - public PresenceWaiter(PresenceMessage.Action event, Channel channel) throws AblyException { - reset(); - channel.presence.subscribe(event, this); - } - - public PresenceWaiter(EnumSet events, Channel channel) throws AblyException { - reset(); - channel.presence.subscribe(events, this); - } - - /** - * Wait for a given count of any type of message - * @param count - */ - public synchronized void waitFor(int count) { - while(receivedMessages.size() < count) - try { wait(); } catch(InterruptedException e) {} - } - - /** - * Wait for a given clientId. - * @param clientId - */ - public synchronized void waitFor(String clientId) { - while(contains(clientId) == null) - try { wait(); } catch(InterruptedException e) {} - } - - /** - * Wait for a given count of messages for a given clientId - * having a given action. - * @param clientId - * @param action - */ - public synchronized void waitFor(String clientId, PresenceMessage.Action action) { - while(contains(clientId, action) == null) - try { wait(); } catch(InterruptedException e) {} - } - - /** - * Reset the counter. Waiters will continue to - * wait, and will be unblocked when the revised count - * meets their requirements. - */ - public synchronized void reset() { - receivedMessages = new ArrayList(); - } - - /** - * PresenceListener API - */ - @Override - public void onPresenceMessage(PresenceMessage message) { - synchronized(this) { - receivedMessages.add(message); - notify(); - } - } - - /** - * Internal - */ - PresenceMessage contains(String clientId) { - for(PresenceMessage message : receivedMessages) - if(clientId.equals(message.clientId)) - return message; - return null; - } - public PresenceMessage contains(String clientId, PresenceMessage.Action action) { - for(PresenceMessage message : receivedMessages) - if(clientId.equals(message.clientId) && action == message.action) - return message; - return null; - } - public PresenceMessage contains(String clientId, String connectionId, PresenceMessage.Action action) { - for(PresenceMessage message : receivedMessages) - if(clientId.equals(message.clientId) && connectionId.equals(message.connectionId) && action == message.action) - return message; - return null; - } - } - - /** - * A class that listens for state change events on a connection. - * @author paddy - * - */ - public static class ConnectionWaiter implements ConnectionStateListener { - - /** - * Public API - */ - public ConnectionWaiter(Connection connection) { - reset(); - this.connection = connection; - connection.on(this); - } - - /** - * Wait for a given state to be reached. - * @param state - * @return error info - */ - public synchronized ErrorInfo waitFor(ConnectionState state) { - String targetStateName = state.getConnectionEvent().name(); - Log.d(TAG, "waitFor(state=" + targetStateName + ")"); - while (currentState() != state) { - try { - wait(); - } catch (InterruptedException e) { - } - } - Log.d(TAG, "waitFor done: state=" + targetStateName + ")"); - return reason; - } - - /** - * Wait for a given state to be reached a given number of times. - * @param state - * @param count - */ - public synchronized void waitFor(ConnectionState state, int count) { - Log.d(TAG, "waitFor(state=" + state.getConnectionEvent().name() + ", count=" + count + ")"); - - while(getStateCount(state) < count) - try { wait(); } catch(InterruptedException e) {} - Log.d(TAG, "waitFor done: state=" + latestChange.current.getConnectionEvent().name() + ", count=" + getStateCount(state) + ")"); - } - - /** - * Wait for a given state to be reached a given number of times, with a - * timeout - * @param state - * @param count - * @param time timeout in ms - * @return true if state was reached - */ - public synchronized boolean waitFor(ConnectionState state, int count, long time) { - Log.d(TAG, "waitFor(state=" + state.getConnectionEvent().name() + ", count=" + count + ", time=" + time + ")"); - long targetTime = System.currentTimeMillis() + time; - long remaining = time; - while(getStateCount(state) < count && remaining > 0) { - Log.d(TAG, "waitFor(state=" + state.getConnectionEvent().name() + ", waiting for=" + remaining + ")"); - try { wait(remaining); } catch(InterruptedException e) {} - remaining = targetTime - System.currentTimeMillis(); - } - int stateCount = getStateCount(state); - if(remaining <= 0) { - Log.d(TAG, "waitFor timed out: current state=" + currentState().getConnectionEvent().name()); - } else { - Log.d(TAG, "waitFor done: state=" + currentState().getConnectionEvent().name() + - ", count=" + Integer.toString(stateCount)); - } - return stateCount >= count; - } - - /** - * Get the count of number of times visited for a given state. - * @param state - * @return - */ - public synchronized int getCount(ConnectionState state) { - Counter counter = stateCounts.get(state); - if (counter == null) - return 0; - return counter.value; - } - - /** - * Reset counters. Waiters will continue to - * wait, and will be unblocked when the revised count - * meets their requirements. - */ - public synchronized void reset() { - Log.d(TAG, "reset()"); - stateCounts = new HashMap(); - } - - /** - * ConnectionStateListener interface - */ - @Override - public void onConnectionStateChanged(ConnectionStateListener.ConnectionStateChange state) { - synchronized(this) { - latestChange = state; - reason = state.reason; - Counter counter = stateCounts.get(state.current); if(counter == null) stateCounts.put(state.current, (counter = new Counter())); - counter.incr(); - Log.d(TAG, "onConnectionStateChanged(" + state.current + "): count now " + counter.value); - if(state.current == ConnectionState.connected) { - Log.d(TAG, "onConnectionStateChanged(connected): count now " + counter.value); - } - notify(); - } - } - - /** - * Helper function - */ - private synchronized int getStateCount(ConnectionState state) { - Counter counter = stateCounts.get(state); - return counter != null ? counter.value : 0; - } - - private synchronized ConnectionState currentState() { - return latestChange == null ? connection.state : latestChange.current; - } - - /** - * Internal - */ - private Connection connection; - private ErrorInfo reason; - private ConnectionStateChange latestChange; - private Map stateCounts; - private static final String TAG = ConnectionWaiter.class.getName(); - } - - /** - * A class that listens for state change events on a {@code ConnectionManager}. - */ - public static class ConnectionManagerWaiter { - private static final long INTERVAL_POLLING = 1000; - - /** - * Public API - */ - public ConnectionManagerWaiter(ConnectionManager connectionManager) { - this.connectionManager = connectionManager; - } - - /** - * Wait for a given state to be reached. - * @param state - * @return error info - */ - public synchronized ErrorInfo waitFor(ConnectionState state) { - while(connectionManager.getConnectionState().state != state) - try { wait(INTERVAL_POLLING); } catch(InterruptedException e) {} - return connectionManager.getConnectionState().defaultErrorInfo; - } - - /** - * Internal - */ - private ConnectionManager connectionManager; - } - - /** - * A class that listens for state change events on a channel. - * @author paddy - * - */ - public static class ChannelWaiter implements ChannelStateListener { - private static final String TAG = ChannelWaiter.class.getName(); - - /** - * Public API - * @param channel - */ - public ChannelWaiter(Channel channel) { - this.channel = channel; - channel.on(this); - } - - /** - * Wait for a given state to be reached. - * @param state - */ - public synchronized ErrorInfo waitFor(ChannelState state) { - Log.d(TAG, "waitFor(" + state + ")"); - while(channel.state != state) - try { wait(); } catch(InterruptedException e) {} - Log.d(TAG, "waitFor done: " + channel.state + ", " + channel.reason + ")"); - return channel.reason; - } - - /** - * ChannelStateListener interface - */ - @Override - public void onChannelStateChanged(ChannelStateListener.ChannelStateChange stateChange) { - synchronized(this) { notify(); } - } - - /** - * Internal - */ - private Channel channel; - } - - /** - * A class that listens for raw protocol messages sent and received on a realtime connection. - * - */ - public static class RawProtocolMonitor implements RawProtocolListener { - public Action sendAction; - public Action recvAction; - public String connectUrl; - public List sentMessages; - public List receivedMessages; - - /** - * Public API - */ - public static RawProtocolMonitor createReceiver(Action recvAction) { - return new RawProtocolMonitor(null, recvAction); - } - - public static RawProtocolMonitor createSender(Action sendAction) { - return new RawProtocolMonitor(sendAction, null); - } - - public static RawProtocolMonitor createMonitor(Action sendAction, Action recvAction) { - return new RawProtocolMonitor(sendAction, recvAction); - } - - /** - * Wait for a given number of messages - */ - public void waitForRecv() { - waitForRecv(1); - } - public void waitForSend() { - waitForSend(1); - } - - /** - * Wait for a given number of messages - * @param count - */ - public synchronized void waitForRecv(int count) { - while(receivedMessages.size() < count) { - try { wait(); } catch(InterruptedException e) {} - } - } - public synchronized void waitForSend(int count) { - while(sentMessages.size() < count) { - try { wait(); } catch(InterruptedException e) {} - } - } - - /** - * Reset the counter. Waiters will continue to - * wait, and will be unblocked when the revised count - * meets their requirements. - */ - public synchronized void reset() { - sentMessages = new ArrayList(); - receivedMessages = new ArrayList(); - } - - - /** - * RawProtocolListener interface - */ - @Override - public void onRawMessageRecv(ProtocolMessage message) { - if(message.action == recvAction) { - synchronized(this) { - receivedMessages.add(message); - notify(); - } - } - } - - @Override - public void onRawMessageSend(ProtocolMessage message) { - if(message.action == sendAction) { - synchronized(this) { - sentMessages.add(message); - notify(); - } - } - } - - @Override - public void onRawConnectRequested(String url) {} - - @Override - public void onRawConnect(String url) { - connectUrl = url; - } - - private RawProtocolMonitor(Action sendAction, Action recvAction) { - this.sendAction = sendAction; - this.recvAction = recvAction; - reset(); - } - } - - /** - * A class that allows a series of async operations to be - * tracked, and allows a caller to wait for all to complete. - * @author paddy - * - */ - public static class CompletionSet { - public Set pending = new HashSet(); - public Set errors = new HashSet(); - - /** - * Member type. A Member instance exists for each of - * the operations added to a CompletionSet. - * @author paddy - * - */ - public class Member implements CompletionListener { - @Override - public void onSuccess() { - synchronized(CompletionSet.this) { - pending.remove(this); - CompletionSet.this.notifyAll(); - } - } - @Override - public void onError(ErrorInfo reason) { - synchronized(CompletionSet.this) { - pending.remove(this); - errors.add(reason); - CompletionSet.this.notifyAll(); - } - } - } - - /** - * Obtain a new member/listener to associate with an - * operation to be added to this set. - * @return - */ - public Member add() { - Member member = new Member(); - synchronized(CompletionSet.this) { pending.add(member); } - return member; - } - - /** - * Wait for all members to complete. - * @return an array of errors. - */ - public ErrorInfo[] waitFor() { - synchronized(CompletionSet.this) { - while(pending.size() > 0) - try { CompletionSet.this.wait(); } catch(InterruptedException e) {} - } - return errors.toArray(new ErrorInfo[errors.size()]); - } - } - - public static void assertMessagesEqual(BaseMessage expected, BaseMessage actual) { - assertEquals("Encodings differ.", expected.encoding, actual.encoding); - assertEquals("Message classes differ.", expected.getClass(), actual.getClass()); - assertEquals("Message data classes differ.", expected.data.getClass(), actual.data.getClass()); - if(expected.data instanceof byte[]) { - assertArrayEquals("Message binary data contents differ.", (byte[])expected.data, (byte[])actual.data); - } else if (expected.data instanceof JsonObject || expected.data instanceof JsonArray) { - Gson gson = new Gson(); - assertEquals("Message JSON contents differ.", gson.toJson((JsonElement)expected.data), gson.toJson((JsonElement)actual.data)); - } else { - assertEquals("Message data contents differ.", expected.data, actual.data); - } - } - - public static class AsyncWaiter implements Callback { - @Override - public synchronized void onSuccess(T result) { - this.result = result; - gotResult = true; - notify(); - } - - @Override - public synchronized void onError(ErrorInfo error) { - this.error = error; - gotResult = true; - notify(); - } - - public synchronized void waitFor() { - try { - while(!gotResult) { - wait(); - } - } catch(InterruptedException e) {} - } - - public T result; - public ErrorInfo error; - private boolean gotResult = false; - } - - public static boolean equalStrings(String one, String two) { - return one != null && one.equals(two); - } - - public static boolean equalNullableStrings(String one, String two) { - return (one == null) ? (two == null) : one.equals(two); - } - - public static class RawHttpRequest { - public String id; - public URL url; - public HttpURLConnection conn; - public String method; - public String authHeader; - public Map> requestHeaders; - public HttpCore.RequestBody requestBody; - public HttpCore.Response response; - public Throwable error; - } - - public static class RawHttpTracker extends LinkedHashMap implements RawHttpListener { - private static final long serialVersionUID = 1L; - private boolean locked = false; - public HttpCore.Response mockResponse = null; - private AsyncWaiter requestWaiter = null; - - @Override - public HttpCore.Response onRawHttpRequest(String id, HttpURLConnection conn, String method, String authHeader, Map> requestHeaders, - HttpCore.RequestBody requestBody) { - - /* duplicating if necessary, ensure lower-case versions of header names are present */ - Map> normalisedHeaders = new HashMap>(); - if(requestHeaders != null) { - normalisedHeaders.putAll(requestHeaders); - for(String header : requestHeaders.keySet()) { - normalisedHeaders.put(header.toLowerCase(), requestHeaders.get(header)); - } - } - RawHttpRequest req = new RawHttpRequest(); - req.id = id; - req.url = conn.getURL(); - req.conn = conn; - req.method = method; - req.authHeader = authHeader; - req.requestHeaders = normalisedHeaders; - req.requestBody = requestBody; - put(id, req); - - if (requestWaiter != null) { - AsyncWaiter w = requestWaiter; - requestWaiter = null; - w.onSuccess(req); - } - - while (true) { - boolean l; - synchronized (this) { - l = locked; - } - if (!l) { - break; - } - try { - synchronized (this) { - wait(); - } - } catch (InterruptedException e) {} - } - - HttpCore.Response response = mockResponse; - mockResponse = null; - return response; - } - - @Override - public void onRawHttpResponse(String id, String method, HttpCore.Response response) { - /* duplicating if necessary, ensure lower-case versions of header names are present */ - Map> headers = response.headers; - Map> normalisedHeaders = new HashMap>(); - if(headers != null) { - normalisedHeaders.putAll(headers); - for(String header : headers.keySet()) { - normalisedHeaders.put(header.toLowerCase(), headers.get(header)); - } - response.headers = normalisedHeaders; - } - - RawHttpRequest req = get(id); - if(req != null) { - req.response = response; - } - } - - @Override - public void onRawHttpException(String id, String method, Throwable t) { - RawHttpRequest req = get(id); - if(req != null) { - req.error = t; - } - } - - public RawHttpRequest getFirstRequest() { - Collection reqs = values(); - return (RawHttpRequest) reqs.toArray()[0]; - } - - public RawHttpRequest getLastRequest() { - Collection reqs = values(); - return (RawHttpRequest) reqs.toArray()[reqs.size() - 1]; - } - - public String getRequestParam(String id, String param) { - String result = null; - RawHttpRequest req = get(id); - if(req != null) { - String query = req.conn.getURL().getQuery(); - if(query != null && !query.isEmpty()) { - result = HttpUtils.decodeParams(query).get(param).value; - } - } - return result; - } - - public List getRequestHeader(String id, String header) { - List result = null; - RawHttpRequest req = get(id); - if(req != null) { - header = header.toLowerCase(); - if(header.equalsIgnoreCase("authorization")) { - result = Collections.singletonList(req.authHeader); - } else { - result = req.requestHeaders.get(header); - } - } - return result; - } - - public List getResponseHeader(String id, String header) { - List result = null; - RawHttpRequest req = get(id); - if(req != null) { - header = header.toLowerCase(); - Listheaders = req.response.headers.get(header); - if(headers != null && headers.size() > 0) { - result = headers; - } - } - return result; - } - - public void lockRequests() { - synchronized (this) { - locked = true; - } - } - - public void unlockRequests() { - synchronized (this) { - locked = false; - notifyAll(); - } - } - - public AsyncWaiter getRequestWaiter() { - requestWaiter = new AsyncWaiter<>(); - return requestWaiter; - } - } - - public static class RandomGenerator { - - private static Random random = new Random((new Date()).getTime()); - private static final char[] values = {'a','b','c','d','e','f','g','h','i','j', - 'k','l','m','n','o','p','q','r','s','t', - 'u','v','w','x','y','z','0','1','2','3', - '4','5','6','7','8','9'}; - - public static String generateRandomString(int length) { - char[] chars = new char[length]; - for (int i = 0; i < length; i++) { - int idx = random.nextInt(values.length); - chars[i] = values[idx]; - } - return new String(chars); - } - - public static byte[] generateRandomBuffer(int length) { - byte[] buf = new byte[length]; - for (int i = 0; i < length; i++) { - int idx = random.nextInt(256); - buf[i] = (byte)(idx - 128); - } - return buf; - } - } - - public abstract static class SyncAndAsync { - public abstract T getSync(Arg arg) throws AblyException; - public abstract void getAsync(Arg arg, Callback callback); - public abstract void then(AblyFunction get) throws AblyException; - - public void run() throws AblyException { - then(new AblyFunction() { - @Override - public T apply(Arg arg) throws AblyException { - return getSync(arg); - } - }); - - then(new AblyFunction() { - @Override - public T apply(Arg arg) throws AblyException { - AsyncWaiter callback = new AsyncWaiter(); - getAsync(arg, callback); - callback.waitFor(); - if (callback.error != null) { - throw AblyException.fromErrorInfo(callback.error); - } - return callback.result; - } - }); - } - } - - public interface AblyFunction { - Result apply(Arg arg) throws AblyException; - } + public static void assertArrayUnorderedEquals(T[] expected, T[] got) { + Set expectedSet = new CopyOnWriteArraySet(Arrays.asList(expected)); + Set gotSet = new CopyOnWriteArraySet(Arrays.asList(got)); + assertEquals(expectedSet, gotSet); + } + + public static T expectedError(AblyFunction f, String expectedError) { + return expectedError(f, expectedError, 0); + } + + public static T expectedError(AblyFunction f, String expectedError, int expectedStatusCode) { + return expectedError(f, expectedError, expectedStatusCode, 0); + } + + public static T expectedError(AblyFunction f, String expectedError, int expectedStatusCode, int expectedCode) { + try { + T result = f.apply(null); + assertEquals(null, expectedError); + return result; + } catch (AblyException e) { + try { + assertNotNull(String.format("got error \"%s\", none expected", e.errorInfo.message), expectedError); + assertEquals(String.format("expected to match \"%s\", got \"%s\"", expectedError, e.errorInfo.message), true, Pattern.compile(expectedError).matcher(e.errorInfo.message).find()); + if (expectedCode > 0) { + assertEquals(expectedCode, e.errorInfo.code); + } + if (expectedStatusCode > 0) { + assertEquals(expectedStatusCode, e.errorInfo.statusCode); + } + } catch(AssertionError ae) { + e.printStackTrace(); + throw ae; + } + } + return null; + } + + public static void assertInstanceOf(Class c, Object o) { + assertTrue(String.format("expected object of class %s to be instance of %s", o.getClass().getName(), c.getName()), c.isInstance(o)); + } + + public static void assertSize(int expected, Collection c) { + int size = c.size(); + assertEquals(String.format("expected collection to have size %d, got %d: %s", expected, size, c), expected, size); + } + + public static void assertSize(int expected, T[] c) { + int size = c.length; + assertEquals(String.format("expected array to have size %d, got %d: %s", expected, size, c), expected, size); + } + + public static HttpCore.Response httpResponseFromErrorInfo(final ErrorInfo errorInfo) { + HttpCore.Response response = new HttpCore.Response(); + response.contentType = "application/json"; + response.statusCode = errorInfo.statusCode > 0 ? errorInfo.statusCode : 400; + response.body = Serialisation.gson.toJson(new ErrorResponse() {{ + error = errorInfo; + }}, ErrorResponse.class).getBytes(); + return response; + } + + public static String tokenFromAuthHeader(String authHeader) { + if (!authHeader.startsWith("Bearer ")) { + return null; + } + + String token64 = authHeader.substring("Bearer ".length()); + String token = Base64Coder.decodeString(token64); + + return token; + } + + /** + * Trivial container for an int as counter. + * @author paddy + * + */ + private static class Counter { + public int value; + public int incr() { return ++value; } + } + + /** + * A class that may be passed as a listener for completion + * of an async operation. + * @author paddy + * + */ + public static class CompletionWaiter implements CompletionListener { + public boolean success; + public int successCount; + public ErrorInfo error; + + /** + * Public API + */ + public CompletionWaiter() { + reset(); + } + + public void reset() { + success = false; + successCount = 0; + error = null; + } + + public synchronized ErrorInfo waitFor(int count) { + while(successCount= count; + return error; + } + + public synchronized ErrorInfo waitFor() { + return waitFor(1); + } + + /** + * CompletionListener methods + */ + @Override + public void onSuccess() { + synchronized(this) { + successCount++; + notifyAll(); + } + } + @Override + public void onError(ErrorInfo reason) { + synchronized(this) { + error = reason; + notifyAll(); + } + } + } + + /** + * A class that subscribes to a channel and tracks messages received. + * @author paddy + * + */ + public static class MessageWaiter implements MessageListener { + public List receivedMessages; + + /** + * Public API + */ + + /** + * Track all messages on a channel. + * @param channel + */ + public MessageWaiter(Channel channel) { + reset(); + try { + channel.subscribe(this); + } catch(AblyException e) {} + } + + /** + * Track messages on a channel with a given event name + * @param channel + * @param event + */ + public MessageWaiter(Channel channel, String event) { + reset(); + try { + channel.subscribe(event, this); + } catch(AblyException e) {} + } + + /** + * Wait for a given number of messages + * @param count + */ + public synchronized void waitFor(int count) { + while(receivedMessages.size() < count) + try { wait(); } catch(InterruptedException e) {} + } + + /** + * Wait for a given interval for a number of messages + * @param count + */ + public synchronized void waitFor(int count, long time) { + long targetTime = System.currentTimeMillis() + time; + long remaining = time; + while(receivedMessages.size() < count && remaining > 0) { + try { wait(remaining); } catch(InterruptedException e) {} + remaining = targetTime - System.currentTimeMillis(); + } + } + + /** + * Reset the counter. Waiters will continue to + * wait, and will be unblocked when the revised count + * meets their requirements. + */ + public synchronized void reset() { + receivedMessages = new ArrayList(); + } + + /** + * MessageListener interface + */ + @Override + public void onMessage(Message message) { + synchronized(this) { + receivedMessages.add(message); + notify(); + } + } + } + + /** + * A class that tracks presence events on a channel + * @author paddy + * + */ + public static class PresenceWaiter implements PresenceListener { + public List receivedMessages; + + /** + * Public API + * @param channel + */ + public PresenceWaiter(Channel channel) { + reset(); + try { + channel.presence.subscribe(this); + } catch(AblyException e) {} + } + + public PresenceWaiter(PresenceMessage.Action event, Channel channel) throws AblyException { + reset(); + channel.presence.subscribe(event, this); + } + + public PresenceWaiter(EnumSet events, Channel channel) throws AblyException { + reset(); + channel.presence.subscribe(events, this); + } + + /** + * Wait for a given count of any type of message + * @param count + */ + public synchronized void waitFor(int count) { + while(receivedMessages.size() < count) + try { wait(); } catch(InterruptedException e) {} + } + + /** + * Wait for a given clientId. + * @param clientId + */ + public synchronized void waitFor(String clientId) { + while(contains(clientId) == null) + try { wait(); } catch(InterruptedException e) {} + } + + /** + * Wait for a given count of messages for a given clientId + * having a given action. + * @param clientId + * @param action + */ + public synchronized void waitFor(String clientId, PresenceMessage.Action action) { + while(contains(clientId, action) == null) + try { wait(); } catch(InterruptedException e) {} + } + + /** + * Reset the counter. Waiters will continue to + * wait, and will be unblocked when the revised count + * meets their requirements. + */ + public synchronized void reset() { + receivedMessages = new ArrayList(); + } + + /** + * PresenceListener API + */ + @Override + public void onPresenceMessage(PresenceMessage message) { + synchronized(this) { + receivedMessages.add(message); + notify(); + } + } + + /** + * Internal + */ + PresenceMessage contains(String clientId) { + for(PresenceMessage message : receivedMessages) + if(clientId.equals(message.clientId)) + return message; + return null; + } + public PresenceMessage contains(String clientId, PresenceMessage.Action action) { + for(PresenceMessage message : receivedMessages) + if(clientId.equals(message.clientId) && action == message.action) + return message; + return null; + } + public PresenceMessage contains(String clientId, String connectionId, PresenceMessage.Action action) { + for(PresenceMessage message : receivedMessages) + if(clientId.equals(message.clientId) && connectionId.equals(message.connectionId) && action == message.action) + return message; + return null; + } + } + + /** + * A class that listens for state change events on a connection. + * @author paddy + * + */ + public static class ConnectionWaiter implements ConnectionStateListener { + + /** + * Public API + */ + public ConnectionWaiter(Connection connection) { + reset(); + this.connection = connection; + connection.on(this); + } + + /** + * Wait for a given state to be reached. + * @param state + * @return error info + */ + public synchronized ErrorInfo waitFor(ConnectionState state) { + String targetStateName = state.getConnectionEvent().name(); + Log.d(TAG, "waitFor(state=" + targetStateName + ")"); + while (currentState() != state) { + try { + wait(); + } catch (InterruptedException e) { + } + } + Log.d(TAG, "waitFor done: state=" + targetStateName + ")"); + return reason; + } + + /** + * Wait for a given state to be reached a given number of times. + * @param state + * @param count + */ + public synchronized void waitFor(ConnectionState state, int count) { + Log.d(TAG, "waitFor(state=" + state.getConnectionEvent().name() + ", count=" + count + ")"); + + while(getStateCount(state) < count) + try { wait(); } catch(InterruptedException e) {} + Log.d(TAG, "waitFor done: state=" + latestChange.current.getConnectionEvent().name() + ", count=" + getStateCount(state) + ")"); + } + + /** + * Wait for a given state to be reached a given number of times, with a + * timeout + * @param state + * @param count + * @param time timeout in ms + * @return true if state was reached + */ + public synchronized boolean waitFor(ConnectionState state, int count, long time) { + Log.d(TAG, "waitFor(state=" + state.getConnectionEvent().name() + ", count=" + count + ", time=" + time + ")"); + long targetTime = System.currentTimeMillis() + time; + long remaining = time; + while(getStateCount(state) < count && remaining > 0) { + Log.d(TAG, "waitFor(state=" + state.getConnectionEvent().name() + ", waiting for=" + remaining + ")"); + try { wait(remaining); } catch(InterruptedException e) {} + remaining = targetTime - System.currentTimeMillis(); + } + int stateCount = getStateCount(state); + if(remaining <= 0) { + Log.d(TAG, "waitFor timed out: current state=" + currentState().getConnectionEvent().name()); + } else { + Log.d(TAG, "waitFor done: state=" + currentState().getConnectionEvent().name() + + ", count=" + Integer.toString(stateCount)); + } + return stateCount >= count; + } + + /** + * Get the count of number of times visited for a given state. + * @param state + * @return + */ + public synchronized int getCount(ConnectionState state) { + Counter counter = stateCounts.get(state); + if (counter == null) + return 0; + return counter.value; + } + + /** + * Reset counters. Waiters will continue to + * wait, and will be unblocked when the revised count + * meets their requirements. + */ + public synchronized void reset() { + Log.d(TAG, "reset()"); + stateCounts = new HashMap(); + } + + /** + * ConnectionStateListener interface + */ + @Override + public void onConnectionStateChanged(ConnectionStateListener.ConnectionStateChange state) { + synchronized(this) { + latestChange = state; + reason = state.reason; + Counter counter = stateCounts.get(state.current); if(counter == null) stateCounts.put(state.current, (counter = new Counter())); + counter.incr(); + Log.d(TAG, "onConnectionStateChanged(" + state.current + "): count now " + counter.value); + if(state.current == ConnectionState.connected) { + Log.d(TAG, "onConnectionStateChanged(connected): count now " + counter.value); + } + notify(); + } + } + + /** + * Helper function + */ + private synchronized int getStateCount(ConnectionState state) { + Counter counter = stateCounts.get(state); + return counter != null ? counter.value : 0; + } + + private synchronized ConnectionState currentState() { + return latestChange == null ? connection.state : latestChange.current; + } + + /** + * Internal + */ + private Connection connection; + private ErrorInfo reason; + private ConnectionStateChange latestChange; + private Map stateCounts; + private static final String TAG = ConnectionWaiter.class.getName(); + } + + /** + * A class that listens for state change events on a {@code ConnectionManager}. + */ + public static class ConnectionManagerWaiter { + private static final long INTERVAL_POLLING = 1000; + + /** + * Public API + */ + public ConnectionManagerWaiter(ConnectionManager connectionManager) { + this.connectionManager = connectionManager; + } + + /** + * Wait for a given state to be reached. + * @param state + * @return error info + */ + public synchronized ErrorInfo waitFor(ConnectionState state) { + while(connectionManager.getConnectionState().state != state) + try { wait(INTERVAL_POLLING); } catch(InterruptedException e) {} + return connectionManager.getConnectionState().defaultErrorInfo; + } + + /** + * Internal + */ + private ConnectionManager connectionManager; + } + + /** + * A class that listens for state change events on a channel. + * @author paddy + * + */ + public static class ChannelWaiter implements ChannelStateListener { + private static final String TAG = ChannelWaiter.class.getName(); + + /** + * Public API + * @param channel + */ + public ChannelWaiter(Channel channel) { + this.channel = channel; + channel.on(this); + } + + /** + * Wait for a given state to be reached. + * @param state + */ + public synchronized ErrorInfo waitFor(ChannelState state) { + Log.d(TAG, "waitFor(" + state + ")"); + while(channel.state != state) + try { wait(); } catch(InterruptedException e) {} + Log.d(TAG, "waitFor done: " + channel.state + ", " + channel.reason + ")"); + return channel.reason; + } + + /** + * ChannelStateListener interface + */ + @Override + public void onChannelStateChanged(ChannelStateListener.ChannelStateChange stateChange) { + synchronized(this) { notify(); } + } + + /** + * Internal + */ + private Channel channel; + } + + /** + * A class that listens for raw protocol messages sent and received on a realtime connection. + * + */ + public static class RawProtocolMonitor implements RawProtocolListener { + public Action sendAction; + public Action recvAction; + public String connectUrl; + public List sentMessages; + public List receivedMessages; + + /** + * Public API + */ + public static RawProtocolMonitor createReceiver(Action recvAction) { + return new RawProtocolMonitor(null, recvAction); + } + + public static RawProtocolMonitor createSender(Action sendAction) { + return new RawProtocolMonitor(sendAction, null); + } + + public static RawProtocolMonitor createMonitor(Action sendAction, Action recvAction) { + return new RawProtocolMonitor(sendAction, recvAction); + } + + /** + * Wait for a given number of messages + */ + public void waitForRecv() { + waitForRecv(1); + } + public void waitForSend() { + waitForSend(1); + } + + /** + * Wait for a given number of messages + * @param count + */ + public synchronized void waitForRecv(int count) { + while(receivedMessages.size() < count) { + try { wait(); } catch(InterruptedException e) {} + } + } + public synchronized void waitForSend(int count) { + while(sentMessages.size() < count) { + try { wait(); } catch(InterruptedException e) {} + } + } + + /** + * Reset the counter. Waiters will continue to + * wait, and will be unblocked when the revised count + * meets their requirements. + */ + public synchronized void reset() { + sentMessages = new ArrayList(); + receivedMessages = new ArrayList(); + } + + + /** + * RawProtocolListener interface + */ + @Override + public void onRawMessageRecv(ProtocolMessage message) { + if(message.action == recvAction) { + synchronized(this) { + receivedMessages.add(message); + notify(); + } + } + } + + @Override + public void onRawMessageSend(ProtocolMessage message) { + if(message.action == sendAction) { + synchronized(this) { + sentMessages.add(message); + notify(); + } + } + } + + @Override + public void onRawConnectRequested(String url) {} + + @Override + public void onRawConnect(String url) { + connectUrl = url; + } + + private RawProtocolMonitor(Action sendAction, Action recvAction) { + this.sendAction = sendAction; + this.recvAction = recvAction; + reset(); + } + } + + /** + * A class that allows a series of async operations to be + * tracked, and allows a caller to wait for all to complete. + * @author paddy + * + */ + public static class CompletionSet { + public Set pending = new HashSet(); + public Set errors = new HashSet(); + + /** + * Member type. A Member instance exists for each of + * the operations added to a CompletionSet. + * @author paddy + * + */ + public class Member implements CompletionListener { + @Override + public void onSuccess() { + synchronized(CompletionSet.this) { + pending.remove(this); + CompletionSet.this.notifyAll(); + } + } + @Override + public void onError(ErrorInfo reason) { + synchronized(CompletionSet.this) { + pending.remove(this); + errors.add(reason); + CompletionSet.this.notifyAll(); + } + } + } + + /** + * Obtain a new member/listener to associate with an + * operation to be added to this set. + * @return + */ + public Member add() { + Member member = new Member(); + synchronized(CompletionSet.this) { pending.add(member); } + return member; + } + + /** + * Wait for all members to complete. + * @return an array of errors. + */ + public ErrorInfo[] waitFor() { + synchronized(CompletionSet.this) { + while(pending.size() > 0) + try { CompletionSet.this.wait(); } catch(InterruptedException e) {} + } + return errors.toArray(new ErrorInfo[errors.size()]); + } + } + + public static void assertMessagesEqual(BaseMessage expected, BaseMessage actual) { + assertEquals("Encodings differ.", expected.encoding, actual.encoding); + assertEquals("Message classes differ.", expected.getClass(), actual.getClass()); + assertEquals("Message data classes differ.", expected.data.getClass(), actual.data.getClass()); + if(expected.data instanceof byte[]) { + assertArrayEquals("Message binary data contents differ.", (byte[])expected.data, (byte[])actual.data); + } else if (expected.data instanceof JsonObject || expected.data instanceof JsonArray) { + Gson gson = new Gson(); + assertEquals("Message JSON contents differ.", gson.toJson((JsonElement)expected.data), gson.toJson((JsonElement)actual.data)); + } else { + assertEquals("Message data contents differ.", expected.data, actual.data); + } + } + + public static class AsyncWaiter implements Callback { + @Override + public synchronized void onSuccess(T result) { + this.result = result; + gotResult = true; + notify(); + } + + @Override + public synchronized void onError(ErrorInfo error) { + this.error = error; + gotResult = true; + notify(); + } + + public synchronized void waitFor() { + try { + while(!gotResult) { + wait(); + } + } catch(InterruptedException e) {} + } + + public T result; + public ErrorInfo error; + private boolean gotResult = false; + } + + public static boolean equalStrings(String one, String two) { + return one != null && one.equals(two); + } + + public static boolean equalNullableStrings(String one, String two) { + return (one == null) ? (two == null) : one.equals(two); + } + + public static class RawHttpRequest { + public String id; + public URL url; + public HttpURLConnection conn; + public String method; + public String authHeader; + public Map> requestHeaders; + public HttpCore.RequestBody requestBody; + public HttpCore.Response response; + public Throwable error; + } + + public static class RawHttpTracker extends LinkedHashMap implements RawHttpListener { + private static final long serialVersionUID = 1L; + private boolean locked = false; + public HttpCore.Response mockResponse = null; + private AsyncWaiter requestWaiter = null; + + @Override + public HttpCore.Response onRawHttpRequest(String id, HttpURLConnection conn, String method, String authHeader, Map> requestHeaders, + HttpCore.RequestBody requestBody) { + + /* duplicating if necessary, ensure lower-case versions of header names are present */ + Map> normalisedHeaders = new HashMap>(); + if(requestHeaders != null) { + normalisedHeaders.putAll(requestHeaders); + for(String header : requestHeaders.keySet()) { + normalisedHeaders.put(header.toLowerCase(), requestHeaders.get(header)); + } + } + RawHttpRequest req = new RawHttpRequest(); + req.id = id; + req.url = conn.getURL(); + req.conn = conn; + req.method = method; + req.authHeader = authHeader; + req.requestHeaders = normalisedHeaders; + req.requestBody = requestBody; + put(id, req); + + if (requestWaiter != null) { + AsyncWaiter w = requestWaiter; + requestWaiter = null; + w.onSuccess(req); + } + + while (true) { + boolean l; + synchronized (this) { + l = locked; + } + if (!l) { + break; + } + try { + synchronized (this) { + wait(); + } + } catch (InterruptedException e) {} + } + + HttpCore.Response response = mockResponse; + mockResponse = null; + return response; + } + + @Override + public void onRawHttpResponse(String id, String method, HttpCore.Response response) { + /* duplicating if necessary, ensure lower-case versions of header names are present */ + Map> headers = response.headers; + Map> normalisedHeaders = new HashMap>(); + if(headers != null) { + normalisedHeaders.putAll(headers); + for(String header : headers.keySet()) { + normalisedHeaders.put(header.toLowerCase(), headers.get(header)); + } + response.headers = normalisedHeaders; + } + + RawHttpRequest req = get(id); + if(req != null) { + req.response = response; + } + } + + @Override + public void onRawHttpException(String id, String method, Throwable t) { + RawHttpRequest req = get(id); + if(req != null) { + req.error = t; + } + } + + public RawHttpRequest getFirstRequest() { + Collection reqs = values(); + return (RawHttpRequest) reqs.toArray()[0]; + } + + public RawHttpRequest getLastRequest() { + Collection reqs = values(); + return (RawHttpRequest) reqs.toArray()[reqs.size() - 1]; + } + + public String getRequestParam(String id, String param) { + String result = null; + RawHttpRequest req = get(id); + if(req != null) { + String query = req.conn.getURL().getQuery(); + if(query != null && !query.isEmpty()) { + result = HttpUtils.decodeParams(query).get(param).value; + } + } + return result; + } + + public List getRequestHeader(String id, String header) { + List result = null; + RawHttpRequest req = get(id); + if(req != null) { + header = header.toLowerCase(); + if(header.equalsIgnoreCase("authorization")) { + result = Collections.singletonList(req.authHeader); + } else { + result = req.requestHeaders.get(header); + } + } + return result; + } + + public List getResponseHeader(String id, String header) { + List result = null; + RawHttpRequest req = get(id); + if(req != null) { + header = header.toLowerCase(); + Listheaders = req.response.headers.get(header); + if(headers != null && headers.size() > 0) { + result = headers; + } + } + return result; + } + + public void lockRequests() { + synchronized (this) { + locked = true; + } + } + + public void unlockRequests() { + synchronized (this) { + locked = false; + notifyAll(); + } + } + + public AsyncWaiter getRequestWaiter() { + requestWaiter = new AsyncWaiter<>(); + return requestWaiter; + } + } + + public static class RandomGenerator { + + private static Random random = new Random((new Date()).getTime()); + private static final char[] values = {'a','b','c','d','e','f','g','h','i','j', + 'k','l','m','n','o','p','q','r','s','t', + 'u','v','w','x','y','z','0','1','2','3', + '4','5','6','7','8','9'}; + + public static String generateRandomString(int length) { + char[] chars = new char[length]; + for (int i = 0; i < length; i++) { + int idx = random.nextInt(values.length); + chars[i] = values[idx]; + } + return new String(chars); + } + + public static byte[] generateRandomBuffer(int length) { + byte[] buf = new byte[length]; + for (int i = 0; i < length; i++) { + int idx = random.nextInt(256); + buf[i] = (byte)(idx - 128); + } + return buf; + } + } + + public abstract static class SyncAndAsync { + public abstract T getSync(Arg arg) throws AblyException; + public abstract void getAsync(Arg arg, Callback callback); + public abstract void then(AblyFunction get) throws AblyException; + + public void run() throws AblyException { + then(new AblyFunction() { + @Override + public T apply(Arg arg) throws AblyException { + return getSync(arg); + } + }); + + then(new AblyFunction() { + @Override + public T apply(Arg arg) throws AblyException { + AsyncWaiter callback = new AsyncWaiter(); + getAsync(arg, callback); + callback.waitFor(); + if (callback.error != null) { + throw AblyException.fromErrorInfo(callback.error); + } + return callback.result; + } + }); + } + } + + public interface AblyFunction { + Result apply(Arg arg) throws AblyException; + } } diff --git a/lib/src/test/java/io/ably/lib/test/common/ParameterizedTest.java b/lib/src/test/java/io/ably/lib/test/common/ParameterizedTest.java index 6ee8719dc..8893bb245 100644 --- a/lib/src/test/java/io/ably/lib/test/common/ParameterizedTest.java +++ b/lib/src/test/java/io/ably/lib/test/common/ParameterizedTest.java @@ -22,71 +22,71 @@ @RunWith(Parameterized.class) public class ParameterizedTest { - @Parameters(name = "{0}") - public static Iterable data() { - return Arrays.asList( - Setup.TestParameters.TEXT, - Setup.TestParameters.BINARY - ); - } - - @Parameter - public Setup.TestParameters testParams; - - @Rule - public Timeout testTimeout = Timeout.seconds(10); - - protected static Setup.TestVars testVars; - - @BeforeClass - public static void setUpBeforeClass() throws Exception { - testVars = Setup.getTestVars(); - } - - @AfterClass - public static void tearDownAfterClass() throws Exception { - Setup.clearTestVars(); - } - - protected DebugOptions createOptions() throws AblyException { - return testVars.createOptions(testParams); - } - - protected DebugOptions createOptions(String key) throws AblyException { - return testVars.createOptions(key, testParams); - } - - protected void fillInOptions(ClientOptions opts) { - testVars.fillInOptions(opts, testParams); - } - - /** - * Helper method to merge auth parameters - */ - protected static Param[] mergeParams(Param[][] items) { - Map merged = new HashMap(); - for(Param[] item : items) { - if(item != null) { - for(Param param : item) { merged.put(param.key, param); } - } - } - return merged.values().toArray(new Param[merged.size()]); - } - - protected static Param[] mergeParams(Param[] target, Param[] src) { - return mergeParams(new Param[][]{target, src}); - } - - final SimpleDateFormat timestampDateFormat = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss.SSS"); - - /** - * Generate a channel name that conforms to Ably's restrictions but is, as far as is - * reasonably achievable, unique to the test that's running. - * - * @see What restrictions exist for the name field of a channel? - */ - protected String createChannelName(final String testTitle) { - return this.getClass().getCanonicalName() + "/" + testTitle - + "/" + timestampDateFormat.format(new Date()); - } + @Parameters(name = "{0}") + public static Iterable data() { + return Arrays.asList( + Setup.TestParameters.TEXT, + Setup.TestParameters.BINARY + ); + } + + @Parameter + public Setup.TestParameters testParams; + + @Rule + public Timeout testTimeout = Timeout.seconds(10); + + protected static Setup.TestVars testVars; + + @BeforeClass + public static void setUpBeforeClass() throws Exception { + testVars = Setup.getTestVars(); + } + + @AfterClass + public static void tearDownAfterClass() throws Exception { + Setup.clearTestVars(); + } + + protected DebugOptions createOptions() throws AblyException { + return testVars.createOptions(testParams); + } + + protected DebugOptions createOptions(String key) throws AblyException { + return testVars.createOptions(key, testParams); + } + + protected void fillInOptions(ClientOptions opts) { + testVars.fillInOptions(opts, testParams); + } + + /** + * Helper method to merge auth parameters + */ + protected static Param[] mergeParams(Param[][] items) { + Map merged = new HashMap(); + for(Param[] item : items) { + if(item != null) { + for(Param param : item) { merged.put(param.key, param); } + } + } + return merged.values().toArray(new Param[merged.size()]); + } + + protected static Param[] mergeParams(Param[] target, Param[] src) { + return mergeParams(new Param[][]{target, src}); + } + + final SimpleDateFormat timestampDateFormat = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss.SSS"); + + /** + * Generate a channel name that conforms to Ably's restrictions but is, as far as is + * reasonably achievable, unique to the test that's running. + * + * @see What restrictions exist for the name field of a channel? + */ + protected String createChannelName(final String testTitle) { + return this.getClass().getCanonicalName() + "/" + testTitle + + "/" + timestampDateFormat.format(new Date()); + } } diff --git a/lib/src/test/java/io/ably/lib/test/common/Setup.java b/lib/src/test/java/io/ably/lib/test/common/Setup.java index c904eb131..cc1d1ccf1 100644 --- a/lib/src/test/java/io/ably/lib/test/common/Setup.java +++ b/lib/src/test/java/io/ably/lib/test/common/Setup.java @@ -23,256 +23,256 @@ public class Setup { - public static Object loadJson(String resourceName, Class expectedType) throws IOException { - try { - byte[] jsonBytes = resourceLoader.read(resourceName); - return gson.fromJson(new String(jsonBytes), expectedType); - } catch(IOException e) { - e.printStackTrace(); - return null; - } - } - - public static class Key { - public String keyName; - public String keySecret; - public String keyStr; - public String capability; - public int status; - } - - public static class Namespace { - public String id; - public boolean persisted; - public boolean pushEnabled; - public int status; - } - - public static class Connection { - public String id; - } - - public static class Channel { - public String name; - public PresenceMessage[] presence; - } - - public static class AppSpec { - public String id; - public String appId; - public String accountId; - public Key[] keys; - public Namespace[] namespaces; - public Connection[] connections; - public Channel[] channels; - public boolean tlsOnly; - public String notes; - } - - public static class TestParameters { - public boolean useBinaryProtocol; - public String name; - - public static TestParameters BINARY = new TestParameters(true, "binary_protocol"); - public static TestParameters TEXT = new TestParameters(false, "text_protocol"); - - public TestParameters(boolean useBinaryProtocol, String name) { - this.useBinaryProtocol = useBinaryProtocol; - this.name = name; - } - - public boolean equals(Object obj) { - TestParameters arg = (TestParameters)obj; - return arg.useBinaryProtocol == this.useBinaryProtocol; - } - - public String toString() { - return name; - } - - public static TestParameters getDefault() { - return BINARY; - } - } - - public static class TestVars extends AppSpec { - public String restHost; - public String realtimeHost; - public String environment; - public int port; - public int tlsPort; - public boolean tls; - - public DebugOptions createOptions() { - DebugOptions opts = new DebugOptions(); - fillInOptions(opts, null); - return opts; - } - - public DebugOptions createOptions(String key) throws AblyException { - return createOptions(key, null); - } - - public DebugOptions createOptions(TestParameters params) throws AblyException { - DebugOptions opts = new DebugOptions(); - fillInOptions(opts, params); - return opts; - } - - public DebugOptions createOptions(String key, TestParameters params) throws AblyException { - DebugOptions opts = new DebugOptions(key); - fillInOptions(opts, params); - return opts; - } - - public void fillInOptions(ClientOptions opts) { - fillInOptions(opts, null); - } - - public void fillInOptions(ClientOptions opts, TestParameters params) { - if(params == null) { params = TestParameters.getDefault(); } - opts.useBinaryProtocol = params.useBinaryProtocol; - opts.restHost = restHost; - opts.realtimeHost = realtimeHost; - opts.environment = environment; - opts.port = port; - opts.tlsPort = tlsPort; - opts.tls = tls; - } - } - - public static synchronized TestVars getTestVars() { - return (refCount++ == 0) ? __getTestVars() : testVars; - } - - private static TestVars __getTestVars() { - if(testVars == null) { - host = argumentLoader.getTestArgument("ABLY_REST_HOST"); - environment = argumentLoader.getTestArgument("ABLY_ENV"); - if(environment == null) { - environment = "sandbox"; - } - - if(host != null) { - wsHost = argumentLoader.getTestArgument("ABLY_REALTIME_HOST"); - if(wsHost == null) - wsHost = host; - } - - if(argumentLoader.getTestArgument("ABLY_PORT") != null) { - port = Integer.valueOf(argumentLoader.getTestArgument("ABLY_PORT")); - tlsPort = Integer.valueOf(argumentLoader.getTestArgument("ABLY_TLS_PORT")); - } else if((host != null && host.contains("local")) || environment.equals("local")) { - port = 8080; - tlsPort = 8081; - } else { - /* default to connecting to sandbox or production through load balancer */ - port = 80; - tlsPort = 443; - } - - if(ably == null) { - try { - ClientOptions opts = new ClientOptions(); - /* we need to provide an appId to keep the library happy, - * but we are only instancing the library to use the http - * convenience methods */ - opts.key = "none:none"; - opts.restHost = host; - opts.environment = environment; - opts.port = port; - opts.tlsPort = tlsPort; - opts.tls = true; - ably = new AblyRest(opts); - } catch(AblyException e) { - System.err.println("Unable to instance AblyRest: " + e); - e.printStackTrace(); - System.exit(1); - } - } - - Setup.AppSpec appSpec = null; - try { - appSpec = (Setup.AppSpec)loadJson(specFile, Setup.AppSpec.class); - appSpec.notes = "Test app; created by ably-java realtime tests; date = " + new Date().toString(); - } catch(IOException ioe) { - System.err.println("Unable to read spec file: " + ioe); - ioe.printStackTrace(); - System.exit(1); - } - try { - testVars = HttpHelpers.postSync(ably.http, "/apps", null, null, new HttpUtils.JsonRequestBody(appSpec), new HttpCore.ResponseHandler() { - @Override - public TestVars handleResponse(HttpCore.Response response, ErrorInfo error) throws AblyException { - if(error != null) { - throw AblyException.fromErrorInfo(error); - } - - TestVars result = (TestVars)Serialisation.gson.fromJson(new String(response.body), TestVars.class); - result.restHost = host; - result.realtimeHost = wsHost; - result.environment = environment; - result.port = port; - result.tlsPort = tlsPort; - result.tls = true; - return result; - }}, false); - } catch (AblyException ae) { - System.err.println("Unable to create test app: " + ae); - ae.printStackTrace(); - System.exit(1); - } - } - return testVars; - } - - public static synchronized void clearTestVars() { - if(--refCount == 0) - __clearTestVars(); - } - - private static void __clearTestVars() { - if(testVars != null) { - try { - ClientOptions opts = new ClientOptions(testVars.keys[0].keyStr); - opts.restHost = host; - opts.environment = environment; - opts.port = port; - opts.tlsPort = tlsPort; - opts.tls = true; - ably = new AblyRest(opts); - ably.http.request(new Http.Execute() { - @Override - public void execute(HttpScheduler http, Callback callback) throws AblyException { - http.del("/apps/" + testVars.appId, HttpUtils.defaultAcceptHeaders(false), null, null, true, callback); - } - }).sync(); - } catch (AblyException ae) { - System.err.println("Unable to delete test app: " + ae); - ae.printStackTrace(); - System.exit(1); - } - testVars = null; - } - } - - static { - argumentLoader = new ArgumentLoader(); - resourceLoader = new ResourceLoader(); - } - - private static ArgumentLoader argumentLoader; - private static ResourceLoader resourceLoader; - private static final String specFile = "local/testAppSpec.json"; - - private static AblyRest ably; - private static String environment; - private static String host; - private static String wsHost; - private static int port; - private static int tlsPort; - - private static TestVars testVars; - private static int refCount; - private static Gson gson = new Gson(); + public static Object loadJson(String resourceName, Class expectedType) throws IOException { + try { + byte[] jsonBytes = resourceLoader.read(resourceName); + return gson.fromJson(new String(jsonBytes), expectedType); + } catch(IOException e) { + e.printStackTrace(); + return null; + } + } + + public static class Key { + public String keyName; + public String keySecret; + public String keyStr; + public String capability; + public int status; + } + + public static class Namespace { + public String id; + public boolean persisted; + public boolean pushEnabled; + public int status; + } + + public static class Connection { + public String id; + } + + public static class Channel { + public String name; + public PresenceMessage[] presence; + } + + public static class AppSpec { + public String id; + public String appId; + public String accountId; + public Key[] keys; + public Namespace[] namespaces; + public Connection[] connections; + public Channel[] channels; + public boolean tlsOnly; + public String notes; + } + + public static class TestParameters { + public boolean useBinaryProtocol; + public String name; + + public static TestParameters BINARY = new TestParameters(true, "binary_protocol"); + public static TestParameters TEXT = new TestParameters(false, "text_protocol"); + + public TestParameters(boolean useBinaryProtocol, String name) { + this.useBinaryProtocol = useBinaryProtocol; + this.name = name; + } + + public boolean equals(Object obj) { + TestParameters arg = (TestParameters)obj; + return arg.useBinaryProtocol == this.useBinaryProtocol; + } + + public String toString() { + return name; + } + + public static TestParameters getDefault() { + return BINARY; + } + } + + public static class TestVars extends AppSpec { + public String restHost; + public String realtimeHost; + public String environment; + public int port; + public int tlsPort; + public boolean tls; + + public DebugOptions createOptions() { + DebugOptions opts = new DebugOptions(); + fillInOptions(opts, null); + return opts; + } + + public DebugOptions createOptions(String key) throws AblyException { + return createOptions(key, null); + } + + public DebugOptions createOptions(TestParameters params) throws AblyException { + DebugOptions opts = new DebugOptions(); + fillInOptions(opts, params); + return opts; + } + + public DebugOptions createOptions(String key, TestParameters params) throws AblyException { + DebugOptions opts = new DebugOptions(key); + fillInOptions(opts, params); + return opts; + } + + public void fillInOptions(ClientOptions opts) { + fillInOptions(opts, null); + } + + public void fillInOptions(ClientOptions opts, TestParameters params) { + if(params == null) { params = TestParameters.getDefault(); } + opts.useBinaryProtocol = params.useBinaryProtocol; + opts.restHost = restHost; + opts.realtimeHost = realtimeHost; + opts.environment = environment; + opts.port = port; + opts.tlsPort = tlsPort; + opts.tls = tls; + } + } + + public static synchronized TestVars getTestVars() { + return (refCount++ == 0) ? __getTestVars() : testVars; + } + + private static TestVars __getTestVars() { + if(testVars == null) { + host = argumentLoader.getTestArgument("ABLY_REST_HOST"); + environment = argumentLoader.getTestArgument("ABLY_ENV"); + if(environment == null) { + environment = "sandbox"; + } + + if(host != null) { + wsHost = argumentLoader.getTestArgument("ABLY_REALTIME_HOST"); + if(wsHost == null) + wsHost = host; + } + + if(argumentLoader.getTestArgument("ABLY_PORT") != null) { + port = Integer.valueOf(argumentLoader.getTestArgument("ABLY_PORT")); + tlsPort = Integer.valueOf(argumentLoader.getTestArgument("ABLY_TLS_PORT")); + } else if((host != null && host.contains("local")) || environment.equals("local")) { + port = 8080; + tlsPort = 8081; + } else { + /* default to connecting to sandbox or production through load balancer */ + port = 80; + tlsPort = 443; + } + + if(ably == null) { + try { + ClientOptions opts = new ClientOptions(); + /* we need to provide an appId to keep the library happy, + * but we are only instancing the library to use the http + * convenience methods */ + opts.key = "none:none"; + opts.restHost = host; + opts.environment = environment; + opts.port = port; + opts.tlsPort = tlsPort; + opts.tls = true; + ably = new AblyRest(opts); + } catch(AblyException e) { + System.err.println("Unable to instance AblyRest: " + e); + e.printStackTrace(); + System.exit(1); + } + } + + Setup.AppSpec appSpec = null; + try { + appSpec = (Setup.AppSpec)loadJson(specFile, Setup.AppSpec.class); + appSpec.notes = "Test app; created by ably-java realtime tests; date = " + new Date().toString(); + } catch(IOException ioe) { + System.err.println("Unable to read spec file: " + ioe); + ioe.printStackTrace(); + System.exit(1); + } + try { + testVars = HttpHelpers.postSync(ably.http, "/apps", null, null, new HttpUtils.JsonRequestBody(appSpec), new HttpCore.ResponseHandler() { + @Override + public TestVars handleResponse(HttpCore.Response response, ErrorInfo error) throws AblyException { + if(error != null) { + throw AblyException.fromErrorInfo(error); + } + + TestVars result = (TestVars)Serialisation.gson.fromJson(new String(response.body), TestVars.class); + result.restHost = host; + result.realtimeHost = wsHost; + result.environment = environment; + result.port = port; + result.tlsPort = tlsPort; + result.tls = true; + return result; + }}, false); + } catch (AblyException ae) { + System.err.println("Unable to create test app: " + ae); + ae.printStackTrace(); + System.exit(1); + } + } + return testVars; + } + + public static synchronized void clearTestVars() { + if(--refCount == 0) + __clearTestVars(); + } + + private static void __clearTestVars() { + if(testVars != null) { + try { + ClientOptions opts = new ClientOptions(testVars.keys[0].keyStr); + opts.restHost = host; + opts.environment = environment; + opts.port = port; + opts.tlsPort = tlsPort; + opts.tls = true; + ably = new AblyRest(opts); + ably.http.request(new Http.Execute() { + @Override + public void execute(HttpScheduler http, Callback callback) throws AblyException { + http.del("/apps/" + testVars.appId, HttpUtils.defaultAcceptHeaders(false), null, null, true, callback); + } + }).sync(); + } catch (AblyException ae) { + System.err.println("Unable to delete test app: " + ae); + ae.printStackTrace(); + System.exit(1); + } + testVars = null; + } + } + + static { + argumentLoader = new ArgumentLoader(); + resourceLoader = new ResourceLoader(); + } + + private static ArgumentLoader argumentLoader; + private static ResourceLoader resourceLoader; + private static final String specFile = "local/testAppSpec.json"; + + private static AblyRest ably; + private static String environment; + private static String host; + private static String wsHost; + private static int port; + private static int tlsPort; + + private static TestVars testVars; + private static int refCount; + private static Gson gson = new Gson(); } diff --git a/lib/src/test/java/io/ably/lib/test/realtime/ConnectionManagerTest.java b/lib/src/test/java/io/ably/lib/test/realtime/ConnectionManagerTest.java index 935737f21..fd4db4da2 100644 --- a/lib/src/test/java/io/ably/lib/test/realtime/ConnectionManagerTest.java +++ b/lib/src/test/java/io/ably/lib/test/realtime/ConnectionManagerTest.java @@ -53,681 +53,681 @@ */ public class ConnectionManagerTest extends ParameterizedTest { - @Rule - public Timeout testTimeout = Timeout.seconds(60); - - /** - *

- * Verifies that ably connects to default host, - * when everything is fine. - *

- * - * @throws AblyException - */ - @Test - public void connectionmanager_fallback_none() throws AblyException { - ClientOptions opts = createOptions(testVars.keys[0].keyStr); - try (AblyRealtime ably = new AblyRealtime(opts)) { - ConnectionManager connectionManager = ably.connection.connectionManager; - - new Helpers.ConnectionWaiter(ably.connection).waitFor(ConnectionState.connected); - - /* Verify that, - * - connectionManager is connected - * - connectionManager is connected to the host without any fallback - */ - assertThat(connectionManager.getConnectionState().state, is(ConnectionState.connected)); - assertThat(connectionManager.getHost(), is(equalTo(opts.environment + "-realtime.ably.io"))); - } - } - - /** - *

- * Verifies that fallback behaviour doesn't apply, when the default - * custom endpoint is being used - *

- *

- * Spec: RTN17b - *

- * - * @throws AblyException - */ - @Test - public void connectionmanager_fallback_none_customhost() throws AblyException { - ClientOptions opts = createOptions(testVars.keys[0].keyStr); - opts.realtimeHost = "un.reachable.host.example.com"; - opts.environment = null; - try(AblyRealtime ably = new AblyRealtime(opts)) { - ConnectionManager connectionManager = ably.connection.connectionManager; - - new Helpers.ConnectionWaiter(ably.connection).waitFor(ConnectionState.disconnected); - - /* Verify that, - * - connectionManager is disconnected - * - connectionManager's last host did not have any fallback - */ - assertThat(connectionManager.getConnectionState().state, is(ConnectionState.disconnected)); - assertThat(connectionManager.getHost(), is(equalTo(opts.realtimeHost))); - } - } - - /** - *

- * Verifies that the {@code ConnectionManager} first checks if an internet connection is - * available by issuing a GET request to https://internet-up.ably-realtime.com/is-the-internet-up.txt - * , when In the case of an error necessitating use of an alternative host (see RTN17d). - *

- *

- * Spec: RTN17c - *

- * - * @throws AblyException - */ - @Test - public void connectionmanager_fallback_none_withoutconnection() throws AblyException { - ClientOptions opts = createOptions(testVars.keys[0].keyStr); - opts.realtimeHost = "un.reachable.host"; - opts.environment = null; - opts.autoConnect = false; - try(AblyRealtime ably = new AblyRealtime(opts)) { - Connection connection = Mockito.mock(Connection.class); - final ConnectionManager.Channels channels = Mockito.mock(ConnectionManager.Channels.class); - - ConnectionManager connectionManager = new ConnectionManager(ably, connection, channels) { - @Override - protected boolean checkConnectivity() { - return false; - } - }; - - connectionManager.connect(); - - new Helpers.ConnectionManagerWaiter(connectionManager).waitFor(ConnectionState.disconnected); - - /* Verify that, - * - connectionManager is disconnected - * - connectionManager did not apply any fallback behavior - */ - assertThat(connectionManager.getConnectionState().state, is(ConnectionState.disconnected)); - assertThat(connectionManager.getHost(), is(equalTo(opts.realtimeHost))); - - connectionManager.close(); - } - } - - /** - *

- * Verifies that fallback behaviour is applied and HTTP client is using same fallback - * endpoint, when the default realtime.ably.io endpoint is being used and has not been - * overriden, and a fallback is applied - *

- *

- * Spec: RTN17b, RTN17c - *

- * - * @throws AblyException - */ - @Test - public void connectionmanager_default_fallback_applied() throws AblyException { - DebugOptions opts = new DebugOptions(testVars.keys[0].keyStr); - fillInOptions(opts); - - final Hosts hosts = new Hosts(null, Defaults.HOST_REALTIME, opts); - final String primaryHost = hosts.getPrimaryHost(); - - /* clear the environment override, so we trigger default fallback behaviour */ - opts.environment = null; - - /* set up mock transport */ - MockWebsocketFactory mockTransport = new MockWebsocketFactory(); - opts.transportFactory = mockTransport; - - /* ensure that all connection attempts ultimately resolve to the primary host */ - mockTransport.setHostTransform(new MockWebsocketFactory.HostTransform() { - @Override - public String transformHost(String givenHost) { - return primaryHost; - } - }); - - /* set up a filter on a mock transport to fail connections to the primary host */ - mockTransport.failConnect(new MockWebsocketFactory.HostFilter() { - @Override - public boolean matches(String hostname) { - return hostname.equals(primaryHost); - } - }); - - try (final AblyRealtime ably = new AblyRealtime(opts)) { - ConnectionManager connectionManager = ably.connection.connectionManager; - - new Helpers.ConnectionWaiter(ably.connection).waitFor(ConnectionState.connected); - - /* Verify that, - * - connectionManager is connected - * - connectionManager's last host was a fallback host - */ - assertThat(connectionManager.getConnectionState().state, is(ConnectionState.connected)); - assertThat(connectionManager.getHost(), is(not(equalTo(primaryHost)))); - } - } - - /** - * Verify that when environment is overridden, no fallback is used by default - * - *

- * Spec: RTN17b - *

- */ - @Test - public void connectionmanager_default_endpoint_no_fallback() throws AblyException { - DebugOptions opts = new DebugOptions(testVars.keys[0].keyStr); - fillInOptions(opts); - - final Hosts hosts = new Hosts(null, Defaults.HOST_REALTIME, opts); - final String primaryHost = hosts.getPrimaryHost(); - - MockWebsocketFactory mockTransport = new MockWebsocketFactory(); - opts.transportFactory = mockTransport; - - /* ensure that all connection attempts ultimately resolve to the primary host */ - mockTransport.setHostTransform(new MockWebsocketFactory.HostTransform() { - @Override - public String transformHost(String givenHost) { - return primaryHost; - } - }); - - /* set up a filter on a mock transport to fail connections to the primary host */ - mockTransport.failConnect(new MockWebsocketFactory.HostFilter() { - @Override - public boolean matches(String hostname) { - return hostname.equals(primaryHost); - } - }); - - try (final AblyRealtime ably = new AblyRealtime(opts)) { - ConnectionManager connectionManager = ably.connection.connectionManager; - - System.out.println("waiting for disconnected"); - new Helpers.ConnectionWaiter(ably.connection).waitFor(ConnectionState.disconnected); - System.out.println("got disconnected"); - - /* Verify that, - * - connectionManager is disconnected - * - connectionManager's last host was the primary host - */ - assertThat(connectionManager.getConnectionState().state, is(ConnectionState.disconnected)); - assertThat(connectionManager.getHost(), is(equalTo(primaryHost))); - } - } - - /** - * Verify that when environment is overridden and fallback specified, the fallback is used - * - *

- * Spec: RTN17b - *

- */ - @Test - public void connectionmanager_default_endpoint_explicit_fallback() throws AblyException { - DebugOptions opts = new DebugOptions(testVars.keys[0].keyStr); - fillInOptions(opts); - - final Hosts hosts = new Hosts(null, Defaults.HOST_REALTIME, opts); - final String primaryHost = hosts.getPrimaryHost(); - - opts.fallbackHosts = new String[]{"fallback 1", "fallback 2"}; - - MockWebsocketFactory mockTransport = new MockWebsocketFactory(); - opts.transportFactory = mockTransport; - - /* ensure that all connection attempts ultimately resolve to the primary host */ - mockTransport.setHostTransform(new MockWebsocketFactory.HostTransform() { - @Override - public String transformHost(String givenHost) { - return primaryHost; - } - }); - - /* set up a filter on a mock transport to fail connections to the primary host */ - mockTransport.failConnect(new MockWebsocketFactory.HostFilter() { - @Override - public boolean matches(String hostname) { - return hostname.equals(primaryHost); - } - }); - - try (final AblyRealtime ably = new AblyRealtime(opts)) { - ConnectionManager connectionManager = ably.connection.connectionManager; - - System.out.println("waiting for connected"); - new Helpers.ConnectionWaiter(ably.connection).waitFor(ConnectionState.connected); - System.out.println("got connected"); - - /* Verify that, - * - connectionManager is connected - * - connectionManager's last host was a fallback host - */ - assertThat(connectionManager.getConnectionState().state, is(ConnectionState.connected)); - assertThat(connectionManager.getHost(), is(not(equalTo(primaryHost)))); - } - } - - /** - * Test that default fallback happens with a non-default host if - * fallbackHostsUseDefault is set. - */ - @Test - public void connectionmanager_reconnect_default_fallback() throws AblyException { - DebugOptions opts = new DebugOptions(testVars.keys[0].keyStr); - fillInOptions(opts); - - opts.fallbackHostsUseDefault = true; - - final Hosts hosts = new Hosts(null, Defaults.HOST_REALTIME, opts); - final String primaryHost = hosts.getPrimaryHost(); - - MockWebsocketFactory mockTransport = new MockWebsocketFactory(); - opts.transportFactory = mockTransport; - - /* ensure that all connection attempts ultimately resolve to the primary host */ - mockTransport.setHostTransform(new MockWebsocketFactory.HostTransform() { - @Override - public String transformHost(String givenHost) { - return primaryHost; - } - }); - - /* set up a filter on a mock transport to fail connections to the primary host */ - mockTransport.failConnect(new MockWebsocketFactory.HostFilter() { - @Override - public boolean matches(String hostname) { - return hostname.equals(primaryHost); - } - }); - - try (final AblyRealtime ably = new AblyRealtime(opts)) { - ConnectionManager connectionManager = ably.connection.connectionManager; - - System.out.println("waiting for connected"); - new Helpers.ConnectionWaiter(ably.connection).waitFor(ConnectionState.connected); - System.out.println("got connected"); - ably.close(); - - /* Verify that, - * - connectionManager is connected - * - connectionManager's last host was a fallback host - */ - assertThat(connectionManager.getConnectionState().state, is(ConnectionState.connected)); - assertThat(connectionManager.getHost(), is(not(equalTo(opts.realtimeHost)))); - } - } - - /** - * Connect, and then perform a close() from the calling ConnectionManager context; - * verify that the closed state is reached, and the connectionmanager thread has exited - */ - @Test - public void close_from_connectionmanager() throws AblyException { - ClientOptions opts = createOptions(testVars.keys[0].keyStr); - final AblyRealtime ably = new AblyRealtime(opts); - final Thread[] threadContainer = new Thread[1]; - ably.connection.on(ConnectionEvent.connected, new ConnectionStateListener() { - @Override - public void onConnectionStateChanged(ConnectionStateChange state) { - ably.close(); - threadContainer[0] = Thread.currentThread(); - } - }); - - /* wait for cm thread to exit */ - try { - Thread.sleep(2000L); - } catch(InterruptedException e) {} - - assertEquals("Verify closed state is reached", ConnectionState.closed, ably.connection.state); - Thread.State cmThreadState = threadContainer[0].getState(); - assertEquals("Verify cm thread has exited", cmThreadState, Thread.State.TERMINATED); - } - - /** - * Connect, and then perform a close(); - * verify that the closed state is reached, and immediately - * reconnect; verify that it reconnects successfully - */ - @Test - public void connectionmanager_restart_race() throws AblyException { - ClientOptions opts = createOptions(testVars.keys[0].keyStr); - final AblyRealtime ably = new AblyRealtime(opts); - ConnectionWaiter connectionWaiter = new ConnectionWaiter(ably.connection); - - ably.connection.once(ConnectionEvent.connected, new ConnectionStateListener() { - @Override - public void onConnectionStateChanged(ConnectionStateChange state) { - ably.close(); - } - }); - - connectionWaiter.waitFor(ConnectionState.closed); - assertEquals("Verify closed state is reached", ConnectionState.closed, ably.connection.state); - connectionWaiter.reset(); - - /* reconnect */ - ably.connect(); - - /* verify the connection is reestablished */ - connectionWaiter.waitFor(ConnectionState.connected); - assertEquals("Verify connected state is reached", ConnectionState.connected, ably.connection.state); - - /* close the connection */ - ably.close(); - connectionWaiter.waitFor(ConnectionState.closed); - assertEquals("Verify closed state is reached", ConnectionState.closed, ably.connection.state); - } - - /** - * Connect, and then perform a close() from the calling ConnectionManager context; - * verify that the closed state is reached, and the connectionmanager thread has exited - */ - @Test - public void open_from_dedicated_thread() throws AblyException { - ClientOptions opts = createOptions(testVars.keys[0].keyStr); - opts.autoConnect = false; - final AblyRealtime ably = new AblyRealtime(opts); - final Thread[] threadContainer = new Thread[1]; - ably.connection.on(ConnectionEvent.connected, new ConnectionStateListener() { - @Override - public void onConnectionStateChanged(ConnectionStateChange state) { - threadContainer[0] = Thread.currentThread(); - } - }); - - ExecutorService executor = Executors.newSingleThreadExecutor(); - executor.submit(new Runnable() { - public void run() { - try { - ably.connection.connect(); - } catch (Throwable t) { - t.printStackTrace(); - } - } - }); - - ConnectionWaiter connectionWaiter = new ConnectionWaiter(ably.connection); - - connectionWaiter.waitFor(ConnectionState.connected); - assertEquals("Verify connected state is reached", ConnectionState.connected, ably.connection.state); - assertTrue("Not expecting token auth", ably.auth.getAuthMethod() == AuthMethod.basic); - - ably.close(); - connectionWaiter.waitFor(ConnectionState.closed); - assertEquals("Verify closed state is reached", ConnectionState.closed, ably.connection.state); - - /* wait for cm thread to exit */ - try { - Thread.sleep(2000L); - } catch(InterruptedException e) {} - - Thread.State cmThreadState = threadContainer[0].getState(); - assertEquals("Verify cm thread has exited", cmThreadState, Thread.State.TERMINATED); - } - - /** - * Connect, and then perform a close() from the calling ConnectionManager context; - * verify that the closed state is reached, and the connectionmanager thread has exited - */ - @Test - public void close_from_dedicated_thread() throws AblyException { - ClientOptions opts = createOptions(testVars.keys[0].keyStr); - opts.autoConnect = false; - final AblyRealtime ably = new AblyRealtime(opts); - final Thread[] threadContainer = new Thread[1]; - ably.connection.on(ConnectionEvent.connected, new ConnectionStateListener() { - @Override - public void onConnectionStateChanged(ConnectionStateChange state) { - threadContainer[0] = Thread.currentThread(); - } - }); - - ably.connection.connect(); - final ConnectionWaiter connectionWaiter = new ConnectionWaiter(ably.connection); - connectionWaiter.waitFor(ConnectionState.connected); - assertEquals("Verify connected state is reached", ConnectionState.connected, ably.connection.state); - - ExecutorService executor = Executors.newSingleThreadExecutor(); - executor.submit(new Runnable() { - public void run() { - try { - ably.close(); - connectionWaiter.waitFor(ConnectionState.closed); - assertEquals("Verify closed state is reached", ConnectionState.closed, ably.connection.state); - - /* wait for cm thread to exit */ - try { - Thread.sleep(2000L); - } catch(InterruptedException e) {} - - Thread.State cmThreadState = threadContainer[0].getState(); - assertEquals("Verify cm thread has exited", cmThreadState, Thread.State.TERMINATED); - } catch (Throwable t) { - t.printStackTrace(); - } - } - }); - } - - /** - * Connect and then verify that the connection manager has the default value for connectionStateTtl; - */ - @Test - public void connection_details_has_ttl() throws AblyException { - ClientOptions opts = createOptions(testVars.keys[0].keyStr); - try (final AblyRealtime ably = new AblyRealtime(opts)) { - final boolean[] callbackWasRun = new boolean[1]; - ably.connection.on(ConnectionEvent.connected, new ConnectionStateListener() { - @Override - public void onConnectionStateChanged(ConnectionStateChange state) { - synchronized(callbackWasRun) { - callbackWasRun[0] = true; - try { - Field field = ably.connection.connectionManager.getClass().getDeclaredField("connectionStateTtl"); - field.setAccessible(true); - assertEquals("Verify connectionStateTtl has the default value", field.get(ably.connection.connectionManager), 120000L); - } catch (NoSuchFieldException|IllegalAccessException e) { - fail("Unexpected exception in checking connectionStateTtl"); - } - callbackWasRun.notify(); - } - } - }); - - synchronized (callbackWasRun) { - try { callbackWasRun.wait(); } catch(InterruptedException ie) {} - assertTrue("Connected callback was not run", callbackWasRun[0]); - } - } - } - - /** - * RTN15g1, RTN15g2. Connect, disconnect, reconnect after (ttl + idle interval) period has passed, - * check that the connection is a new one; - */ - @Test - public void connection_has_new_id_when_reconnecting_after_statettl_plus_idleinterval_has_passed() throws AblyException { - ClientOptions opts = createOptions(testVars.keys[0].keyStr); - opts.realtimeRequestTimeout = 2000L; - try(final AblyRealtime ably = new AblyRealtime(opts)) { - final long newTtl = 1000L; - final long newIdleInterval = 1000L; - /* We want this greater than newTtl + newIdleInterval */ - final long waitInDisconnectedState = 3000L; - - ably.connection.on(ConnectionEvent.connected, new ConnectionStateListener() { - @Override - public void onConnectionStateChanged(ConnectionStateChange state) { - try { - Field connectionStateField = ably.connection.connectionManager.getClass().getDeclaredField("connectionStateTtl"); - connectionStateField.setAccessible(true); - connectionStateField.setLong(ably.connection.connectionManager, newTtl); - Field maxIdleField = ably.connection.connectionManager.getClass().getDeclaredField("maxIdleInterval"); - maxIdleField.setAccessible(true); - maxIdleField.setLong(ably.connection.connectionManager, newIdleInterval); - } catch (NoSuchFieldException | IllegalAccessException e) { - fail("Unexpected exception in checking connectionStateTtl"); - } - } - }); - - ConnectionWaiter connectionWaiter = new ConnectionWaiter(ably.connection); - connectionWaiter.waitFor(ConnectionState.connected); - final String firstConnectionId = ably.connection.id; - - /* suppress automatic retries by the connection manager and disconnect */ - try { - Method method = ably.connection.connectionManager.getClass().getDeclaredMethod("disconnectAndSuppressRetries"); - method.setAccessible(true); - method.invoke(ably.connection.connectionManager); - } catch (NoSuchMethodException | IllegalAccessException | InvocationTargetException e) { - fail("Unexpected exception in suppressing retries"); - } - connectionWaiter.waitFor(ConnectionState.disconnected); - assertEquals("Disconnected state was not reached", ConnectionState.disconnected, ably.connection.state); - - /* Wait for the connection to go stale, then reconnect */ - try { - Thread.sleep(waitInDisconnectedState); - } catch (InterruptedException e) { - } - ably.connection.connect(); - connectionWaiter.waitFor(ConnectionState.connected); - assertEquals("Connected state was not reached", ConnectionState.connected, ably.connection.state); - - /* Verify the connection is new */ - assertNotNull(ably.connection.id); - assertNotEquals("Connection has the same id", firstConnectionId, ably.connection.id); - } - } - - /** - * RTN15g1, RTN15g2. Connect, disconnect, reconnect before (ttl + idle interval) period has passed, - * check that the connection is the same; - */ - @Test - public void connection_has_same_id_when_reconnecting_before_statettl_plus_idleinterval_has_passed() throws AblyException { - ClientOptions opts = createOptions(testVars.keys[0].keyStr); - try(final AblyRealtime ably = new AblyRealtime(opts)) { - ConnectionWaiter connectionWaiter = new ConnectionWaiter(ably.connection); - connectionWaiter.waitFor(ConnectionState.connected); - String firstConnectionId = ably.connection.id; - ably.connection.connectionManager.requestState(ConnectionState.disconnected); - - /* Wait for a connected state after the disconnection triggered above */ - connectionWaiter.waitFor(ConnectionState.connected); - - String secondConnectionId = ably.connection.id; - assertNotNull(secondConnectionId); - assertEquals("connection has the same id", firstConnectionId, secondConnectionId); - } - } - - /** - * RTN15g3. Connect, attach some channels, disconnect, reconnect after (ttl + idle interval) period has passed, - * check that the client reconnects with a different connection and that the channels attached during the first - * connection are correctly reattached; - */ - @Test - public void channels_are_reattached_after_reconnecting_when_statettl_plus_idleinterval_has_passed() throws AblyException { - ClientOptions opts = createOptions(testVars.keys[0].keyStr); - try(final AblyRealtime ably = new AblyRealtime(opts)) { - final long newTtl = 1000L; - final long newIdleInterval = 1000L; - /* We want this greater than newTtl + newIdleInterval */ - final long waitInDisconnectedState = 3000L; - final List attachedChannelHistory = new ArrayList(); - final List expectedAttachedChannelHistory = Arrays.asList("attaching", "attached", "attaching", "attached"); - final List suspendedChannelHistory = new ArrayList(); - final List expectedSuspendedChannelHistory = Arrays.asList("attaching", "attached"); - ably.connection.on(ConnectionEvent.connected, new ConnectionStateListener() { - @Override - public void onConnectionStateChanged(ConnectionStateChange state) { - try { - Field connectionStateField = ably.connection.connectionManager.getClass().getDeclaredField("connectionStateTtl"); - connectionStateField.setAccessible(true); - connectionStateField.setLong(ably.connection.connectionManager, newTtl); - Field maxIdleField = ably.connection.connectionManager.getClass().getDeclaredField("maxIdleInterval"); - maxIdleField.setAccessible(true); - maxIdleField.setLong(ably.connection.connectionManager, newIdleInterval); - } catch (NoSuchFieldException | IllegalAccessException e) { - fail("Unexpected exception in checking connectionStateTtl"); - } - } - }); - - ConnectionWaiter connectionWaiter = new ConnectionWaiter(ably.connection); - connectionWaiter.waitFor(ConnectionState.connected); - final String firstConnectionId = ably.connection.id; - - /* Prepare channels */ - final Channel attachedChannel = ably.channels.get("test-reattach-after-ttl" + testParams.name); - ChannelWaiter attachedChannelWaiter = new Helpers.ChannelWaiter(attachedChannel); - attachedChannel.on(new ChannelStateListener() { - @Override - public void onChannelStateChanged(ChannelStateChange stateChange) { - attachedChannelHistory.add(stateChange.current.name()); - } - }); - final Channel suspendedChannel = ably.channels.get("test-reattach-suspended-after-ttl" + testParams.name); - suspendedChannel.state = ChannelState.suspended; - ChannelWaiter suspendedChannelWaiter = new Helpers.ChannelWaiter(suspendedChannel); - suspendedChannel.on(new ChannelStateListener() { - @Override - public void onChannelStateChanged(ChannelStateChange stateChange) { - suspendedChannelHistory.add(stateChange.current.name()); - } - }); - - /* attach first channel and wait for it to be attached */ - attachedChannel.attach(); - attachedChannelWaiter.waitFor(ChannelState.attached); - - /* suppress automatic retries by the connection manager and disconnect */ - try { - Method method = ably.connection.connectionManager.getClass().getDeclaredMethod("disconnectAndSuppressRetries"); - method.setAccessible(true); - method.invoke(ably.connection.connectionManager); - } catch (NoSuchMethodException | IllegalAccessException | InvocationTargetException e) { - fail("Unexpected exception in suppressing retries"); - } - connectionWaiter.waitFor(ConnectionState.disconnected); - assertEquals("Disconnected state was not reached", ConnectionState.disconnected, ably.connection.state); - - /* Wait for the connection to go stale, then reconnect */ - try { - Thread.sleep(waitInDisconnectedState); - } catch (InterruptedException e) { - } - ably.connection.connect(); - connectionWaiter.waitFor(ConnectionState.connected); - assertEquals("Connected state was not reached", ConnectionState.connected, ably.connection.state); - - /* Verify the connection is new */ - assertNotNull(ably.connection.id); - assertNotEquals("Connection has the same id", firstConnectionId, ably.connection.id); - - /* Verify that the attached channel is reattached with resumed false */ - attachedChannel.once(ChannelEvent.attached, new ChannelStateListener() { - @Override - public void onChannelStateChanged(ChannelStateChange stateChange) { - assertEquals("Resumed is true and should be false", stateChange.resumed, false); - } - }); - - /* Wait for both channels to reattach and verify state histories match the expected ones */ - attachedChannelWaiter.waitFor(ChannelState.attached); - suspendedChannelWaiter.waitFor(ChannelState.attached); - assertEquals("Attached channel histories do not match", attachedChannelHistory, expectedAttachedChannelHistory); - assertEquals("Suspended channel histories do not match", suspendedChannelHistory, expectedSuspendedChannelHistory); - } - } + @Rule + public Timeout testTimeout = Timeout.seconds(60); + + /** + *

+ * Verifies that ably connects to default host, + * when everything is fine. + *

+ * + * @throws AblyException + */ + @Test + public void connectionmanager_fallback_none() throws AblyException { + ClientOptions opts = createOptions(testVars.keys[0].keyStr); + try (AblyRealtime ably = new AblyRealtime(opts)) { + ConnectionManager connectionManager = ably.connection.connectionManager; + + new Helpers.ConnectionWaiter(ably.connection).waitFor(ConnectionState.connected); + + /* Verify that, + * - connectionManager is connected + * - connectionManager is connected to the host without any fallback + */ + assertThat(connectionManager.getConnectionState().state, is(ConnectionState.connected)); + assertThat(connectionManager.getHost(), is(equalTo(opts.environment + "-realtime.ably.io"))); + } + } + + /** + *

+ * Verifies that fallback behaviour doesn't apply, when the default + * custom endpoint is being used + *

+ *

+ * Spec: RTN17b + *

+ * + * @throws AblyException + */ + @Test + public void connectionmanager_fallback_none_customhost() throws AblyException { + ClientOptions opts = createOptions(testVars.keys[0].keyStr); + opts.realtimeHost = "un.reachable.host.example.com"; + opts.environment = null; + try(AblyRealtime ably = new AblyRealtime(opts)) { + ConnectionManager connectionManager = ably.connection.connectionManager; + + new Helpers.ConnectionWaiter(ably.connection).waitFor(ConnectionState.disconnected); + + /* Verify that, + * - connectionManager is disconnected + * - connectionManager's last host did not have any fallback + */ + assertThat(connectionManager.getConnectionState().state, is(ConnectionState.disconnected)); + assertThat(connectionManager.getHost(), is(equalTo(opts.realtimeHost))); + } + } + + /** + *

+ * Verifies that the {@code ConnectionManager} first checks if an internet connection is + * available by issuing a GET request to https://internet-up.ably-realtime.com/is-the-internet-up.txt + * , when In the case of an error necessitating use of an alternative host (see RTN17d). + *

+ *

+ * Spec: RTN17c + *

+ * + * @throws AblyException + */ + @Test + public void connectionmanager_fallback_none_withoutconnection() throws AblyException { + ClientOptions opts = createOptions(testVars.keys[0].keyStr); + opts.realtimeHost = "un.reachable.host"; + opts.environment = null; + opts.autoConnect = false; + try(AblyRealtime ably = new AblyRealtime(opts)) { + Connection connection = Mockito.mock(Connection.class); + final ConnectionManager.Channels channels = Mockito.mock(ConnectionManager.Channels.class); + + ConnectionManager connectionManager = new ConnectionManager(ably, connection, channels) { + @Override + protected boolean checkConnectivity() { + return false; + } + }; + + connectionManager.connect(); + + new Helpers.ConnectionManagerWaiter(connectionManager).waitFor(ConnectionState.disconnected); + + /* Verify that, + * - connectionManager is disconnected + * - connectionManager did not apply any fallback behavior + */ + assertThat(connectionManager.getConnectionState().state, is(ConnectionState.disconnected)); + assertThat(connectionManager.getHost(), is(equalTo(opts.realtimeHost))); + + connectionManager.close(); + } + } + + /** + *

+ * Verifies that fallback behaviour is applied and HTTP client is using same fallback + * endpoint, when the default realtime.ably.io endpoint is being used and has not been + * overriden, and a fallback is applied + *

+ *

+ * Spec: RTN17b, RTN17c + *

+ * + * @throws AblyException + */ + @Test + public void connectionmanager_default_fallback_applied() throws AblyException { + DebugOptions opts = new DebugOptions(testVars.keys[0].keyStr); + fillInOptions(opts); + + final Hosts hosts = new Hosts(null, Defaults.HOST_REALTIME, opts); + final String primaryHost = hosts.getPrimaryHost(); + + /* clear the environment override, so we trigger default fallback behaviour */ + opts.environment = null; + + /* set up mock transport */ + MockWebsocketFactory mockTransport = new MockWebsocketFactory(); + opts.transportFactory = mockTransport; + + /* ensure that all connection attempts ultimately resolve to the primary host */ + mockTransport.setHostTransform(new MockWebsocketFactory.HostTransform() { + @Override + public String transformHost(String givenHost) { + return primaryHost; + } + }); + + /* set up a filter on a mock transport to fail connections to the primary host */ + mockTransport.failConnect(new MockWebsocketFactory.HostFilter() { + @Override + public boolean matches(String hostname) { + return hostname.equals(primaryHost); + } + }); + + try (final AblyRealtime ably = new AblyRealtime(opts)) { + ConnectionManager connectionManager = ably.connection.connectionManager; + + new Helpers.ConnectionWaiter(ably.connection).waitFor(ConnectionState.connected); + + /* Verify that, + * - connectionManager is connected + * - connectionManager's last host was a fallback host + */ + assertThat(connectionManager.getConnectionState().state, is(ConnectionState.connected)); + assertThat(connectionManager.getHost(), is(not(equalTo(primaryHost)))); + } + } + + /** + * Verify that when environment is overridden, no fallback is used by default + * + *

+ * Spec: RTN17b + *

+ */ + @Test + public void connectionmanager_default_endpoint_no_fallback() throws AblyException { + DebugOptions opts = new DebugOptions(testVars.keys[0].keyStr); + fillInOptions(opts); + + final Hosts hosts = new Hosts(null, Defaults.HOST_REALTIME, opts); + final String primaryHost = hosts.getPrimaryHost(); + + MockWebsocketFactory mockTransport = new MockWebsocketFactory(); + opts.transportFactory = mockTransport; + + /* ensure that all connection attempts ultimately resolve to the primary host */ + mockTransport.setHostTransform(new MockWebsocketFactory.HostTransform() { + @Override + public String transformHost(String givenHost) { + return primaryHost; + } + }); + + /* set up a filter on a mock transport to fail connections to the primary host */ + mockTransport.failConnect(new MockWebsocketFactory.HostFilter() { + @Override + public boolean matches(String hostname) { + return hostname.equals(primaryHost); + } + }); + + try (final AblyRealtime ably = new AblyRealtime(opts)) { + ConnectionManager connectionManager = ably.connection.connectionManager; + + System.out.println("waiting for disconnected"); + new Helpers.ConnectionWaiter(ably.connection).waitFor(ConnectionState.disconnected); + System.out.println("got disconnected"); + + /* Verify that, + * - connectionManager is disconnected + * - connectionManager's last host was the primary host + */ + assertThat(connectionManager.getConnectionState().state, is(ConnectionState.disconnected)); + assertThat(connectionManager.getHost(), is(equalTo(primaryHost))); + } + } + + /** + * Verify that when environment is overridden and fallback specified, the fallback is used + * + *

+ * Spec: RTN17b + *

+ */ + @Test + public void connectionmanager_default_endpoint_explicit_fallback() throws AblyException { + DebugOptions opts = new DebugOptions(testVars.keys[0].keyStr); + fillInOptions(opts); + + final Hosts hosts = new Hosts(null, Defaults.HOST_REALTIME, opts); + final String primaryHost = hosts.getPrimaryHost(); + + opts.fallbackHosts = new String[]{"fallback 1", "fallback 2"}; + + MockWebsocketFactory mockTransport = new MockWebsocketFactory(); + opts.transportFactory = mockTransport; + + /* ensure that all connection attempts ultimately resolve to the primary host */ + mockTransport.setHostTransform(new MockWebsocketFactory.HostTransform() { + @Override + public String transformHost(String givenHost) { + return primaryHost; + } + }); + + /* set up a filter on a mock transport to fail connections to the primary host */ + mockTransport.failConnect(new MockWebsocketFactory.HostFilter() { + @Override + public boolean matches(String hostname) { + return hostname.equals(primaryHost); + } + }); + + try (final AblyRealtime ably = new AblyRealtime(opts)) { + ConnectionManager connectionManager = ably.connection.connectionManager; + + System.out.println("waiting for connected"); + new Helpers.ConnectionWaiter(ably.connection).waitFor(ConnectionState.connected); + System.out.println("got connected"); + + /* Verify that, + * - connectionManager is connected + * - connectionManager's last host was a fallback host + */ + assertThat(connectionManager.getConnectionState().state, is(ConnectionState.connected)); + assertThat(connectionManager.getHost(), is(not(equalTo(primaryHost)))); + } + } + + /** + * Test that default fallback happens with a non-default host if + * fallbackHostsUseDefault is set. + */ + @Test + public void connectionmanager_reconnect_default_fallback() throws AblyException { + DebugOptions opts = new DebugOptions(testVars.keys[0].keyStr); + fillInOptions(opts); + + opts.fallbackHostsUseDefault = true; + + final Hosts hosts = new Hosts(null, Defaults.HOST_REALTIME, opts); + final String primaryHost = hosts.getPrimaryHost(); + + MockWebsocketFactory mockTransport = new MockWebsocketFactory(); + opts.transportFactory = mockTransport; + + /* ensure that all connection attempts ultimately resolve to the primary host */ + mockTransport.setHostTransform(new MockWebsocketFactory.HostTransform() { + @Override + public String transformHost(String givenHost) { + return primaryHost; + } + }); + + /* set up a filter on a mock transport to fail connections to the primary host */ + mockTransport.failConnect(new MockWebsocketFactory.HostFilter() { + @Override + public boolean matches(String hostname) { + return hostname.equals(primaryHost); + } + }); + + try (final AblyRealtime ably = new AblyRealtime(opts)) { + ConnectionManager connectionManager = ably.connection.connectionManager; + + System.out.println("waiting for connected"); + new Helpers.ConnectionWaiter(ably.connection).waitFor(ConnectionState.connected); + System.out.println("got connected"); + ably.close(); + + /* Verify that, + * - connectionManager is connected + * - connectionManager's last host was a fallback host + */ + assertThat(connectionManager.getConnectionState().state, is(ConnectionState.connected)); + assertThat(connectionManager.getHost(), is(not(equalTo(opts.realtimeHost)))); + } + } + + /** + * Connect, and then perform a close() from the calling ConnectionManager context; + * verify that the closed state is reached, and the connectionmanager thread has exited + */ + @Test + public void close_from_connectionmanager() throws AblyException { + ClientOptions opts = createOptions(testVars.keys[0].keyStr); + final AblyRealtime ably = new AblyRealtime(opts); + final Thread[] threadContainer = new Thread[1]; + ably.connection.on(ConnectionEvent.connected, new ConnectionStateListener() { + @Override + public void onConnectionStateChanged(ConnectionStateChange state) { + ably.close(); + threadContainer[0] = Thread.currentThread(); + } + }); + + /* wait for cm thread to exit */ + try { + Thread.sleep(2000L); + } catch(InterruptedException e) {} + + assertEquals("Verify closed state is reached", ConnectionState.closed, ably.connection.state); + Thread.State cmThreadState = threadContainer[0].getState(); + assertEquals("Verify cm thread has exited", cmThreadState, Thread.State.TERMINATED); + } + + /** + * Connect, and then perform a close(); + * verify that the closed state is reached, and immediately + * reconnect; verify that it reconnects successfully + */ + @Test + public void connectionmanager_restart_race() throws AblyException { + ClientOptions opts = createOptions(testVars.keys[0].keyStr); + final AblyRealtime ably = new AblyRealtime(opts); + ConnectionWaiter connectionWaiter = new ConnectionWaiter(ably.connection); + + ably.connection.once(ConnectionEvent.connected, new ConnectionStateListener() { + @Override + public void onConnectionStateChanged(ConnectionStateChange state) { + ably.close(); + } + }); + + connectionWaiter.waitFor(ConnectionState.closed); + assertEquals("Verify closed state is reached", ConnectionState.closed, ably.connection.state); + connectionWaiter.reset(); + + /* reconnect */ + ably.connect(); + + /* verify the connection is reestablished */ + connectionWaiter.waitFor(ConnectionState.connected); + assertEquals("Verify connected state is reached", ConnectionState.connected, ably.connection.state); + + /* close the connection */ + ably.close(); + connectionWaiter.waitFor(ConnectionState.closed); + assertEquals("Verify closed state is reached", ConnectionState.closed, ably.connection.state); + } + + /** + * Connect, and then perform a close() from the calling ConnectionManager context; + * verify that the closed state is reached, and the connectionmanager thread has exited + */ + @Test + public void open_from_dedicated_thread() throws AblyException { + ClientOptions opts = createOptions(testVars.keys[0].keyStr); + opts.autoConnect = false; + final AblyRealtime ably = new AblyRealtime(opts); + final Thread[] threadContainer = new Thread[1]; + ably.connection.on(ConnectionEvent.connected, new ConnectionStateListener() { + @Override + public void onConnectionStateChanged(ConnectionStateChange state) { + threadContainer[0] = Thread.currentThread(); + } + }); + + ExecutorService executor = Executors.newSingleThreadExecutor(); + executor.submit(new Runnable() { + public void run() { + try { + ably.connection.connect(); + } catch (Throwable t) { + t.printStackTrace(); + } + } + }); + + ConnectionWaiter connectionWaiter = new ConnectionWaiter(ably.connection); + + connectionWaiter.waitFor(ConnectionState.connected); + assertEquals("Verify connected state is reached", ConnectionState.connected, ably.connection.state); + assertTrue("Not expecting token auth", ably.auth.getAuthMethod() == AuthMethod.basic); + + ably.close(); + connectionWaiter.waitFor(ConnectionState.closed); + assertEquals("Verify closed state is reached", ConnectionState.closed, ably.connection.state); + + /* wait for cm thread to exit */ + try { + Thread.sleep(2000L); + } catch(InterruptedException e) {} + + Thread.State cmThreadState = threadContainer[0].getState(); + assertEquals("Verify cm thread has exited", cmThreadState, Thread.State.TERMINATED); + } + + /** + * Connect, and then perform a close() from the calling ConnectionManager context; + * verify that the closed state is reached, and the connectionmanager thread has exited + */ + @Test + public void close_from_dedicated_thread() throws AblyException { + ClientOptions opts = createOptions(testVars.keys[0].keyStr); + opts.autoConnect = false; + final AblyRealtime ably = new AblyRealtime(opts); + final Thread[] threadContainer = new Thread[1]; + ably.connection.on(ConnectionEvent.connected, new ConnectionStateListener() { + @Override + public void onConnectionStateChanged(ConnectionStateChange state) { + threadContainer[0] = Thread.currentThread(); + } + }); + + ably.connection.connect(); + final ConnectionWaiter connectionWaiter = new ConnectionWaiter(ably.connection); + connectionWaiter.waitFor(ConnectionState.connected); + assertEquals("Verify connected state is reached", ConnectionState.connected, ably.connection.state); + + ExecutorService executor = Executors.newSingleThreadExecutor(); + executor.submit(new Runnable() { + public void run() { + try { + ably.close(); + connectionWaiter.waitFor(ConnectionState.closed); + assertEquals("Verify closed state is reached", ConnectionState.closed, ably.connection.state); + + /* wait for cm thread to exit */ + try { + Thread.sleep(2000L); + } catch(InterruptedException e) {} + + Thread.State cmThreadState = threadContainer[0].getState(); + assertEquals("Verify cm thread has exited", cmThreadState, Thread.State.TERMINATED); + } catch (Throwable t) { + t.printStackTrace(); + } + } + }); + } + + /** + * Connect and then verify that the connection manager has the default value for connectionStateTtl; + */ + @Test + public void connection_details_has_ttl() throws AblyException { + ClientOptions opts = createOptions(testVars.keys[0].keyStr); + try (final AblyRealtime ably = new AblyRealtime(opts)) { + final boolean[] callbackWasRun = new boolean[1]; + ably.connection.on(ConnectionEvent.connected, new ConnectionStateListener() { + @Override + public void onConnectionStateChanged(ConnectionStateChange state) { + synchronized(callbackWasRun) { + callbackWasRun[0] = true; + try { + Field field = ably.connection.connectionManager.getClass().getDeclaredField("connectionStateTtl"); + field.setAccessible(true); + assertEquals("Verify connectionStateTtl has the default value", field.get(ably.connection.connectionManager), 120000L); + } catch (NoSuchFieldException|IllegalAccessException e) { + fail("Unexpected exception in checking connectionStateTtl"); + } + callbackWasRun.notify(); + } + } + }); + + synchronized (callbackWasRun) { + try { callbackWasRun.wait(); } catch(InterruptedException ie) {} + assertTrue("Connected callback was not run", callbackWasRun[0]); + } + } + } + + /** + * RTN15g1, RTN15g2. Connect, disconnect, reconnect after (ttl + idle interval) period has passed, + * check that the connection is a new one; + */ + @Test + public void connection_has_new_id_when_reconnecting_after_statettl_plus_idleinterval_has_passed() throws AblyException { + ClientOptions opts = createOptions(testVars.keys[0].keyStr); + opts.realtimeRequestTimeout = 2000L; + try(final AblyRealtime ably = new AblyRealtime(opts)) { + final long newTtl = 1000L; + final long newIdleInterval = 1000L; + /* We want this greater than newTtl + newIdleInterval */ + final long waitInDisconnectedState = 3000L; + + ably.connection.on(ConnectionEvent.connected, new ConnectionStateListener() { + @Override + public void onConnectionStateChanged(ConnectionStateChange state) { + try { + Field connectionStateField = ably.connection.connectionManager.getClass().getDeclaredField("connectionStateTtl"); + connectionStateField.setAccessible(true); + connectionStateField.setLong(ably.connection.connectionManager, newTtl); + Field maxIdleField = ably.connection.connectionManager.getClass().getDeclaredField("maxIdleInterval"); + maxIdleField.setAccessible(true); + maxIdleField.setLong(ably.connection.connectionManager, newIdleInterval); + } catch (NoSuchFieldException | IllegalAccessException e) { + fail("Unexpected exception in checking connectionStateTtl"); + } + } + }); + + ConnectionWaiter connectionWaiter = new ConnectionWaiter(ably.connection); + connectionWaiter.waitFor(ConnectionState.connected); + final String firstConnectionId = ably.connection.id; + + /* suppress automatic retries by the connection manager and disconnect */ + try { + Method method = ably.connection.connectionManager.getClass().getDeclaredMethod("disconnectAndSuppressRetries"); + method.setAccessible(true); + method.invoke(ably.connection.connectionManager); + } catch (NoSuchMethodException | IllegalAccessException | InvocationTargetException e) { + fail("Unexpected exception in suppressing retries"); + } + connectionWaiter.waitFor(ConnectionState.disconnected); + assertEquals("Disconnected state was not reached", ConnectionState.disconnected, ably.connection.state); + + /* Wait for the connection to go stale, then reconnect */ + try { + Thread.sleep(waitInDisconnectedState); + } catch (InterruptedException e) { + } + ably.connection.connect(); + connectionWaiter.waitFor(ConnectionState.connected); + assertEquals("Connected state was not reached", ConnectionState.connected, ably.connection.state); + + /* Verify the connection is new */ + assertNotNull(ably.connection.id); + assertNotEquals("Connection has the same id", firstConnectionId, ably.connection.id); + } + } + + /** + * RTN15g1, RTN15g2. Connect, disconnect, reconnect before (ttl + idle interval) period has passed, + * check that the connection is the same; + */ + @Test + public void connection_has_same_id_when_reconnecting_before_statettl_plus_idleinterval_has_passed() throws AblyException { + ClientOptions opts = createOptions(testVars.keys[0].keyStr); + try(final AblyRealtime ably = new AblyRealtime(opts)) { + ConnectionWaiter connectionWaiter = new ConnectionWaiter(ably.connection); + connectionWaiter.waitFor(ConnectionState.connected); + String firstConnectionId = ably.connection.id; + ably.connection.connectionManager.requestState(ConnectionState.disconnected); + + /* Wait for a connected state after the disconnection triggered above */ + connectionWaiter.waitFor(ConnectionState.connected); + + String secondConnectionId = ably.connection.id; + assertNotNull(secondConnectionId); + assertEquals("connection has the same id", firstConnectionId, secondConnectionId); + } + } + + /** + * RTN15g3. Connect, attach some channels, disconnect, reconnect after (ttl + idle interval) period has passed, + * check that the client reconnects with a different connection and that the channels attached during the first + * connection are correctly reattached; + */ + @Test + public void channels_are_reattached_after_reconnecting_when_statettl_plus_idleinterval_has_passed() throws AblyException { + ClientOptions opts = createOptions(testVars.keys[0].keyStr); + try(final AblyRealtime ably = new AblyRealtime(opts)) { + final long newTtl = 1000L; + final long newIdleInterval = 1000L; + /* We want this greater than newTtl + newIdleInterval */ + final long waitInDisconnectedState = 3000L; + final List attachedChannelHistory = new ArrayList(); + final List expectedAttachedChannelHistory = Arrays.asList("attaching", "attached", "attaching", "attached"); + final List suspendedChannelHistory = new ArrayList(); + final List expectedSuspendedChannelHistory = Arrays.asList("attaching", "attached"); + ably.connection.on(ConnectionEvent.connected, new ConnectionStateListener() { + @Override + public void onConnectionStateChanged(ConnectionStateChange state) { + try { + Field connectionStateField = ably.connection.connectionManager.getClass().getDeclaredField("connectionStateTtl"); + connectionStateField.setAccessible(true); + connectionStateField.setLong(ably.connection.connectionManager, newTtl); + Field maxIdleField = ably.connection.connectionManager.getClass().getDeclaredField("maxIdleInterval"); + maxIdleField.setAccessible(true); + maxIdleField.setLong(ably.connection.connectionManager, newIdleInterval); + } catch (NoSuchFieldException | IllegalAccessException e) { + fail("Unexpected exception in checking connectionStateTtl"); + } + } + }); + + ConnectionWaiter connectionWaiter = new ConnectionWaiter(ably.connection); + connectionWaiter.waitFor(ConnectionState.connected); + final String firstConnectionId = ably.connection.id; + + /* Prepare channels */ + final Channel attachedChannel = ably.channels.get("test-reattach-after-ttl" + testParams.name); + ChannelWaiter attachedChannelWaiter = new Helpers.ChannelWaiter(attachedChannel); + attachedChannel.on(new ChannelStateListener() { + @Override + public void onChannelStateChanged(ChannelStateChange stateChange) { + attachedChannelHistory.add(stateChange.current.name()); + } + }); + final Channel suspendedChannel = ably.channels.get("test-reattach-suspended-after-ttl" + testParams.name); + suspendedChannel.state = ChannelState.suspended; + ChannelWaiter suspendedChannelWaiter = new Helpers.ChannelWaiter(suspendedChannel); + suspendedChannel.on(new ChannelStateListener() { + @Override + public void onChannelStateChanged(ChannelStateChange stateChange) { + suspendedChannelHistory.add(stateChange.current.name()); + } + }); + + /* attach first channel and wait for it to be attached */ + attachedChannel.attach(); + attachedChannelWaiter.waitFor(ChannelState.attached); + + /* suppress automatic retries by the connection manager and disconnect */ + try { + Method method = ably.connection.connectionManager.getClass().getDeclaredMethod("disconnectAndSuppressRetries"); + method.setAccessible(true); + method.invoke(ably.connection.connectionManager); + } catch (NoSuchMethodException | IllegalAccessException | InvocationTargetException e) { + fail("Unexpected exception in suppressing retries"); + } + connectionWaiter.waitFor(ConnectionState.disconnected); + assertEquals("Disconnected state was not reached", ConnectionState.disconnected, ably.connection.state); + + /* Wait for the connection to go stale, then reconnect */ + try { + Thread.sleep(waitInDisconnectedState); + } catch (InterruptedException e) { + } + ably.connection.connect(); + connectionWaiter.waitFor(ConnectionState.connected); + assertEquals("Connected state was not reached", ConnectionState.connected, ably.connection.state); + + /* Verify the connection is new */ + assertNotNull(ably.connection.id); + assertNotEquals("Connection has the same id", firstConnectionId, ably.connection.id); + + /* Verify that the attached channel is reattached with resumed false */ + attachedChannel.once(ChannelEvent.attached, new ChannelStateListener() { + @Override + public void onChannelStateChanged(ChannelStateChange stateChange) { + assertEquals("Resumed is true and should be false", stateChange.resumed, false); + } + }); + + /* Wait for both channels to reattach and verify state histories match the expected ones */ + attachedChannelWaiter.waitFor(ChannelState.attached); + suspendedChannelWaiter.waitFor(ChannelState.attached); + assertEquals("Attached channel histories do not match", attachedChannelHistory, expectedAttachedChannelHistory); + assertEquals("Suspended channel histories do not match", suspendedChannelHistory, expectedSuspendedChannelHistory); + } + } } diff --git a/lib/src/test/java/io/ably/lib/test/realtime/EventEmitterTest.java b/lib/src/test/java/io/ably/lib/test/realtime/EventEmitterTest.java index 511457d99..1f089addf 100644 --- a/lib/src/test/java/io/ably/lib/test/realtime/EventEmitterTest.java +++ b/lib/src/test/java/io/ably/lib/test/realtime/EventEmitterTest.java @@ -12,339 +12,339 @@ public class EventEmitterTest { - private static enum MyEvents { - event_0, - event_1 - } - - private static class MyEventPayload { - public MyEvents event; - public String message; - } - - private interface MyListener { - void onMyThingHappened(MyEventPayload theThing); - } - - private static class CountingListener implements MyListener { - @Override - public void onMyThingHappened(MyEventPayload theThing) { - Integer count = counts.get(theThing.event); if(count == null) count = 0; - counts.put(theThing.event, count + 1); - } - HashMap counts = new HashMap(); - } - - private static class MyEmitter extends EventEmitter { - @Override - protected void apply(MyListener listener, final MyEvents ev, final Object... args) { - listener.onMyThingHappened(new MyEventPayload() {{ event = ev; message = (String)args[0]; }}); - } - } - - /** - * Register a listener, and verify it is called - * when the event is emitted - */ - @Test - public void on_simple() { - MyEmitter emitter = new MyEmitter(); - emitter.on(new MyListener() { - @Override - public void onMyThingHappened(MyEventPayload theThing) { - assertEquals(theThing.event, MyEvents.event_0); - assertEquals(theThing.message, "on_simple"); - } - }); - emitter.emit(MyEvents.event_0, "on_simple"); - } - - /** - * Register a listener, and verify it is called - * when the event is emitted more than once - */ - @Test - public void on_multiple() { - MyEmitter emitter = new MyEmitter(); - CountingListener listener = new CountingListener(); - emitter.on(listener); - emitter.emit(MyEvents.event_0, "on_multiple_0"); - emitter.emit(MyEvents.event_0, "on_multiple_0"); - emitter.emit(MyEvents.event_1, "on_multiple_1"); - assertEquals(listener.counts.get(MyEvents.event_0), Integer.valueOf(2)); - assertEquals(listener.counts.get(MyEvents.event_1), Integer.valueOf(1)); - } - - /** - * Register and unregister listener, and verify it - * is not called when the event is emitted - */ - @Test - public void off_simple() { - MyEmitter emitter = new MyEmitter(); - CountingListener listener = new CountingListener(); - emitter.on(listener); - emitter.off(listener); - emitter.emit(MyEvents.event_0, "on_multiple_0"); - emitter.emit(MyEvents.event_1, "on_multiple_1"); - assertNull(listener.counts.get(MyEvents.event_0)); - assertNull(listener.counts.get(MyEvents.event_1)); - } - - /** - * Register and unregister multiple listeners, and verify they - * are not called when the event is emitted - */ - @Test - public void off_all() { - MyEmitter emitter = new MyEmitter(); - CountingListener listener1 = new CountingListener(); - CountingListener listener2 = new CountingListener(); - emitter.on(listener1); - emitter.on(listener2); - emitter.off(); - emitter.emit(MyEvents.event_0, "on_multiple_0"); - emitter.emit(MyEvents.event_1, "on_multiple_1"); - assertNull(listener1.counts.get(MyEvents.event_0)); - assertNull(listener1.counts.get(MyEvents.event_1)); - assertNull(listener2.counts.get(MyEvents.event_0)); - assertNull(listener2.counts.get(MyEvents.event_1)); - } - - /** - * Register a listener for a specific event, and verify it is called - * only when that event is emitted - */ - @Test - public void on_event_simple() { - MyEmitter emitter = new MyEmitter(); - CountingListener listener = new CountingListener(); - emitter.on(MyEvents.event_0, listener); - emitter.emit(MyEvents.event_0, "on_event_simple_0"); - emitter.emit(MyEvents.event_0, "on_event_simple_0"); - emitter.emit(MyEvents.event_1, "on_event_simple_1"); - assertEquals(listener.counts.get(MyEvents.event_0), Integer.valueOf(2)); - assertNull(listener.counts.get(MyEvents.event_1)); - } - - /** - * Register a listener for a specific event, and verify - * it is no longer called after it has been removed - */ - @Test - public void off_event_simple() { - MyEmitter emitter = new MyEmitter(); - CountingListener listener = new CountingListener(); - emitter.on(MyEvents.event_0, listener); - emitter.emit(MyEvents.event_0, "off_event_simple_0"); - emitter.emit(MyEvents.event_1, "off_event_simple_1"); - emitter.off(MyEvents.event_0, listener); - emitter.emit(MyEvents.event_0, "off_event_simple_0"); - assertEquals(listener.counts.get(MyEvents.event_0), Integer.valueOf(1)); - assertNull(listener.counts.get(MyEvents.event_1)); - } - - /** - * Register a "once" listener for a specific event, and - * verify it is called only once when that event is emitted - */ - @Test - public void once_event_simple() { - MyEmitter emitter = new MyEmitter(); - CountingListener listener = new CountingListener(); - emitter.once(MyEvents.event_0, listener); - emitter.emit(MyEvents.event_0, "once_event_simple_0"); - emitter.emit(MyEvents.event_0, "once_event_simple_0"); - emitter.emit(MyEvents.event_1, "once_event_simple_1"); - assertEquals(listener.counts.get(MyEvents.event_0), Integer.valueOf(1)); - assertNull(listener.counts.get(MyEvents.event_1)); - } - - /** - * Register a "once" listener for a specific event, then - * remove it, and verify it is not called when that event is emitted - */ - @Test - public void once_off_event_simple() { - MyEmitter emitter = new MyEmitter(); - CountingListener listener = new CountingListener(); - emitter.once(MyEvents.event_0, listener); - emitter.emit(MyEvents.event_1, "once_event_simple_1"); - emitter.off(MyEvents.event_0, listener); - emitter.emit(MyEvents.event_0, "once_event_simple_0"); - assertNull(listener.counts.get(MyEvents.event_0)); - assertNull(listener.counts.get(MyEvents.event_1)); - } - - /** - * Register event listeners inside the listener and ensure they are not called during that event. - */ - @Test - public void on_all_events_listener_in_listener() { - final MyEmitter emitter = new MyEmitter(); - - final CountingListener allEventsListener = new CountingListener(); - - emitter.on(MyEvents.event_0, new MyListener() { - @Override - public void onMyThingHappened(MyEventPayload theThing) { - //Add a listener here. - emitter.on(allEventsListener); - assertTrue(theThing.message.contains("fireEvent")); - - } - }); - - emitter.emit(MyEvents.event_0, "fireEvent"); - emitter.emit(MyEvents.event_0, "fireEvent"); - - assertEquals(allEventsListener.counts.get(MyEvents.event_0), Integer.valueOf(1)); - } - - /** - * Register event listener "once" inside the listener and ensure they are not called during that event. - */ - @Test - public void once_all_events_listener_in_listener() { - final MyEmitter emitter = new MyEmitter(); - - final CountingListener allEventsListener = new CountingListener(); - - final CountingListener event0Listener = new CountingListener() { - @Override - public void onMyThingHappened(MyEventPayload theThing) { - super.onMyThingHappened(theThing); - //Add a "once" listener in event0. We ensure it's added only once. - if ("event_0_first".equalsIgnoreCase(theThing.message)) { - emitter.once(allEventsListener); - } - - assertTrue(theThing.message.contains("event_0")); - } - }; - - emitter.on(MyEvents.event_0, event0Listener); - - emitter.emit(MyEvents.event_0, "event_0_first"); - //Let's fire "once" added listener - emitter.emit(MyEvents.event_0, "event_0_second"); - //We fire once again. - emitter.emit(MyEvents.event_0, "event_0_third"); - - assertEquals(allEventsListener.counts.get(MyEvents.event_0), Integer.valueOf(1)); - } - - /** - * Register event listener "once" inside the listener and ensure they are not called during that event. - */ - @Test - public void once_event_specific_listener_in_listener() { - final MyEmitter emitter = new MyEmitter(); - - final CountingListener event0Listener = new CountingListener(); - final CountingListener event1Listener = new CountingListener(); - - emitter.once(MyEvents.event_0, new MyListener() { - @Override - public void onMyThingHappened(MyEventPayload theThing) { - //Add a "once" listener here. - emitter.once(MyEvents.event_0, event0Listener); - emitter.once(MyEvents.event_1, event1Listener); - assertTrue(theThing.message.contains("fireEvent")); - } - }); - - emitter.emit(MyEvents.event_0, "fireEvent1"); - //Let's fire "once" added listener - emitter.emit(MyEvents.event_0, "fireEvent2"); - //Once listener should not called again. - emitter.emit(MyEvents.event_0, "fireEvent3"); - - assertEquals(event0Listener.counts.get(MyEvents.event_0), Integer.valueOf(1)); - assertNull(event1Listener.counts.get(MyEvents.event_1)); - } - - /** - * Register a specific event listener inside the listener and ensure they are not called during that event. - */ - @Test - public void on_specific_event_listener_in_listener() { - final MyEmitter emitter = new MyEmitter(); - - final CountingListener event0Listener = new CountingListener(); - final CountingListener event1Listener = new CountingListener(); - - emitter.on(MyEvents.event_0, new MyListener() { - @Override - public void onMyThingHappened(MyEventPayload theThing) { - emitter.on(MyEvents.event_0, event0Listener); - emitter.on(MyEvents.event_1, event1Listener); - assertTrue(theThing.message.contains("fireEvent")); - } - }); - - emitter.emit(MyEvents.event_0, "fireEvent"); - emitter.emit(MyEvents.event_0, "fireEvent"); - - assertEquals(event0Listener.counts.get(MyEvents.event_0), Integer.valueOf(1)); - assertNull(event1Listener.counts.get(MyEvents.event_1)); - } - - /** - * Remove "off" an all events listener inside the listener and ensure they are not called during that event. - */ - @Test - public void off_all_events_listener_in_listener() { - final MyEmitter emitter = new MyEmitter(); - - final CountingListener allEventsListener = new CountingListener(); - - //This is called once. - emitter.on(allEventsListener); - - emitter.on(MyEvents.event_0, new MyListener() { - @Override - public void onMyThingHappened(MyEventPayload theThing) { - //Turn off here. - emitter.off(allEventsListener); - assertTrue(theThing.message.contains("fireEvent")); - } - }); - - emitter.emit(MyEvents.event_0, "fireEvent"); - emitter.emit(MyEvents.event_0, "fireEvent"); - - assertEquals(allEventsListener.counts.get(MyEvents.event_0), Integer.valueOf(1)); - } - - /** - * "off" event specific listeners inside the listener and ensure they are not called during that event. - */ - - @Test - public void off_specific_event_listener_in_listener() { - final MyEmitter emitter = new MyEmitter(); - - final CountingListener event0Listener = new CountingListener(); - final CountingListener event1Listener = new CountingListener(); - - //This is called once. - emitter.on(MyEvents.event_0, event0Listener); - - emitter.on(MyEvents.event_0, new MyListener() { - @Override - public void onMyThingHappened(MyEventPayload theThing) { - //Turn off here. - emitter.off(MyEvents.event_0, event0Listener); - emitter.off(MyEvents.event_1, event1Listener); - assertTrue(theThing.message.contains("fireEvent")); - } - }); - - emitter.emit(MyEvents.event_0, "fireEvent"); - emitter.emit(MyEvents.event_0, "fireEvent"); - - assertEquals(event0Listener.counts.get(MyEvents.event_0), Integer.valueOf(1)); - assertNull(event1Listener.counts.get(MyEvents.event_1)); - } + private static enum MyEvents { + event_0, + event_1 + } + + private static class MyEventPayload { + public MyEvents event; + public String message; + } + + private interface MyListener { + void onMyThingHappened(MyEventPayload theThing); + } + + private static class CountingListener implements MyListener { + @Override + public void onMyThingHappened(MyEventPayload theThing) { + Integer count = counts.get(theThing.event); if(count == null) count = 0; + counts.put(theThing.event, count + 1); + } + HashMap counts = new HashMap(); + } + + private static class MyEmitter extends EventEmitter { + @Override + protected void apply(MyListener listener, final MyEvents ev, final Object... args) { + listener.onMyThingHappened(new MyEventPayload() {{ event = ev; message = (String)args[0]; }}); + } + } + + /** + * Register a listener, and verify it is called + * when the event is emitted + */ + @Test + public void on_simple() { + MyEmitter emitter = new MyEmitter(); + emitter.on(new MyListener() { + @Override + public void onMyThingHappened(MyEventPayload theThing) { + assertEquals(theThing.event, MyEvents.event_0); + assertEquals(theThing.message, "on_simple"); + } + }); + emitter.emit(MyEvents.event_0, "on_simple"); + } + + /** + * Register a listener, and verify it is called + * when the event is emitted more than once + */ + @Test + public void on_multiple() { + MyEmitter emitter = new MyEmitter(); + CountingListener listener = new CountingListener(); + emitter.on(listener); + emitter.emit(MyEvents.event_0, "on_multiple_0"); + emitter.emit(MyEvents.event_0, "on_multiple_0"); + emitter.emit(MyEvents.event_1, "on_multiple_1"); + assertEquals(listener.counts.get(MyEvents.event_0), Integer.valueOf(2)); + assertEquals(listener.counts.get(MyEvents.event_1), Integer.valueOf(1)); + } + + /** + * Register and unregister listener, and verify it + * is not called when the event is emitted + */ + @Test + public void off_simple() { + MyEmitter emitter = new MyEmitter(); + CountingListener listener = new CountingListener(); + emitter.on(listener); + emitter.off(listener); + emitter.emit(MyEvents.event_0, "on_multiple_0"); + emitter.emit(MyEvents.event_1, "on_multiple_1"); + assertNull(listener.counts.get(MyEvents.event_0)); + assertNull(listener.counts.get(MyEvents.event_1)); + } + + /** + * Register and unregister multiple listeners, and verify they + * are not called when the event is emitted + */ + @Test + public void off_all() { + MyEmitter emitter = new MyEmitter(); + CountingListener listener1 = new CountingListener(); + CountingListener listener2 = new CountingListener(); + emitter.on(listener1); + emitter.on(listener2); + emitter.off(); + emitter.emit(MyEvents.event_0, "on_multiple_0"); + emitter.emit(MyEvents.event_1, "on_multiple_1"); + assertNull(listener1.counts.get(MyEvents.event_0)); + assertNull(listener1.counts.get(MyEvents.event_1)); + assertNull(listener2.counts.get(MyEvents.event_0)); + assertNull(listener2.counts.get(MyEvents.event_1)); + } + + /** + * Register a listener for a specific event, and verify it is called + * only when that event is emitted + */ + @Test + public void on_event_simple() { + MyEmitter emitter = new MyEmitter(); + CountingListener listener = new CountingListener(); + emitter.on(MyEvents.event_0, listener); + emitter.emit(MyEvents.event_0, "on_event_simple_0"); + emitter.emit(MyEvents.event_0, "on_event_simple_0"); + emitter.emit(MyEvents.event_1, "on_event_simple_1"); + assertEquals(listener.counts.get(MyEvents.event_0), Integer.valueOf(2)); + assertNull(listener.counts.get(MyEvents.event_1)); + } + + /** + * Register a listener for a specific event, and verify + * it is no longer called after it has been removed + */ + @Test + public void off_event_simple() { + MyEmitter emitter = new MyEmitter(); + CountingListener listener = new CountingListener(); + emitter.on(MyEvents.event_0, listener); + emitter.emit(MyEvents.event_0, "off_event_simple_0"); + emitter.emit(MyEvents.event_1, "off_event_simple_1"); + emitter.off(MyEvents.event_0, listener); + emitter.emit(MyEvents.event_0, "off_event_simple_0"); + assertEquals(listener.counts.get(MyEvents.event_0), Integer.valueOf(1)); + assertNull(listener.counts.get(MyEvents.event_1)); + } + + /** + * Register a "once" listener for a specific event, and + * verify it is called only once when that event is emitted + */ + @Test + public void once_event_simple() { + MyEmitter emitter = new MyEmitter(); + CountingListener listener = new CountingListener(); + emitter.once(MyEvents.event_0, listener); + emitter.emit(MyEvents.event_0, "once_event_simple_0"); + emitter.emit(MyEvents.event_0, "once_event_simple_0"); + emitter.emit(MyEvents.event_1, "once_event_simple_1"); + assertEquals(listener.counts.get(MyEvents.event_0), Integer.valueOf(1)); + assertNull(listener.counts.get(MyEvents.event_1)); + } + + /** + * Register a "once" listener for a specific event, then + * remove it, and verify it is not called when that event is emitted + */ + @Test + public void once_off_event_simple() { + MyEmitter emitter = new MyEmitter(); + CountingListener listener = new CountingListener(); + emitter.once(MyEvents.event_0, listener); + emitter.emit(MyEvents.event_1, "once_event_simple_1"); + emitter.off(MyEvents.event_0, listener); + emitter.emit(MyEvents.event_0, "once_event_simple_0"); + assertNull(listener.counts.get(MyEvents.event_0)); + assertNull(listener.counts.get(MyEvents.event_1)); + } + + /** + * Register event listeners inside the listener and ensure they are not called during that event. + */ + @Test + public void on_all_events_listener_in_listener() { + final MyEmitter emitter = new MyEmitter(); + + final CountingListener allEventsListener = new CountingListener(); + + emitter.on(MyEvents.event_0, new MyListener() { + @Override + public void onMyThingHappened(MyEventPayload theThing) { + //Add a listener here. + emitter.on(allEventsListener); + assertTrue(theThing.message.contains("fireEvent")); + + } + }); + + emitter.emit(MyEvents.event_0, "fireEvent"); + emitter.emit(MyEvents.event_0, "fireEvent"); + + assertEquals(allEventsListener.counts.get(MyEvents.event_0), Integer.valueOf(1)); + } + + /** + * Register event listener "once" inside the listener and ensure they are not called during that event. + */ + @Test + public void once_all_events_listener_in_listener() { + final MyEmitter emitter = new MyEmitter(); + + final CountingListener allEventsListener = new CountingListener(); + + final CountingListener event0Listener = new CountingListener() { + @Override + public void onMyThingHappened(MyEventPayload theThing) { + super.onMyThingHappened(theThing); + //Add a "once" listener in event0. We ensure it's added only once. + if ("event_0_first".equalsIgnoreCase(theThing.message)) { + emitter.once(allEventsListener); + } + + assertTrue(theThing.message.contains("event_0")); + } + }; + + emitter.on(MyEvents.event_0, event0Listener); + + emitter.emit(MyEvents.event_0, "event_0_first"); + //Let's fire "once" added listener + emitter.emit(MyEvents.event_0, "event_0_second"); + //We fire once again. + emitter.emit(MyEvents.event_0, "event_0_third"); + + assertEquals(allEventsListener.counts.get(MyEvents.event_0), Integer.valueOf(1)); + } + + /** + * Register event listener "once" inside the listener and ensure they are not called during that event. + */ + @Test + public void once_event_specific_listener_in_listener() { + final MyEmitter emitter = new MyEmitter(); + + final CountingListener event0Listener = new CountingListener(); + final CountingListener event1Listener = new CountingListener(); + + emitter.once(MyEvents.event_0, new MyListener() { + @Override + public void onMyThingHappened(MyEventPayload theThing) { + //Add a "once" listener here. + emitter.once(MyEvents.event_0, event0Listener); + emitter.once(MyEvents.event_1, event1Listener); + assertTrue(theThing.message.contains("fireEvent")); + } + }); + + emitter.emit(MyEvents.event_0, "fireEvent1"); + //Let's fire "once" added listener + emitter.emit(MyEvents.event_0, "fireEvent2"); + //Once listener should not called again. + emitter.emit(MyEvents.event_0, "fireEvent3"); + + assertEquals(event0Listener.counts.get(MyEvents.event_0), Integer.valueOf(1)); + assertNull(event1Listener.counts.get(MyEvents.event_1)); + } + + /** + * Register a specific event listener inside the listener and ensure they are not called during that event. + */ + @Test + public void on_specific_event_listener_in_listener() { + final MyEmitter emitter = new MyEmitter(); + + final CountingListener event0Listener = new CountingListener(); + final CountingListener event1Listener = new CountingListener(); + + emitter.on(MyEvents.event_0, new MyListener() { + @Override + public void onMyThingHappened(MyEventPayload theThing) { + emitter.on(MyEvents.event_0, event0Listener); + emitter.on(MyEvents.event_1, event1Listener); + assertTrue(theThing.message.contains("fireEvent")); + } + }); + + emitter.emit(MyEvents.event_0, "fireEvent"); + emitter.emit(MyEvents.event_0, "fireEvent"); + + assertEquals(event0Listener.counts.get(MyEvents.event_0), Integer.valueOf(1)); + assertNull(event1Listener.counts.get(MyEvents.event_1)); + } + + /** + * Remove "off" an all events listener inside the listener and ensure they are not called during that event. + */ + @Test + public void off_all_events_listener_in_listener() { + final MyEmitter emitter = new MyEmitter(); + + final CountingListener allEventsListener = new CountingListener(); + + //This is called once. + emitter.on(allEventsListener); + + emitter.on(MyEvents.event_0, new MyListener() { + @Override + public void onMyThingHappened(MyEventPayload theThing) { + //Turn off here. + emitter.off(allEventsListener); + assertTrue(theThing.message.contains("fireEvent")); + } + }); + + emitter.emit(MyEvents.event_0, "fireEvent"); + emitter.emit(MyEvents.event_0, "fireEvent"); + + assertEquals(allEventsListener.counts.get(MyEvents.event_0), Integer.valueOf(1)); + } + + /** + * "off" event specific listeners inside the listener and ensure they are not called during that event. + */ + + @Test + public void off_specific_event_listener_in_listener() { + final MyEmitter emitter = new MyEmitter(); + + final CountingListener event0Listener = new CountingListener(); + final CountingListener event1Listener = new CountingListener(); + + //This is called once. + emitter.on(MyEvents.event_0, event0Listener); + + emitter.on(MyEvents.event_0, new MyListener() { + @Override + public void onMyThingHappened(MyEventPayload theThing) { + //Turn off here. + emitter.off(MyEvents.event_0, event0Listener); + emitter.off(MyEvents.event_1, event1Listener); + assertTrue(theThing.message.contains("fireEvent")); + } + }); + + emitter.emit(MyEvents.event_0, "fireEvent"); + emitter.emit(MyEvents.event_0, "fireEvent"); + + assertEquals(event0Listener.counts.get(MyEvents.event_0), Integer.valueOf(1)); + assertNull(event1Listener.counts.get(MyEvents.event_1)); + } } diff --git a/lib/src/test/java/io/ably/lib/test/realtime/HostsTest.java b/lib/src/test/java/io/ably/lib/test/realtime/HostsTest.java index bed858b2b..0dfd4e527 100644 --- a/lib/src/test/java/io/ably/lib/test/realtime/HostsTest.java +++ b/lib/src/test/java/io/ably/lib/test/realtime/HostsTest.java @@ -19,172 +19,172 @@ public class HostsTest { - /** - * Tests for Hosts class (fallback hosts). - */ - @Test - public void hosts_fallback() { - try { - ClientOptions options = new ClientOptions(); - Hosts hosts = new Hosts(Defaults.HOST_REALTIME, Defaults.HOST_REALTIME, options); - String host = hosts.getFallback(Defaults.HOST_REALTIME); - /* Expect given fallback host string is (relatively) valid */ - assertThat(host, is(not(isEmptyOrNullString()))); - /* Expect multiple calls will provide different (relatively) valid fallback hosts */ - String host2 = hosts.getFallback(host); - assertThat(host2, is(not(allOf(isEmptyOrNullString(), equalTo(host))))); - /* Expect a null, when we requested more than available fallback hosts */ - for (int i = Defaults.HOST_FALLBACKS.length - 1; i > 0; i--) { - assertThat(host2, is(not(equalTo(null)))); - host2 = hosts.getFallback(host2); - } - } catch (Exception e) { - fail("Unexpected exception " + e); - } - } + /** + * Tests for Hosts class (fallback hosts). + */ + @Test + public void hosts_fallback() { + try { + ClientOptions options = new ClientOptions(); + Hosts hosts = new Hosts(Defaults.HOST_REALTIME, Defaults.HOST_REALTIME, options); + String host = hosts.getFallback(Defaults.HOST_REALTIME); + /* Expect given fallback host string is (relatively) valid */ + assertThat(host, is(not(isEmptyOrNullString()))); + /* Expect multiple calls will provide different (relatively) valid fallback hosts */ + String host2 = hosts.getFallback(host); + assertThat(host2, is(not(allOf(isEmptyOrNullString(), equalTo(host))))); + /* Expect a null, when we requested more than available fallback hosts */ + for (int i = Defaults.HOST_FALLBACKS.length - 1; i > 0; i--) { + assertThat(host2, is(not(equalTo(null)))); + host2 = hosts.getFallback(host2); + } + } catch (Exception e) { + fail("Unexpected exception " + e); + } + } - /** - * Expect an exception when setting both realtimeHost and environment. - */ - @Test - public void hosts_host_and_environment() { - try { - ClientOptions options = new ClientOptions(); - options.environment = "myenv"; - Hosts hosts = new Hosts(Defaults.HOST_REALTIME, Defaults.HOST_REALTIME, options); - fail("Expected exception from setting realtimeHost and environment"); - } catch (Exception e) { - /* pass */ - } - } + /** + * Expect an exception when setting both realtimeHost and environment. + */ + @Test + public void hosts_host_and_environment() { + try { + ClientOptions options = new ClientOptions(); + options.environment = "myenv"; + Hosts hosts = new Hosts(Defaults.HOST_REALTIME, Defaults.HOST_REALTIME, options); + fail("Expected exception from setting realtimeHost and environment"); + } catch (Exception e) { + /* pass */ + } + } - /** - * Expect a null, when we provide empty array of fallback hosts - */ - @Test - public void hosts_fallback_empty_array() { - try { - ClientOptions options = new ClientOptions(); - String[] fallbackHosts = {}; - options.fallbackHosts = fallbackHosts; - Hosts hosts = new Hosts(Defaults.HOST_REALTIME, Defaults.HOST_REALTIME, options); - String host = hosts.getFallback(Defaults.HOST_REALTIME); - assertThat(host, is(equalTo(null))); - } catch (Exception e) { - fail("Unexpected exception " + e); - } - } + /** + * Expect a null, when we provide empty array of fallback hosts + */ + @Test + public void hosts_fallback_empty_array() { + try { + ClientOptions options = new ClientOptions(); + String[] fallbackHosts = {}; + options.fallbackHosts = fallbackHosts; + Hosts hosts = new Hosts(Defaults.HOST_REALTIME, Defaults.HOST_REALTIME, options); + String host = hosts.getFallback(Defaults.HOST_REALTIME); + assertThat(host, is(equalTo(null))); + } catch (Exception e) { + fail("Unexpected exception " + e); + } + } - /** - * Expect that returned host is contained within custom host list in options, - * but in shuffled order and the right number of them. - */ - @Test - public void hosts_fallback_custom_hosts() { - try { - ClientOptions options = new ClientOptions(); - String[] customHosts = { "F.ably-realtime.com", "G.ably-realtime.com", "H.ably-realtime.com", "I.ably-realtime.com", "J.ably-realtime.com", "K.ably-realtime.com" }; - options.fallbackHosts = customHosts; - Hosts hosts = new Hosts(Defaults.HOST_REALTIME, Defaults.HOST_REALTIME, options); - int idx; - String host = Defaults.HOST_REALTIME; - boolean shuffled = false; - boolean allFound = true; - for (idx = 0; ; ++idx) { - host = hosts.getFallback(host); - if (host == null) - break; - int found = Arrays.asList(customHosts).indexOf(host); - if (found < 0) - allFound = false; - else if (found != idx) - shuffled = true; - } - assertTrue(idx == customHosts.length); - assertTrue(allFound); - assertTrue(shuffled); - } catch (Exception e) { - fail("Unexpected exception " + e); - } - } + /** + * Expect that returned host is contained within custom host list in options, + * but in shuffled order and the right number of them. + */ + @Test + public void hosts_fallback_custom_hosts() { + try { + ClientOptions options = new ClientOptions(); + String[] customHosts = { "F.ably-realtime.com", "G.ably-realtime.com", "H.ably-realtime.com", "I.ably-realtime.com", "J.ably-realtime.com", "K.ably-realtime.com" }; + options.fallbackHosts = customHosts; + Hosts hosts = new Hosts(Defaults.HOST_REALTIME, Defaults.HOST_REALTIME, options); + int idx; + String host = Defaults.HOST_REALTIME; + boolean shuffled = false; + boolean allFound = true; + for (idx = 0; ; ++idx) { + host = hosts.getFallback(host); + if (host == null) + break; + int found = Arrays.asList(customHosts).indexOf(host); + if (found < 0) + allFound = false; + else if (found != idx) + shuffled = true; + } + assertTrue(idx == customHosts.length); + assertTrue(allFound); + assertTrue(shuffled); + } catch (Exception e) { + fail("Unexpected exception " + e); + } + } - /** - * Expect that returned host is contained within default host list - */ - @Test - public void hosts_fallback_no_custom_hosts(){ - try { - ClientOptions options = new ClientOptions(); - Hosts hosts = new Hosts(Defaults.HOST_REALTIME, Defaults.HOST_REALTIME, options); - int idx; - String host = Defaults.HOST_REALTIME; - boolean shuffled = false; - boolean allFound = true; - for (idx = 0; ; ++idx) { - host = hosts.getFallback(host); - if (host == null) - break; - int found = Arrays.asList(Defaults.HOST_FALLBACKS).indexOf(host); - if (found < 0) - allFound = false; - else if (found != idx) - shuffled = true; - } - assertTrue(idx == Defaults.HOST_FALLBACKS.length); - assertTrue(allFound); - assertTrue(shuffled); - } catch (Exception e) { - fail("Unexpected exception " + e); - } - } + /** + * Expect that returned host is contained within default host list + */ + @Test + public void hosts_fallback_no_custom_hosts(){ + try { + ClientOptions options = new ClientOptions(); + Hosts hosts = new Hosts(Defaults.HOST_REALTIME, Defaults.HOST_REALTIME, options); + int idx; + String host = Defaults.HOST_REALTIME; + boolean shuffled = false; + boolean allFound = true; + for (idx = 0; ; ++idx) { + host = hosts.getFallback(host); + if (host == null) + break; + int found = Arrays.asList(Defaults.HOST_FALLBACKS).indexOf(host); + if (found < 0) + allFound = false; + else if (found != idx) + shuffled = true; + } + assertTrue(idx == Defaults.HOST_FALLBACKS.length); + assertTrue(allFound); + assertTrue(shuffled); + } catch (Exception e) { + fail("Unexpected exception " + e); + } + } - /** - * Expect a null, when realtimeHost is non-default - */ - @Test - public void hosts_fallback_overridden_host() { - try { - ClientOptions options = new ClientOptions(); - String host = "overridden.ably.io"; - Hosts hosts = new Hosts(host, Defaults.HOST_REALTIME, options); - host = hosts.getFallback(host); - assertThat(host, is(equalTo(null))); - } catch (Exception e) { - fail("Unexpected exception " + e); - } - } + /** + * Expect a null, when realtimeHost is non-default + */ + @Test + public void hosts_fallback_overridden_host() { + try { + ClientOptions options = new ClientOptions(); + String host = "overridden.ably.io"; + Hosts hosts = new Hosts(host, Defaults.HOST_REALTIME, options); + host = hosts.getFallback(host); + assertThat(host, is(equalTo(null))); + } catch (Exception e) { + fail("Unexpected exception " + e); + } + } - /** - * Expect that returned host is contained within default host list - * when realtimeHost is non-default and fallbackHostsUseDefault is set - */ - @Test - public void hosts_fallback_use_default(){ - try { - ClientOptions options = new ClientOptions(); - options.fallbackHostsUseDefault = true; - String host = "overridden.ably.io"; - Hosts hosts = new Hosts(host, Defaults.HOST_REALTIME, options); - int idx; - boolean shuffled = false; - boolean allFound = true; - for (idx = 0; ; ++idx) { - host = hosts.getFallback(host); - if (host == null) - break; - int found = Arrays.asList(Defaults.HOST_FALLBACKS).indexOf(host); - if (found < 0) - allFound = false; - else if (found != idx) - shuffled = true; - } - assertTrue(idx == Defaults.HOST_FALLBACKS.length); - assertTrue(allFound); - assertTrue(shuffled); - } catch (Exception e) { - fail("Unexpected exception " + e); - } - } + /** + * Expect that returned host is contained within default host list + * when realtimeHost is non-default and fallbackHostsUseDefault is set + */ + @Test + public void hosts_fallback_use_default(){ + try { + ClientOptions options = new ClientOptions(); + options.fallbackHostsUseDefault = true; + String host = "overridden.ably.io"; + Hosts hosts = new Hosts(host, Defaults.HOST_REALTIME, options); + int idx; + boolean shuffled = false; + boolean allFound = true; + for (idx = 0; ; ++idx) { + host = hosts.getFallback(host); + if (host == null) + break; + int found = Arrays.asList(Defaults.HOST_FALLBACKS).indexOf(host); + if (found < 0) + allFound = false; + else if (found != idx) + shuffled = true; + } + assertTrue(idx == Defaults.HOST_FALLBACKS.length); + assertTrue(allFound); + assertTrue(shuffled); + } catch (Exception e) { + fail("Unexpected exception " + e); + } + } diff --git a/lib/src/test/java/io/ably/lib/test/realtime/RealtimeAuthTest.java b/lib/src/test/java/io/ably/lib/test/realtime/RealtimeAuthTest.java index 579168447..d46357112 100644 --- a/lib/src/test/java/io/ably/lib/test/realtime/RealtimeAuthTest.java +++ b/lib/src/test/java/io/ably/lib/test/realtime/RealtimeAuthTest.java @@ -26,840 +26,840 @@ public class RealtimeAuthTest extends ParameterizedTest { - @Rule - public Timeout testTimeout = Timeout.seconds(30); - - /** - * RSA12a: The clientId attribute of a TokenRequest or TokenDetails - * used for authentication is null, or ConnectionDetails#clientId is null - * following a connection to Ably. In this case, the null value indicates - * that a clientId identity may not be assumed by this client i.e. the - * client is anonymous for all operations - * - * Verify null token clientId in TokenDetails translates to a null clientId - */ - @Test - public void auth_client_match_tokendetails_null_clientId() { - try { - /* init ably for token */ - ClientOptions optsForToken = createOptions(testVars.keys[0].keyStr); - final AblyRest ablyForToken = new AblyRest(optsForToken); - - /* get token */ - Auth.TokenParams tokenParams = new Auth.TokenParams(); - tokenParams.clientId = null; - Auth.TokenDetails tokenDetails = ablyForToken.auth.requestToken(tokenParams, null); - assertNotNull("Expected token value", tokenDetails.token); - - /* create ably realtime with tokenDetails and clientId */ - ClientOptions opts = createOptions(); - opts.clientId = null; - opts.tokenDetails = tokenDetails; - AblyRealtime ablyRealtime = new AblyRealtime(opts); - System.out.println("done create ably"); - - /* wait for connected state */ - Helpers.ConnectionWaiter connectionWaiter = new Helpers.ConnectionWaiter(ablyRealtime.connection); - connectionWaiter.waitFor(ConnectionState.connected); - assertEquals("Verify connected state is reached", ConnectionState.connected, ablyRealtime.connection.state); - - /* check expected clientId */ - assertEquals("Auth#clientId is expected to be null", null, ablyRealtime.auth.clientId); - - ablyRealtime.close(); - } catch (AblyException e) { - e.printStackTrace(); - fail(); - } - } - - /** - * RSA12a: The clientId attribute of a TokenRequest or TokenDetails - * used for authentication is null, or ConnectionDetails#clientId is null - * following a connection to Ably. In this case, the null value indicates - * that a clientId identity may not be assumed by this client i.e. the - * client is anonymous for all operations - * - * Verify null token clientId in token translates to a null clientId - */ - @Test - public void auth_client_match_token_null_clientId() { - try { - /* init ably for token */ - ClientOptions optsForToken = createOptions(testVars.keys[0].keyStr); - final AblyRest ablyForToken = new AblyRest(optsForToken); - - /* get token */ - Auth.TokenParams tokenParams = new Auth.TokenParams(); - tokenParams.clientId = null; - Auth.TokenDetails tokenDetails = ablyForToken.auth.requestToken(tokenParams, null); - assertNotNull("Expected token value", tokenDetails.token); - - /* create ably realtime with tokenDetails and clientId */ - ClientOptions opts = createOptions(); - opts.clientId = null; - opts.token = tokenDetails.token; - AblyRealtime ablyRealtime = new AblyRealtime(opts); - System.out.println("done create ably"); - - /* wait for connected state */ - Helpers.ConnectionWaiter connectionWaiter = new Helpers.ConnectionWaiter(ablyRealtime.connection); - connectionWaiter.waitFor(ConnectionState.connected); - assertEquals("Verify connected state is reached", ConnectionState.connected, ablyRealtime.connection.state); - - /* check expected clientId */ - assertEquals("Auth#clientId is expected to be null", null, ablyRealtime.auth.clientId); - - ablyRealtime.close(); - } catch (AblyException e) { - e.printStackTrace(); - fail(); - } - } - - /** - * Init library with a key and token; verify Auth.clientId is null before - * connection - * Spec: RSA12b, RSA7b2, RSA7b3, RTC4a - */ - @Test - public void auth_clientid_null_before_auth() { - try { - final String clientId = "token clientId"; - - /* create token with clientId */ - ClientOptions optsForToken = createOptions(testVars.keys[0].keyStr); - optsForToken.clientId = clientId; - AblyRest ablyForToken = new AblyRest(optsForToken); - TokenDetails tokenDetails = ablyForToken.auth.requestToken(null, null); - - /* create ably realtime */ - ClientOptions opts = createOptions(); - opts.clientId = null; - opts.token = tokenDetails.token; - opts.autoConnect = false; - AblyRealtime ablyRealtime = new AblyRealtime(opts); - - /* check expected clientId */ - assertEquals("Auth#clientId is expected to be null", null, ablyRealtime.auth.clientId); - - /* wait for connected state */ - ablyRealtime.connection.connect(); - Helpers.ConnectionWaiter connectionWaiter = new Helpers.ConnectionWaiter(ablyRealtime.connection); - connectionWaiter.waitFor(ConnectionState.connected); - assertEquals("Verify connected state is reached", ConnectionState.connected, ablyRealtime.connection.state); - - /* check expected clientId */ - assertEquals("Auth#clientId is expected to be set", clientId, ablyRealtime.auth.clientId); - - ablyRealtime.close(); - } catch (AblyException e) { - e.printStackTrace(); - fail(); - } - } - - /** - * RSA15a: Any clientId provided in ClientOptions must match any - * non wildcard ('*') clientId value in TokenDetails - * RSA15b: If the clientId from TokenDetails or connectionDetails contains - * only a wildcard string '*', then the client is permitted to be either - * unidentified or identified by providing - * a clientId when communicating with Ably - * - * Verify wildcard token clientId in TokenDetails succeeds in - * authenticating a non-null clientId - */ - @Test - public void auth_client_match_tokendetails_wildcard_clientId() { - try { - /* init ably for token */ - ClientOptions optsForToken = createOptions(testVars.keys[0].keyStr); - final AblyRest ablyForToken = new AblyRest(optsForToken); - - /* get token */ - Auth.TokenParams tokenParams = new Auth.TokenParams(); - tokenParams.clientId = "*"; - Auth.TokenDetails tokenDetails = ablyForToken.auth.requestToken(tokenParams, null); - assertNotNull("Expected token value", tokenDetails.token); - - /* create ably realtime with tokenDetails and clientId */ - ClientOptions opts = createOptions(); - opts.clientId = "options clientId"; - opts.tokenDetails = tokenDetails; - AblyRealtime ablyRealtime = new AblyRealtime(opts); - System.out.println("done create ably"); - - /* wait for connected state */ - Helpers.ConnectionWaiter connectionWaiter = new Helpers.ConnectionWaiter(ablyRealtime.connection); - connectionWaiter.waitFor(ConnectionState.connected); - assertEquals("Verify connected state is reached", ConnectionState.connected, ablyRealtime.connection.state); - - /* check expected clientId */ - assertEquals("Auth#clientId is expected to be set", "options clientId", ablyRealtime.auth.clientId); - - ablyRealtime.close(); - } catch (AblyException e) { - e.printStackTrace(); - fail(); - } - } - - /** - * RSA15a: Any clientId provided in ClientOptions must match any - * non wildcard ('*') clientId value in TokenDetails - * RSA15b: If the clientId from TokenDetails or connectionDetails contains - * only a wildcard string '*', then the client is permitted to be either - * unidentified (i.e. authorised to act on behalf of any clientId) or - * identified by providing a clientId when communicating with Ably - * - * Verify wildcard token clientId in token succeeds in - * authenticating a non-null clientId - */ - @Test - public void auth_client_match_token_wildcard_clientId() { - try { - /* init ably for token */ - ClientOptions optsForToken = createOptions(testVars.keys[0].keyStr); - final AblyRest ablyForToken = new AblyRest(optsForToken); - - /* get token */ - Auth.TokenParams tokenParams = new Auth.TokenParams(); - tokenParams.clientId = "*"; - Auth.TokenDetails tokenDetails = ablyForToken.auth.requestToken(tokenParams, null); - assertNotNull("Expected token value", tokenDetails.token); - - /* create ably realtime with token and clientId */ - ClientOptions opts = createOptions(); - opts.clientId = "options clientId"; - opts.token = tokenDetails.token; - AblyRealtime ablyRealtime = new AblyRealtime(opts); - System.out.println("done create ably"); - - /* wait for connected state */ - Helpers.ConnectionWaiter connectionWaiter = new Helpers.ConnectionWaiter(ablyRealtime.connection); - connectionWaiter.waitFor(ConnectionState.connected); - assertEquals("Verify connected state is reached", ConnectionState.connected, ablyRealtime.connection.state); - - /* check expected clientId */ - assertEquals("Auth#clientId is expected to be set", "options clientId", ablyRealtime.auth.clientId); - - ablyRealtime.close(); - } catch (AblyException e) { - e.printStackTrace(); - fail(); - } - } - - /** - * RSA15b: If the clientId from TokenDetails or connectionDetails contains - * only a wildcard string '*', then the client is permitted to be either - * unidentified (i.e. authorised to act on behalf of any clientId) or - * identified by providing a clientId when communicating with Ably - * - * Verify wildcard token clientId in TokenDetails succeeds in - * authenticating a null clientId - */ - @Test - public void auth_client_null_match_tokendetails_wildcard_clientId() { - try { - /* init ably for token */ - ClientOptions optsForToken = createOptions(testVars.keys[0].keyStr); - final AblyRest ablyForToken = new AblyRest(optsForToken); - - /* get token */ - Auth.TokenParams tokenParams = new Auth.TokenParams(); - tokenParams.clientId = "*"; - Auth.TokenDetails tokenDetails = ablyForToken.auth.requestToken(tokenParams, null); - assertNotNull("Expected token value", tokenDetails.token); - - /* create ably realtime with tokenDetails and clientId */ - ClientOptions opts = createOptions(); - opts.clientId = null; - opts.tokenDetails = tokenDetails; - AblyRealtime ablyRealtime = new AblyRealtime(opts); - System.out.println("done create ably"); - - /* wait for connected state */ - Helpers.ConnectionWaiter connectionWaiter = new Helpers.ConnectionWaiter(ablyRealtime.connection); - connectionWaiter.waitFor(ConnectionState.connected); - assertEquals("Verify connected state is reached", ConnectionState.connected, ablyRealtime.connection.state); - - /* check expected clientId */ - assertEquals("Auth#clientId is expected to be set", "*", ablyRealtime.auth.clientId); - - ablyRealtime.close(); - } catch (AblyException e) { - e.printStackTrace(); - fail(); - } - } - - /** - * RSA15b: If the clientId from TokenDetails or connectionDetails contains - * only a wildcard string '*', then the client is permitted to be either - * unidentified (i.e. authorised to act on behalf of any clientId) or - * identified by providing a clientId when communicating with Ably - * - * Verify wildcard token clientId in token succeeds in - * authenticating a null clientId - */ - @Test - public void auth_client_null_match_token_wildcard_clientId() { - try { - /* init ably for token */ - ClientOptions optsForToken = createOptions(testVars.keys[0].keyStr); - final AblyRest ablyForToken = new AblyRest(optsForToken); - - /* get token */ - Auth.TokenParams tokenParams = new Auth.TokenParams(); - tokenParams.clientId = "*"; - Auth.TokenDetails tokenDetails = ablyForToken.auth.requestToken(tokenParams, null); - assertNotNull("Expected token value", tokenDetails.token); - - /* create ably realtime with token and clientId */ - ClientOptions opts = createOptions(); - opts.clientId = null; - opts.token = tokenDetails.token; - AblyRealtime ablyRealtime = new AblyRealtime(opts); - System.out.println("done create ably"); - - /* wait for connected state */ - Helpers.ConnectionWaiter connectionWaiter = new Helpers.ConnectionWaiter(ablyRealtime.connection); - connectionWaiter.waitFor(ConnectionState.connected); - assertEquals("Verify connected state is reached", ConnectionState.connected, ablyRealtime.connection.state); - - /* check expected clientId */ - assertEquals("Auth#clientId is expected to be set", "*", ablyRealtime.auth.clientId); - - ablyRealtime.close(); - } catch (AblyException e) { - e.printStackTrace(); - fail(); - } - } - - /** - * RSA15a: Any clientId provided in ClientOptions must match any - * non wildcard ('*') clientId value in TokenDetails - * - * Verify matching token clientId in TokenDetails succeeds - * in authenticating a non-null clientId - */ - @Test - public void auth_client_match_tokendetails_clientId() { - try { - /* init ably for token */ - ClientOptions optsForToken = createOptions(testVars.keys[0].keyStr); - final AblyRest ablyForToken = new AblyRest(optsForToken); - - /* get token */ - Auth.TokenParams tokenParams = new Auth.TokenParams(); - tokenParams.clientId = "options clientId"; - Auth.TokenDetails tokenDetails = ablyForToken.auth.requestToken(tokenParams, null); - assertNotNull("Expected token value", tokenDetails.token); - - /* create ably realtime with tokenDetails and clientId */ - ClientOptions opts = createOptions(); - opts.clientId = "options clientId"; - opts.tokenDetails = tokenDetails; - AblyRealtime ablyRealtime = new AblyRealtime(opts); - - /* wait for connected state */ - Helpers.ConnectionWaiter connectionWaiter = new Helpers.ConnectionWaiter(ablyRealtime.connection); - connectionWaiter.waitFor(ConnectionState.connected); - assertEquals("Verify connected state is reached", ConnectionState.connected, ablyRealtime.connection.state); - - /* check expected clientId */ - assertEquals("Auth#clientId is expected to be set", "options clientId", ablyRealtime.auth.clientId); - - ablyRealtime.close(); - } catch (AblyException e) { - e.printStackTrace(); - fail(); - } - } - - /** - * RSA15a: Any clientId provided in ClientOptions must match any - * non wildcard ('*') clientId value in TokenDetails - * in authenticating a non-null clientId - * - * Verify matching token clientId in token succeeds - */ - @Test - public void auth_client_match_token_clientId() { - try { - /* init ably for token */ - ClientOptions optsForToken = createOptions(testVars.keys[0].keyStr); - final AblyRest ablyForToken = new AblyRest(optsForToken); - - /* get token */ - Auth.TokenParams tokenParams = new Auth.TokenParams(); - tokenParams.clientId = "options clientId"; - Auth.TokenDetails tokenDetails = ablyForToken.auth.requestToken(tokenParams, null); - assertNotNull("Expected token value", tokenDetails.token); - - /* create ably realtime with token and clientId */ - ClientOptions opts = createOptions(); - opts.clientId = "options clientId"; - opts.token = tokenDetails.token; - AblyRealtime ablyRealtime = new AblyRealtime(opts); - - /* wait for connected state */ - Helpers.ConnectionWaiter connectionWaiter = new Helpers.ConnectionWaiter(ablyRealtime.connection); - connectionWaiter.waitFor(ConnectionState.connected); - assertEquals("Verify connected state is reached", ConnectionState.connected, ablyRealtime.connection.state); - - /* check expected clientId */ - assertEquals("Auth#clientId is expected to be set", "options clientId", ablyRealtime.auth.clientId); - - ablyRealtime.close(); - } catch (AblyException e) { - e.printStackTrace(); - fail(); - } - } - - /** - * RSA15a: Any clientId provided in ClientOptions must match any - * non wildcard ('*') clientId value in TokenDetails - * Verify non-matching token clientId fails to authenticate a non-null clientId - * RSA15c: Following an auth request which uses a TokenDetails or TokenRequest - * object that contains an incompatible clientId, the library should ... transition - * the connection state to FAILED - */ - @Test - public void auth_client_match_tokendetails_clientId_fail() { - try { - /* init ably for token */ - ClientOptions optsForToken = createOptions(testVars.keys[0].keyStr); - final AblyRest ablyForToken = new AblyRest(optsForToken); - - /* get token */ - Auth.TokenParams tokenParams = new Auth.TokenParams(); - tokenParams.clientId = "token clientId"; - Auth.TokenDetails tokenDetails = ablyForToken.auth.requestToken(tokenParams, null); - assertNotNull("Expected token value", tokenDetails.token); - - /* create ably realtime with token and clientId */ - ClientOptions opts = createOptions(); - opts.clientId = "options clientId"; - opts.tokenDetails = tokenDetails; - AblyRealtime ablyRealtime = new AblyRealtime(opts); - } catch (AblyException e) { - assertEquals("Verify error code indicates clientId mismatch", e.errorInfo.code, 40101); - } - } - - /** - * RSA15a: Any clientId provided in ClientOptions must match any - * non wildcard ('*') clientId value in TokenDetails - * Verify non-matching token clientId fails to authenticate a non-null clientId - * RSA15c: Following an auth request which uses a TokenDetails or TokenRequest - * object that contains an incompatible clientId, the library should ... transition - * the connection state to FAILED - */ - @Test - public void auth_client_match_token_clientId_fail() { - try { - /* init ably for token */ - ClientOptions optsForToken = createOptions(testVars.keys[0].keyStr); - final AblyRest ablyForToken = new AblyRest(optsForToken); - - /* get token */ - Auth.TokenParams tokenParams = new Auth.TokenParams(); - tokenParams.clientId = "token clientId"; - Auth.TokenDetails tokenDetails = ablyForToken.auth.requestToken(tokenParams, null); - assertNotNull("Expected token value", tokenDetails.token); - - /* create ably realtime with tokenDetails and clientId */ - ClientOptions opts = createOptions(); - opts.clientId = "options clientId"; - opts.token = tokenDetails.token; - AblyRealtime ablyRealtime = new AblyRealtime(opts); - - /* wait for failed state */ - Helpers.ConnectionWaiter connectionWaiter = new Helpers.ConnectionWaiter(ablyRealtime.connection); - ErrorInfo failure = connectionWaiter.waitFor(ConnectionState.failed); - assertEquals("Verify failed state is reached", ConnectionState.failed, ablyRealtime.connection.state); - assertEquals("Verify failure error code indicates clientId mismatch", failure.code, 40101); - } catch (AblyException e) { - e.printStackTrace(); - fail(); - } - } - - /** - * Verify message does not have explicit client id populated - * when library is identified - * Spec: RTL6g1a,RTL6g1b,RTL6g2,RTL6g3,RSA7e1 - */ - @Test - public void auth_clientid_publish_implicit() { - try { - String clientId = "test clientId"; - - /* create Ably instance with clientId */ - Helpers.RawProtocolMonitor protocolListener = Helpers.RawProtocolMonitor.createMonitor(ProtocolMessage.Action.message, ProtocolMessage.Action.message); - DebugOptions options = new DebugOptions(testVars.keys[0].keyStr); - fillInOptions(options); - options.clientId = clientId; - options.protocolListener = protocolListener; - AblyRealtime ably = new AblyRealtime(options); - - /* wait until connected */ - (new ConnectionWaiter(ably.connection)).waitFor(ConnectionState.connected); - assertEquals("Verify connected state reached", ably.connection.state, ConnectionState.connected); - - /* create a channel and attach */ - Channel channel = ably.channels.get("auth_clientid_publish_implicit_" + testParams.name); - channel.attach(); - (new ChannelWaiter(channel)).waitFor(ChannelState.attached); - assertEquals("Verify attached state reached", channel.state, ChannelState.attached); - - /* Publish a message */ - Message messageToPublish = new Message( - "I have clientId", /* name */ - String.valueOf(System.currentTimeMillis()) /* data */ - ); - channel.publish(new Message[] { messageToPublish }); - - /* wait until message seen on transport */ - protocolListener.waitForSend(1); - - /* Get sent message */ - Message messagePublished = protocolListener.sentMessages.get(0).messages[0]; - assertEquals("Sent message does not contain clientId", messagePublished.clientId, null); - - /* wait until message received on transport */ - protocolListener.waitForRecv(1); - - /* Get received message */ - Message messageReceived = protocolListener.receivedMessages.get(0).messages[0]; - assertEquals("Received message does contain clientId", messageReceived.clientId, clientId); - - /* Publish a message with explicit clientId */ - protocolListener.reset(); - messageToPublish = new Message( - "I have clientId", /* name */ - String.valueOf(System.currentTimeMillis()), - clientId /* clientId */ - ); - - channel.publish(new Message[] { messageToPublish }); - - /* wait until message seen on transport */ - protocolListener.waitForSend(1); - - /* Get sent message */ - messagePublished = protocolListener.sentMessages.get(0).messages[0]; - assertEquals("Sent message does contain clientId", messagePublished.clientId, clientId); - - /* wait until message received on transport */ - protocolListener.waitForRecv(1); - - /* Get sent message */ - messageReceived = protocolListener.receivedMessages.get(0).messages[0]; - assertEquals("Received message was accepted and does contain clientId", messageReceived.clientId, clientId); - - /* Publish a message with incorrect clientId */ - protocolListener.reset(); - messageToPublish = new Message( - "I have clientId", /* name */ - String.valueOf(System.currentTimeMillis()), - "invalid clientId" /* clientId */ - ); - - /* wait for the error callback */ - CompletionSet pubComplete = new CompletionSet(); - channel.publish(messageToPublish, pubComplete.add()); - pubComplete.waitFor(); - assertTrue("Verify publish callback called on completion", pubComplete.pending.isEmpty()); - assertTrue("Verify publish callback returns an error", pubComplete.errors.size() == 1); - assertEquals("Verify publish callback error has expected error code", pubComplete.errors.iterator().next().code, 40012); - - /* verify no message sent or received on transport */ - assertTrue("Verify no messages sent", protocolListener.sentMessages.isEmpty()); - assertTrue("Verify no messages received", protocolListener.receivedMessages.isEmpty()); - - /* Publish a message to verify that use of the channel can continue */ - messageToPublish = new Message( - "I have clientId", /* name */ - String.valueOf(System.currentTimeMillis()) /* data */ - ); - channel.publish(new Message[] { messageToPublish }); - - /* wait until message seen on transport */ - protocolListener.waitForSend(1); - - /* Get sent message */ - messagePublished = protocolListener.sentMessages.get(0).messages[0]; - assertEquals("Sent message does not contain clientId", messagePublished.clientId, null); - - /* wait until message received on transport */ - protocolListener.waitForRecv(1); - - /* Get received message */ - messageReceived = protocolListener.receivedMessages.get(0).messages[0]; - assertEquals("Received message does contain clientId", messageReceived.clientId, clientId); - - ably.close(); - } catch (Exception e) { - e.printStackTrace(); - fail("auth_clientid_publish_implicit: Unexpected exception"); - } - } - - /** - * Verify message does not have implicit client id - * if sent before library is identified, so messages - * are sent with explicit clientId - * Spec: RTL6g4 - */ - @Test - public void auth_clientid_publish_explicit_before_identified() { - AblyRealtime ably = null; - try { - String clientId = "test clientId"; - ClientOptions optsForToken = createOptions(testVars.keys[0].keyStr); - optsForToken.clientId = clientId; - AblyRest ablyForToken = new AblyRest(optsForToken); - TokenDetails tokenDetails = ablyForToken.auth.requestToken(null, null); - - /* create Ably instance with token and implied clientId */ - Helpers.RawProtocolMonitor protocolListener = Helpers.RawProtocolMonitor.createMonitor(ProtocolMessage.Action.message, ProtocolMessage.Action.message); - DebugOptions options = new DebugOptions(testVars.keys[0].keyStr); - fillInOptions(options); - options.token = tokenDetails.token; - options.protocolListener = protocolListener; - ably = new AblyRealtime(options); - - /* verify we don't yet know the implied clientId */ - assertNull("Verify clientId is unknown", ably.auth.clientId); - - /* create a channel */ - Channel channel = ably.channels.get("auth_clientid_publish_explicit_before_identified_" + testParams.name); - - /* publish before connection and attach */ - Message messageToPublish = new Message( - "I have clientId", /* name */ - String.valueOf(System.currentTimeMillis()), - clientId /* clientId */ - ); - channel.attach(); - channel.publish(new Message[] { messageToPublish }); - - /* wait until connected and attached */ - (new ConnectionWaiter(ably.connection)).waitFor(ConnectionState.connected); - assertEquals("Verify connected state reached", ably.connection.state, ConnectionState.connected); - (new ChannelWaiter(channel)).waitFor(ChannelState.attached); - assertEquals("Verify attached state reached", channel.state, ChannelState.attached); - - /* wait until message seen on transport */ - protocolListener.waitForSend(1); - - /* Get sent message */ - Message messagePublished = protocolListener.sentMessages.get(0).messages[0]; - assertEquals("Sent message does contain explicit clientId", messagePublished.clientId, clientId); - - /* wait until message received on transport */ - protocolListener.waitForRecv(1); - - /* Get received message */ - Message messageReceived = protocolListener.receivedMessages.get(0).messages[0]; - assertEquals("Received message does contain clientId", messageReceived.clientId, clientId); - - /* Publish a message to verify that use of the channel can continue */ - protocolListener.reset(); - messageToPublish = new Message( - "I have clientId", /* name */ - String.valueOf(System.currentTimeMillis()) /* data */ - ); - channel.publish(new Message[] { messageToPublish }); - - /* wait until message seen on transport */ - protocolListener.waitForSend(1); - - /* Get sent message */ - messagePublished = protocolListener.sentMessages.get(0).messages[0]; - assertEquals("Sent message does not contain clientId", messagePublished.clientId, null); - - /* wait until message received on transport */ - protocolListener.waitForRecv(1); - - /* Get received message */ - messageReceived = protocolListener.receivedMessages.get(0).messages[0]; - assertEquals("Received message does contain clientId", messageReceived.clientId, clientId); - } catch (Exception e) { - e.printStackTrace(); - fail("auth_clientid_publish_implicit: Unexpected exception"); - } finally { - if(ably != null) { - ably.close(); - } - } - } - - /** - * Call renew() whilst connecting; verify there's no crash (see https://github.com/ably/ably-java/issues/503) - */ - @Test - public void auth_renew_whilst_connecting() { - try { - /* get a TokenDetails */ - final String testKey = testVars.keys[0].keyStr; - ClientOptions optsForToken = createOptions(testKey); - final AblyRest ablyForToken = new AblyRest(optsForToken); - - final TokenDetails tokenDetails = ablyForToken.auth.requestToken(new Auth.TokenParams(){{ ttl = 1000L; }}, null); - assertNotNull("Expected token value", tokenDetails.token); - - /* create Ably realtime instance with token and authCallback */ - class ProtocolListener extends DebugOptions implements DebugOptions.RawProtocolListener { - ProtocolListener() { - Setup.getTestVars().fillInOptions(this); - protocolListener = this; - } - @Override - public void onRawConnectRequested(String url) { - synchronized(this) { - notify(); - } - } - - @Override - public void onRawConnect(String url) {} - @Override - public void onRawMessageSend(ProtocolMessage message) {} - @Override - public void onRawMessageRecv(ProtocolMessage message) {} - } - - ProtocolListener opts = new ProtocolListener(); - opts.autoConnect = false; - opts.tokenDetails = tokenDetails; - opts.authCallback = new Auth.TokenCallback() { - /* implement callback, using Ably instance with key */ - @Override - public Object getTokenRequest(Auth.TokenParams params) { - return tokenDetails; - } - }; - - final AblyRealtime ably = new AblyRealtime(opts); - synchronized (opts) { - ably.connect(); - try { - opts.wait(); - } catch(InterruptedException ie) {} - ably.auth.renew(); - } - - Helpers.ConnectionWaiter connectionWaiter = new Helpers.ConnectionWaiter(ably.connection); - boolean isConnected = connectionWaiter.waitFor(ConnectionState.connected, 1, 4000L); - if(isConnected) { - /* done */ - ably.close(); - } else { - fail("auth_expired_token_expire_renew: unable to connect; final state = " + ably.connection.state); - } - } catch (AblyException e) { - e.printStackTrace(); - fail("auth_expired_token_expire_renew: Unexpected exception instantiating library"); - } - } - - /** - * Verify that with queryTime=false, when instancing with an already-expired token and authCallback, - * connection can succeed - */ - @Test - public void auth_expired_token_expire_before_connect_renew() { - try { - /* get a TokenDetails */ - final String testKey = testVars.keys[0].keyStr; - ClientOptions optsForToken = createOptions(testKey); - optsForToken.queryTime = false; - final AblyRest ablyForToken = new AblyRest(optsForToken); - - TokenDetails tokenDetails = ablyForToken.auth.requestToken(new Auth.TokenParams(){{ ttl = 100L; }}, null); - assertNotNull("Expected token value", tokenDetails.token); - - /* allow to expire */ - try { Thread.sleep(200L); } catch(InterruptedException ie) {} - - /* create Ably realtime instance with token and authCallback */ - ClientOptions opts = createOptions(); - opts.tokenDetails = tokenDetails; - opts.authCallback = new Auth.TokenCallback() { - /* implement callback, using Ably instance with key */ - @Override - public Object getTokenRequest(Auth.TokenParams params) throws AblyException { - return ablyForToken.auth.requestToken(params, null); - } - }; - - /* disable token validity check */ - opts.queryTime = false; - - final AblyRealtime ably = new AblyRealtime(opts); - - Helpers.ConnectionWaiter connectionWaiter = new Helpers.ConnectionWaiter(ably.connection); - boolean isConnected = connectionWaiter.waitFor(ConnectionState.connected, 1, 30000L); - - if(isConnected) { - /* done */ - ably.close(); - } else { - fail("auth_expired_token_expire_renew: unable to connect; final state = " + ably.connection.state); - } - } catch (AblyException e) { - e.printStackTrace(); - fail("auth_expired_token_expire_renew: Unexpected exception instantiating library"); - } - } - - /** - * Verify that with queryTime=false, when instancing with an already-expired token and authCallback, - * connection can succeed - */ - @Test - public void auth_expired_token_expire_after_connect_renew() { - try { - /* get a TokenDetails and allow to expire */ - final String testKey = testVars.keys[0].keyStr; - ClientOptions optsForToken = createOptions(testKey); - optsForToken.queryTime = true; - final AblyRest ablyForToken = new AblyRest(optsForToken); - TokenDetails tokenDetails = ablyForToken.auth.requestToken(new Auth.TokenParams(){{ ttl = 2000L; }}, null); - assertNotNull("Expected token value", tokenDetails.token); - - /* create Ably realtime with token and authCallback */ - ClientOptions opts = createOptions(); - opts.tokenDetails = tokenDetails; - opts.queryTime = false; - opts.authCallback = new Auth.TokenCallback() { - /* implement callback, using Ably instance with key */ - @Override - public Object getTokenRequest(Auth.TokenParams params) throws AblyException { - return ablyForToken.auth.requestToken(params, null); - } - }; - - final AblyRealtime ably = new AblyRealtime(opts); - - Helpers.ConnectionWaiter connectionWaiter = new Helpers.ConnectionWaiter(ably.connection); - if(connectionWaiter.waitFor(ConnectionState.connected, 2, 4000L)) { - /* done */ - ably.close(); - } else { - fail("auth_expired_token_expire_renew: unable to connect; final state = " + ably.connection.state); - } - } catch (AblyException e) { - e.printStackTrace(); - fail("auth_expired_token_expire_renew: Unexpected exception instantiating library"); - } - } + @Rule + public Timeout testTimeout = Timeout.seconds(30); + + /** + * RSA12a: The clientId attribute of a TokenRequest or TokenDetails + * used for authentication is null, or ConnectionDetails#clientId is null + * following a connection to Ably. In this case, the null value indicates + * that a clientId identity may not be assumed by this client i.e. the + * client is anonymous for all operations + * + * Verify null token clientId in TokenDetails translates to a null clientId + */ + @Test + public void auth_client_match_tokendetails_null_clientId() { + try { + /* init ably for token */ + ClientOptions optsForToken = createOptions(testVars.keys[0].keyStr); + final AblyRest ablyForToken = new AblyRest(optsForToken); + + /* get token */ + Auth.TokenParams tokenParams = new Auth.TokenParams(); + tokenParams.clientId = null; + Auth.TokenDetails tokenDetails = ablyForToken.auth.requestToken(tokenParams, null); + assertNotNull("Expected token value", tokenDetails.token); + + /* create ably realtime with tokenDetails and clientId */ + ClientOptions opts = createOptions(); + opts.clientId = null; + opts.tokenDetails = tokenDetails; + AblyRealtime ablyRealtime = new AblyRealtime(opts); + System.out.println("done create ably"); + + /* wait for connected state */ + Helpers.ConnectionWaiter connectionWaiter = new Helpers.ConnectionWaiter(ablyRealtime.connection); + connectionWaiter.waitFor(ConnectionState.connected); + assertEquals("Verify connected state is reached", ConnectionState.connected, ablyRealtime.connection.state); + + /* check expected clientId */ + assertEquals("Auth#clientId is expected to be null", null, ablyRealtime.auth.clientId); + + ablyRealtime.close(); + } catch (AblyException e) { + e.printStackTrace(); + fail(); + } + } + + /** + * RSA12a: The clientId attribute of a TokenRequest or TokenDetails + * used for authentication is null, or ConnectionDetails#clientId is null + * following a connection to Ably. In this case, the null value indicates + * that a clientId identity may not be assumed by this client i.e. the + * client is anonymous for all operations + * + * Verify null token clientId in token translates to a null clientId + */ + @Test + public void auth_client_match_token_null_clientId() { + try { + /* init ably for token */ + ClientOptions optsForToken = createOptions(testVars.keys[0].keyStr); + final AblyRest ablyForToken = new AblyRest(optsForToken); + + /* get token */ + Auth.TokenParams tokenParams = new Auth.TokenParams(); + tokenParams.clientId = null; + Auth.TokenDetails tokenDetails = ablyForToken.auth.requestToken(tokenParams, null); + assertNotNull("Expected token value", tokenDetails.token); + + /* create ably realtime with tokenDetails and clientId */ + ClientOptions opts = createOptions(); + opts.clientId = null; + opts.token = tokenDetails.token; + AblyRealtime ablyRealtime = new AblyRealtime(opts); + System.out.println("done create ably"); + + /* wait for connected state */ + Helpers.ConnectionWaiter connectionWaiter = new Helpers.ConnectionWaiter(ablyRealtime.connection); + connectionWaiter.waitFor(ConnectionState.connected); + assertEquals("Verify connected state is reached", ConnectionState.connected, ablyRealtime.connection.state); + + /* check expected clientId */ + assertEquals("Auth#clientId is expected to be null", null, ablyRealtime.auth.clientId); + + ablyRealtime.close(); + } catch (AblyException e) { + e.printStackTrace(); + fail(); + } + } + + /** + * Init library with a key and token; verify Auth.clientId is null before + * connection + * Spec: RSA12b, RSA7b2, RSA7b3, RTC4a + */ + @Test + public void auth_clientid_null_before_auth() { + try { + final String clientId = "token clientId"; + + /* create token with clientId */ + ClientOptions optsForToken = createOptions(testVars.keys[0].keyStr); + optsForToken.clientId = clientId; + AblyRest ablyForToken = new AblyRest(optsForToken); + TokenDetails tokenDetails = ablyForToken.auth.requestToken(null, null); + + /* create ably realtime */ + ClientOptions opts = createOptions(); + opts.clientId = null; + opts.token = tokenDetails.token; + opts.autoConnect = false; + AblyRealtime ablyRealtime = new AblyRealtime(opts); + + /* check expected clientId */ + assertEquals("Auth#clientId is expected to be null", null, ablyRealtime.auth.clientId); + + /* wait for connected state */ + ablyRealtime.connection.connect(); + Helpers.ConnectionWaiter connectionWaiter = new Helpers.ConnectionWaiter(ablyRealtime.connection); + connectionWaiter.waitFor(ConnectionState.connected); + assertEquals("Verify connected state is reached", ConnectionState.connected, ablyRealtime.connection.state); + + /* check expected clientId */ + assertEquals("Auth#clientId is expected to be set", clientId, ablyRealtime.auth.clientId); + + ablyRealtime.close(); + } catch (AblyException e) { + e.printStackTrace(); + fail(); + } + } + + /** + * RSA15a: Any clientId provided in ClientOptions must match any + * non wildcard ('*') clientId value in TokenDetails + * RSA15b: If the clientId from TokenDetails or connectionDetails contains + * only a wildcard string '*', then the client is permitted to be either + * unidentified or identified by providing + * a clientId when communicating with Ably + * + * Verify wildcard token clientId in TokenDetails succeeds in + * authenticating a non-null clientId + */ + @Test + public void auth_client_match_tokendetails_wildcard_clientId() { + try { + /* init ably for token */ + ClientOptions optsForToken = createOptions(testVars.keys[0].keyStr); + final AblyRest ablyForToken = new AblyRest(optsForToken); + + /* get token */ + Auth.TokenParams tokenParams = new Auth.TokenParams(); + tokenParams.clientId = "*"; + Auth.TokenDetails tokenDetails = ablyForToken.auth.requestToken(tokenParams, null); + assertNotNull("Expected token value", tokenDetails.token); + + /* create ably realtime with tokenDetails and clientId */ + ClientOptions opts = createOptions(); + opts.clientId = "options clientId"; + opts.tokenDetails = tokenDetails; + AblyRealtime ablyRealtime = new AblyRealtime(opts); + System.out.println("done create ably"); + + /* wait for connected state */ + Helpers.ConnectionWaiter connectionWaiter = new Helpers.ConnectionWaiter(ablyRealtime.connection); + connectionWaiter.waitFor(ConnectionState.connected); + assertEquals("Verify connected state is reached", ConnectionState.connected, ablyRealtime.connection.state); + + /* check expected clientId */ + assertEquals("Auth#clientId is expected to be set", "options clientId", ablyRealtime.auth.clientId); + + ablyRealtime.close(); + } catch (AblyException e) { + e.printStackTrace(); + fail(); + } + } + + /** + * RSA15a: Any clientId provided in ClientOptions must match any + * non wildcard ('*') clientId value in TokenDetails + * RSA15b: If the clientId from TokenDetails or connectionDetails contains + * only a wildcard string '*', then the client is permitted to be either + * unidentified (i.e. authorised to act on behalf of any clientId) or + * identified by providing a clientId when communicating with Ably + * + * Verify wildcard token clientId in token succeeds in + * authenticating a non-null clientId + */ + @Test + public void auth_client_match_token_wildcard_clientId() { + try { + /* init ably for token */ + ClientOptions optsForToken = createOptions(testVars.keys[0].keyStr); + final AblyRest ablyForToken = new AblyRest(optsForToken); + + /* get token */ + Auth.TokenParams tokenParams = new Auth.TokenParams(); + tokenParams.clientId = "*"; + Auth.TokenDetails tokenDetails = ablyForToken.auth.requestToken(tokenParams, null); + assertNotNull("Expected token value", tokenDetails.token); + + /* create ably realtime with token and clientId */ + ClientOptions opts = createOptions(); + opts.clientId = "options clientId"; + opts.token = tokenDetails.token; + AblyRealtime ablyRealtime = new AblyRealtime(opts); + System.out.println("done create ably"); + + /* wait for connected state */ + Helpers.ConnectionWaiter connectionWaiter = new Helpers.ConnectionWaiter(ablyRealtime.connection); + connectionWaiter.waitFor(ConnectionState.connected); + assertEquals("Verify connected state is reached", ConnectionState.connected, ablyRealtime.connection.state); + + /* check expected clientId */ + assertEquals("Auth#clientId is expected to be set", "options clientId", ablyRealtime.auth.clientId); + + ablyRealtime.close(); + } catch (AblyException e) { + e.printStackTrace(); + fail(); + } + } + + /** + * RSA15b: If the clientId from TokenDetails or connectionDetails contains + * only a wildcard string '*', then the client is permitted to be either + * unidentified (i.e. authorised to act on behalf of any clientId) or + * identified by providing a clientId when communicating with Ably + * + * Verify wildcard token clientId in TokenDetails succeeds in + * authenticating a null clientId + */ + @Test + public void auth_client_null_match_tokendetails_wildcard_clientId() { + try { + /* init ably for token */ + ClientOptions optsForToken = createOptions(testVars.keys[0].keyStr); + final AblyRest ablyForToken = new AblyRest(optsForToken); + + /* get token */ + Auth.TokenParams tokenParams = new Auth.TokenParams(); + tokenParams.clientId = "*"; + Auth.TokenDetails tokenDetails = ablyForToken.auth.requestToken(tokenParams, null); + assertNotNull("Expected token value", tokenDetails.token); + + /* create ably realtime with tokenDetails and clientId */ + ClientOptions opts = createOptions(); + opts.clientId = null; + opts.tokenDetails = tokenDetails; + AblyRealtime ablyRealtime = new AblyRealtime(opts); + System.out.println("done create ably"); + + /* wait for connected state */ + Helpers.ConnectionWaiter connectionWaiter = new Helpers.ConnectionWaiter(ablyRealtime.connection); + connectionWaiter.waitFor(ConnectionState.connected); + assertEquals("Verify connected state is reached", ConnectionState.connected, ablyRealtime.connection.state); + + /* check expected clientId */ + assertEquals("Auth#clientId is expected to be set", "*", ablyRealtime.auth.clientId); + + ablyRealtime.close(); + } catch (AblyException e) { + e.printStackTrace(); + fail(); + } + } + + /** + * RSA15b: If the clientId from TokenDetails or connectionDetails contains + * only a wildcard string '*', then the client is permitted to be either + * unidentified (i.e. authorised to act on behalf of any clientId) or + * identified by providing a clientId when communicating with Ably + * + * Verify wildcard token clientId in token succeeds in + * authenticating a null clientId + */ + @Test + public void auth_client_null_match_token_wildcard_clientId() { + try { + /* init ably for token */ + ClientOptions optsForToken = createOptions(testVars.keys[0].keyStr); + final AblyRest ablyForToken = new AblyRest(optsForToken); + + /* get token */ + Auth.TokenParams tokenParams = new Auth.TokenParams(); + tokenParams.clientId = "*"; + Auth.TokenDetails tokenDetails = ablyForToken.auth.requestToken(tokenParams, null); + assertNotNull("Expected token value", tokenDetails.token); + + /* create ably realtime with token and clientId */ + ClientOptions opts = createOptions(); + opts.clientId = null; + opts.token = tokenDetails.token; + AblyRealtime ablyRealtime = new AblyRealtime(opts); + System.out.println("done create ably"); + + /* wait for connected state */ + Helpers.ConnectionWaiter connectionWaiter = new Helpers.ConnectionWaiter(ablyRealtime.connection); + connectionWaiter.waitFor(ConnectionState.connected); + assertEquals("Verify connected state is reached", ConnectionState.connected, ablyRealtime.connection.state); + + /* check expected clientId */ + assertEquals("Auth#clientId is expected to be set", "*", ablyRealtime.auth.clientId); + + ablyRealtime.close(); + } catch (AblyException e) { + e.printStackTrace(); + fail(); + } + } + + /** + * RSA15a: Any clientId provided in ClientOptions must match any + * non wildcard ('*') clientId value in TokenDetails + * + * Verify matching token clientId in TokenDetails succeeds + * in authenticating a non-null clientId + */ + @Test + public void auth_client_match_tokendetails_clientId() { + try { + /* init ably for token */ + ClientOptions optsForToken = createOptions(testVars.keys[0].keyStr); + final AblyRest ablyForToken = new AblyRest(optsForToken); + + /* get token */ + Auth.TokenParams tokenParams = new Auth.TokenParams(); + tokenParams.clientId = "options clientId"; + Auth.TokenDetails tokenDetails = ablyForToken.auth.requestToken(tokenParams, null); + assertNotNull("Expected token value", tokenDetails.token); + + /* create ably realtime with tokenDetails and clientId */ + ClientOptions opts = createOptions(); + opts.clientId = "options clientId"; + opts.tokenDetails = tokenDetails; + AblyRealtime ablyRealtime = new AblyRealtime(opts); + + /* wait for connected state */ + Helpers.ConnectionWaiter connectionWaiter = new Helpers.ConnectionWaiter(ablyRealtime.connection); + connectionWaiter.waitFor(ConnectionState.connected); + assertEquals("Verify connected state is reached", ConnectionState.connected, ablyRealtime.connection.state); + + /* check expected clientId */ + assertEquals("Auth#clientId is expected to be set", "options clientId", ablyRealtime.auth.clientId); + + ablyRealtime.close(); + } catch (AblyException e) { + e.printStackTrace(); + fail(); + } + } + + /** + * RSA15a: Any clientId provided in ClientOptions must match any + * non wildcard ('*') clientId value in TokenDetails + * in authenticating a non-null clientId + * + * Verify matching token clientId in token succeeds + */ + @Test + public void auth_client_match_token_clientId() { + try { + /* init ably for token */ + ClientOptions optsForToken = createOptions(testVars.keys[0].keyStr); + final AblyRest ablyForToken = new AblyRest(optsForToken); + + /* get token */ + Auth.TokenParams tokenParams = new Auth.TokenParams(); + tokenParams.clientId = "options clientId"; + Auth.TokenDetails tokenDetails = ablyForToken.auth.requestToken(tokenParams, null); + assertNotNull("Expected token value", tokenDetails.token); + + /* create ably realtime with token and clientId */ + ClientOptions opts = createOptions(); + opts.clientId = "options clientId"; + opts.token = tokenDetails.token; + AblyRealtime ablyRealtime = new AblyRealtime(opts); + + /* wait for connected state */ + Helpers.ConnectionWaiter connectionWaiter = new Helpers.ConnectionWaiter(ablyRealtime.connection); + connectionWaiter.waitFor(ConnectionState.connected); + assertEquals("Verify connected state is reached", ConnectionState.connected, ablyRealtime.connection.state); + + /* check expected clientId */ + assertEquals("Auth#clientId is expected to be set", "options clientId", ablyRealtime.auth.clientId); + + ablyRealtime.close(); + } catch (AblyException e) { + e.printStackTrace(); + fail(); + } + } + + /** + * RSA15a: Any clientId provided in ClientOptions must match any + * non wildcard ('*') clientId value in TokenDetails + * Verify non-matching token clientId fails to authenticate a non-null clientId + * RSA15c: Following an auth request which uses a TokenDetails or TokenRequest + * object that contains an incompatible clientId, the library should ... transition + * the connection state to FAILED + */ + @Test + public void auth_client_match_tokendetails_clientId_fail() { + try { + /* init ably for token */ + ClientOptions optsForToken = createOptions(testVars.keys[0].keyStr); + final AblyRest ablyForToken = new AblyRest(optsForToken); + + /* get token */ + Auth.TokenParams tokenParams = new Auth.TokenParams(); + tokenParams.clientId = "token clientId"; + Auth.TokenDetails tokenDetails = ablyForToken.auth.requestToken(tokenParams, null); + assertNotNull("Expected token value", tokenDetails.token); + + /* create ably realtime with token and clientId */ + ClientOptions opts = createOptions(); + opts.clientId = "options clientId"; + opts.tokenDetails = tokenDetails; + AblyRealtime ablyRealtime = new AblyRealtime(opts); + } catch (AblyException e) { + assertEquals("Verify error code indicates clientId mismatch", e.errorInfo.code, 40101); + } + } + + /** + * RSA15a: Any clientId provided in ClientOptions must match any + * non wildcard ('*') clientId value in TokenDetails + * Verify non-matching token clientId fails to authenticate a non-null clientId + * RSA15c: Following an auth request which uses a TokenDetails or TokenRequest + * object that contains an incompatible clientId, the library should ... transition + * the connection state to FAILED + */ + @Test + public void auth_client_match_token_clientId_fail() { + try { + /* init ably for token */ + ClientOptions optsForToken = createOptions(testVars.keys[0].keyStr); + final AblyRest ablyForToken = new AblyRest(optsForToken); + + /* get token */ + Auth.TokenParams tokenParams = new Auth.TokenParams(); + tokenParams.clientId = "token clientId"; + Auth.TokenDetails tokenDetails = ablyForToken.auth.requestToken(tokenParams, null); + assertNotNull("Expected token value", tokenDetails.token); + + /* create ably realtime with tokenDetails and clientId */ + ClientOptions opts = createOptions(); + opts.clientId = "options clientId"; + opts.token = tokenDetails.token; + AblyRealtime ablyRealtime = new AblyRealtime(opts); + + /* wait for failed state */ + Helpers.ConnectionWaiter connectionWaiter = new Helpers.ConnectionWaiter(ablyRealtime.connection); + ErrorInfo failure = connectionWaiter.waitFor(ConnectionState.failed); + assertEquals("Verify failed state is reached", ConnectionState.failed, ablyRealtime.connection.state); + assertEquals("Verify failure error code indicates clientId mismatch", failure.code, 40101); + } catch (AblyException e) { + e.printStackTrace(); + fail(); + } + } + + /** + * Verify message does not have explicit client id populated + * when library is identified + * Spec: RTL6g1a,RTL6g1b,RTL6g2,RTL6g3,RSA7e1 + */ + @Test + public void auth_clientid_publish_implicit() { + try { + String clientId = "test clientId"; + + /* create Ably instance with clientId */ + Helpers.RawProtocolMonitor protocolListener = Helpers.RawProtocolMonitor.createMonitor(ProtocolMessage.Action.message, ProtocolMessage.Action.message); + DebugOptions options = new DebugOptions(testVars.keys[0].keyStr); + fillInOptions(options); + options.clientId = clientId; + options.protocolListener = protocolListener; + AblyRealtime ably = new AblyRealtime(options); + + /* wait until connected */ + (new ConnectionWaiter(ably.connection)).waitFor(ConnectionState.connected); + assertEquals("Verify connected state reached", ably.connection.state, ConnectionState.connected); + + /* create a channel and attach */ + Channel channel = ably.channels.get("auth_clientid_publish_implicit_" + testParams.name); + channel.attach(); + (new ChannelWaiter(channel)).waitFor(ChannelState.attached); + assertEquals("Verify attached state reached", channel.state, ChannelState.attached); + + /* Publish a message */ + Message messageToPublish = new Message( + "I have clientId", /* name */ + String.valueOf(System.currentTimeMillis()) /* data */ + ); + channel.publish(new Message[] { messageToPublish }); + + /* wait until message seen on transport */ + protocolListener.waitForSend(1); + + /* Get sent message */ + Message messagePublished = protocolListener.sentMessages.get(0).messages[0]; + assertEquals("Sent message does not contain clientId", messagePublished.clientId, null); + + /* wait until message received on transport */ + protocolListener.waitForRecv(1); + + /* Get received message */ + Message messageReceived = protocolListener.receivedMessages.get(0).messages[0]; + assertEquals("Received message does contain clientId", messageReceived.clientId, clientId); + + /* Publish a message with explicit clientId */ + protocolListener.reset(); + messageToPublish = new Message( + "I have clientId", /* name */ + String.valueOf(System.currentTimeMillis()), + clientId /* clientId */ + ); + + channel.publish(new Message[] { messageToPublish }); + + /* wait until message seen on transport */ + protocolListener.waitForSend(1); + + /* Get sent message */ + messagePublished = protocolListener.sentMessages.get(0).messages[0]; + assertEquals("Sent message does contain clientId", messagePublished.clientId, clientId); + + /* wait until message received on transport */ + protocolListener.waitForRecv(1); + + /* Get sent message */ + messageReceived = protocolListener.receivedMessages.get(0).messages[0]; + assertEquals("Received message was accepted and does contain clientId", messageReceived.clientId, clientId); + + /* Publish a message with incorrect clientId */ + protocolListener.reset(); + messageToPublish = new Message( + "I have clientId", /* name */ + String.valueOf(System.currentTimeMillis()), + "invalid clientId" /* clientId */ + ); + + /* wait for the error callback */ + CompletionSet pubComplete = new CompletionSet(); + channel.publish(messageToPublish, pubComplete.add()); + pubComplete.waitFor(); + assertTrue("Verify publish callback called on completion", pubComplete.pending.isEmpty()); + assertTrue("Verify publish callback returns an error", pubComplete.errors.size() == 1); + assertEquals("Verify publish callback error has expected error code", pubComplete.errors.iterator().next().code, 40012); + + /* verify no message sent or received on transport */ + assertTrue("Verify no messages sent", protocolListener.sentMessages.isEmpty()); + assertTrue("Verify no messages received", protocolListener.receivedMessages.isEmpty()); + + /* Publish a message to verify that use of the channel can continue */ + messageToPublish = new Message( + "I have clientId", /* name */ + String.valueOf(System.currentTimeMillis()) /* data */ + ); + channel.publish(new Message[] { messageToPublish }); + + /* wait until message seen on transport */ + protocolListener.waitForSend(1); + + /* Get sent message */ + messagePublished = protocolListener.sentMessages.get(0).messages[0]; + assertEquals("Sent message does not contain clientId", messagePublished.clientId, null); + + /* wait until message received on transport */ + protocolListener.waitForRecv(1); + + /* Get received message */ + messageReceived = protocolListener.receivedMessages.get(0).messages[0]; + assertEquals("Received message does contain clientId", messageReceived.clientId, clientId); + + ably.close(); + } catch (Exception e) { + e.printStackTrace(); + fail("auth_clientid_publish_implicit: Unexpected exception"); + } + } + + /** + * Verify message does not have implicit client id + * if sent before library is identified, so messages + * are sent with explicit clientId + * Spec: RTL6g4 + */ + @Test + public void auth_clientid_publish_explicit_before_identified() { + AblyRealtime ably = null; + try { + String clientId = "test clientId"; + ClientOptions optsForToken = createOptions(testVars.keys[0].keyStr); + optsForToken.clientId = clientId; + AblyRest ablyForToken = new AblyRest(optsForToken); + TokenDetails tokenDetails = ablyForToken.auth.requestToken(null, null); + + /* create Ably instance with token and implied clientId */ + Helpers.RawProtocolMonitor protocolListener = Helpers.RawProtocolMonitor.createMonitor(ProtocolMessage.Action.message, ProtocolMessage.Action.message); + DebugOptions options = new DebugOptions(testVars.keys[0].keyStr); + fillInOptions(options); + options.token = tokenDetails.token; + options.protocolListener = protocolListener; + ably = new AblyRealtime(options); + + /* verify we don't yet know the implied clientId */ + assertNull("Verify clientId is unknown", ably.auth.clientId); + + /* create a channel */ + Channel channel = ably.channels.get("auth_clientid_publish_explicit_before_identified_" + testParams.name); + + /* publish before connection and attach */ + Message messageToPublish = new Message( + "I have clientId", /* name */ + String.valueOf(System.currentTimeMillis()), + clientId /* clientId */ + ); + channel.attach(); + channel.publish(new Message[] { messageToPublish }); + + /* wait until connected and attached */ + (new ConnectionWaiter(ably.connection)).waitFor(ConnectionState.connected); + assertEquals("Verify connected state reached", ably.connection.state, ConnectionState.connected); + (new ChannelWaiter(channel)).waitFor(ChannelState.attached); + assertEquals("Verify attached state reached", channel.state, ChannelState.attached); + + /* wait until message seen on transport */ + protocolListener.waitForSend(1); + + /* Get sent message */ + Message messagePublished = protocolListener.sentMessages.get(0).messages[0]; + assertEquals("Sent message does contain explicit clientId", messagePublished.clientId, clientId); + + /* wait until message received on transport */ + protocolListener.waitForRecv(1); + + /* Get received message */ + Message messageReceived = protocolListener.receivedMessages.get(0).messages[0]; + assertEquals("Received message does contain clientId", messageReceived.clientId, clientId); + + /* Publish a message to verify that use of the channel can continue */ + protocolListener.reset(); + messageToPublish = new Message( + "I have clientId", /* name */ + String.valueOf(System.currentTimeMillis()) /* data */ + ); + channel.publish(new Message[] { messageToPublish }); + + /* wait until message seen on transport */ + protocolListener.waitForSend(1); + + /* Get sent message */ + messagePublished = protocolListener.sentMessages.get(0).messages[0]; + assertEquals("Sent message does not contain clientId", messagePublished.clientId, null); + + /* wait until message received on transport */ + protocolListener.waitForRecv(1); + + /* Get received message */ + messageReceived = protocolListener.receivedMessages.get(0).messages[0]; + assertEquals("Received message does contain clientId", messageReceived.clientId, clientId); + } catch (Exception e) { + e.printStackTrace(); + fail("auth_clientid_publish_implicit: Unexpected exception"); + } finally { + if(ably != null) { + ably.close(); + } + } + } + + /** + * Call renew() whilst connecting; verify there's no crash (see https://github.com/ably/ably-java/issues/503) + */ + @Test + public void auth_renew_whilst_connecting() { + try { + /* get a TokenDetails */ + final String testKey = testVars.keys[0].keyStr; + ClientOptions optsForToken = createOptions(testKey); + final AblyRest ablyForToken = new AblyRest(optsForToken); + + final TokenDetails tokenDetails = ablyForToken.auth.requestToken(new Auth.TokenParams(){{ ttl = 1000L; }}, null); + assertNotNull("Expected token value", tokenDetails.token); + + /* create Ably realtime instance with token and authCallback */ + class ProtocolListener extends DebugOptions implements DebugOptions.RawProtocolListener { + ProtocolListener() { + Setup.getTestVars().fillInOptions(this); + protocolListener = this; + } + @Override + public void onRawConnectRequested(String url) { + synchronized(this) { + notify(); + } + } + + @Override + public void onRawConnect(String url) {} + @Override + public void onRawMessageSend(ProtocolMessage message) {} + @Override + public void onRawMessageRecv(ProtocolMessage message) {} + } + + ProtocolListener opts = new ProtocolListener(); + opts.autoConnect = false; + opts.tokenDetails = tokenDetails; + opts.authCallback = new Auth.TokenCallback() { + /* implement callback, using Ably instance with key */ + @Override + public Object getTokenRequest(Auth.TokenParams params) { + return tokenDetails; + } + }; + + final AblyRealtime ably = new AblyRealtime(opts); + synchronized (opts) { + ably.connect(); + try { + opts.wait(); + } catch(InterruptedException ie) {} + ably.auth.renew(); + } + + Helpers.ConnectionWaiter connectionWaiter = new Helpers.ConnectionWaiter(ably.connection); + boolean isConnected = connectionWaiter.waitFor(ConnectionState.connected, 1, 4000L); + if(isConnected) { + /* done */ + ably.close(); + } else { + fail("auth_expired_token_expire_renew: unable to connect; final state = " + ably.connection.state); + } + } catch (AblyException e) { + e.printStackTrace(); + fail("auth_expired_token_expire_renew: Unexpected exception instantiating library"); + } + } + + /** + * Verify that with queryTime=false, when instancing with an already-expired token and authCallback, + * connection can succeed + */ + @Test + public void auth_expired_token_expire_before_connect_renew() { + try { + /* get a TokenDetails */ + final String testKey = testVars.keys[0].keyStr; + ClientOptions optsForToken = createOptions(testKey); + optsForToken.queryTime = false; + final AblyRest ablyForToken = new AblyRest(optsForToken); + + TokenDetails tokenDetails = ablyForToken.auth.requestToken(new Auth.TokenParams(){{ ttl = 100L; }}, null); + assertNotNull("Expected token value", tokenDetails.token); + + /* allow to expire */ + try { Thread.sleep(200L); } catch(InterruptedException ie) {} + + /* create Ably realtime instance with token and authCallback */ + ClientOptions opts = createOptions(); + opts.tokenDetails = tokenDetails; + opts.authCallback = new Auth.TokenCallback() { + /* implement callback, using Ably instance with key */ + @Override + public Object getTokenRequest(Auth.TokenParams params) throws AblyException { + return ablyForToken.auth.requestToken(params, null); + } + }; + + /* disable token validity check */ + opts.queryTime = false; + + final AblyRealtime ably = new AblyRealtime(opts); + + Helpers.ConnectionWaiter connectionWaiter = new Helpers.ConnectionWaiter(ably.connection); + boolean isConnected = connectionWaiter.waitFor(ConnectionState.connected, 1, 30000L); + + if(isConnected) { + /* done */ + ably.close(); + } else { + fail("auth_expired_token_expire_renew: unable to connect; final state = " + ably.connection.state); + } + } catch (AblyException e) { + e.printStackTrace(); + fail("auth_expired_token_expire_renew: Unexpected exception instantiating library"); + } + } + + /** + * Verify that with queryTime=false, when instancing with an already-expired token and authCallback, + * connection can succeed + */ + @Test + public void auth_expired_token_expire_after_connect_renew() { + try { + /* get a TokenDetails and allow to expire */ + final String testKey = testVars.keys[0].keyStr; + ClientOptions optsForToken = createOptions(testKey); + optsForToken.queryTime = true; + final AblyRest ablyForToken = new AblyRest(optsForToken); + TokenDetails tokenDetails = ablyForToken.auth.requestToken(new Auth.TokenParams(){{ ttl = 2000L; }}, null); + assertNotNull("Expected token value", tokenDetails.token); + + /* create Ably realtime with token and authCallback */ + ClientOptions opts = createOptions(); + opts.tokenDetails = tokenDetails; + opts.queryTime = false; + opts.authCallback = new Auth.TokenCallback() { + /* implement callback, using Ably instance with key */ + @Override + public Object getTokenRequest(Auth.TokenParams params) throws AblyException { + return ablyForToken.auth.requestToken(params, null); + } + }; + + final AblyRealtime ably = new AblyRealtime(opts); + + Helpers.ConnectionWaiter connectionWaiter = new Helpers.ConnectionWaiter(ably.connection); + if(connectionWaiter.waitFor(ConnectionState.connected, 2, 4000L)) { + /* done */ + ably.close(); + } else { + fail("auth_expired_token_expire_renew: unable to connect; final state = " + ably.connection.state); + } + } catch (AblyException e) { + e.printStackTrace(); + fail("auth_expired_token_expire_renew: Unexpected exception instantiating library"); + } + } } diff --git a/lib/src/test/java/io/ably/lib/test/realtime/RealtimeChannelHistoryTest.java b/lib/src/test/java/io/ably/lib/test/realtime/RealtimeChannelHistoryTest.java index c156eb0db..9c7dac07e 100644 --- a/lib/src/test/java/io/ably/lib/test/realtime/RealtimeChannelHistoryTest.java +++ b/lib/src/test/java/io/ably/lib/test/realtime/RealtimeChannelHistoryTest.java @@ -30,1363 +30,1363 @@ public class RealtimeChannelHistoryTest extends ParameterizedTest { - private AblyRealtime ably; - private long timeOffset; - - @Rule - public Timeout testTimeout = Timeout.seconds(300); - - @Before - public void setUpBefore() throws Exception { - ClientOptions opts = createOptions(testVars.keys[0].keyStr); - ably = new AblyRealtime(opts); - long timeFromService = ably.time(); - timeOffset = timeFromService - System.currentTimeMillis(); - } - - /** - * Send a single message on a channel and verify that it can be - * retrieved using channel.history() without needing to wait for - * it to be persisted. - */ - @Test - public void channelhistory_simple() { - AblyRealtime ably = null; - try { - ClientOptions opts = createOptions(testVars.keys[0].keyStr); - ably = new AblyRealtime(opts); - String channelName = "persisted:channelhistory_simple_" + testParams.name; - String messageText = "Test message (channelhistory_simple)"; - - /* create a channel */ - final Channel channel = ably.channels.get(channelName); - - /* attach */ - channel.attach(); - (new ChannelWaiter(channel)).waitFor(ChannelState.attached); - assertEquals("Verify attached state reached", channel.state, ChannelState.attached); - - /* publish to the channel */ - CompletionWaiter msgComplete = new CompletionWaiter(); - channel.publish("test_event", messageText, msgComplete); - - /* wait for the publish callback to be called */ - msgComplete.waitFor(); - assertTrue("Verify success callback was called", msgComplete.success); - - /* get the history for this channel */ - PaginatedResult messages = channel.history(null); - assertNotNull("Expected non-null messages", messages); - assertEquals("Expected 1 message", messages.items().length, 1); - - /* verify message contents */ - assertEquals("Expect correct message text", messages.items()[0].data, messageText); - } catch (AblyException e) { - e.printStackTrace(); - fail("single_history_binary: Unexpected exception instantiating library"); - } finally { - if(ably != null) - ably.close(); - } - } - - /** - * Send couple of messages without listener on a channel and - * verify that it can be retrieved using channel.history() - * without needing to wait for it to be persisted. - */ - @Test - public void channelhistory_simple_withoutlistener() { - AblyRealtime ably = null; - try { - ClientOptions opts = createOptions(testVars.keys[0].keyStr); - ably = new AblyRealtime(opts); - String channelName = "persisted:channelhistory_simple_withoutlistener_" + testParams.name; - String message1Text = "Test message 1 (channelhistory_simple_withoutlistener)"; - Message message2 = new Message("test_event", "Test message 2 (channelhistory_simple_withoutlistener)"); - Message[] messages34 = new Message[] { - new Message("test_event", "Test message 3 (channelhistory_simple_withoutlistener)"), - new Message("test_event", "Test message 4 (channelhistory_simple_withoutlistener)") - }; - - /* create a channel */ - final Channel channel = ably.channels.get(channelName); - - /* attach */ - channel.attach(); - (new ChannelWaiter(channel)).waitFor(ChannelState.attached); - assertEquals("Verify attached state reached", channel.state, ChannelState.attached); - - /* publish to the channel */ - channel.publish("test_event", message1Text); - channel.publish(message2); - channel.publish(messages34); - - /* Get history for the channel. Wait for no longer than 2 seconds for the history to be populated */ - PaginatedResult messages; - int n = 0; - do { - messages = channel.history(null); - if (messages.items().length < 4) { - try { Thread.sleep(100); } catch (InterruptedException e) {} - } - } while (messages.items().length < 4 && ++n < 20); - - assertNotNull("Expected non-null messages", messages); - assertEquals("Expected 4 message", messages.items().length, 4); - - /* verify we received message history from most recent to older */ - assertEquals("Expect correct message text", messages.items()[0].data, messages34[1].data); - assertEquals("Expect correct message text", messages.items()[1].data, messages34[0].data); - assertEquals("Expect correct message text", messages.items()[2].data, message2.data); - assertEquals("Expect correct message text", messages.items()[3].data, message1Text); - } catch (AblyException e) { - e.printStackTrace(); - fail("channelhistory_simple_binary_withoutlistener: Unexpected exception instantiating library"); - } finally { - if(ably != null) - ably.close(); - } - } - - /** - * Publish events with data of various datatypes - */ - @Test - public void channelhistory_types() { - AblyRealtime ably = null; - try { - ClientOptions opts = createOptions(testVars.keys[0].keyStr); - ably = new AblyRealtime(opts); - String channelName = "persisted:channelhistory_types_" + testParams.name; - - /* create a channel */ - final Channel channel = ably.channels.get(channelName); - - /* attach */ - channel.attach(); - (new ChannelWaiter(channel)).waitFor(ChannelState.attached); - assertEquals("Verify attached state reached", channel.state, ChannelState.attached); - - /* publish to the channel */ - CompletionSet msgComplete = new CompletionSet(); - channel.publish("history0", "This is a string message payload", msgComplete.add()); - channel.publish("history1", "This is a byte[] message payload".getBytes(), msgComplete.add()); - - /* wait for the publish callback to be called */ - msgComplete.waitFor(); - assertTrue("Verify success callback was called", msgComplete.errors.isEmpty()); - - /* get the history for this channel */ - PaginatedResult messages = channel.history(null); - assertNotNull("Expected non-null messages", messages); - assertEquals("Expected 2 messages", messages.items().length, 2); - HashMap messageContents = new HashMap(); - - /* verify message contents */ - for(Message message : messages.items()) - messageContents.put(message.name, message); - assertEquals("Expect history0 to be expected String", messageContents.get("history0").data, "This is a string message payload"); - assertEquals("Expect history1 to be expected byte[]", new String((byte[])messageContents.get("history1").data), "This is a byte[] message payload"); - - /* verify message order */ - Message[] expectedMessageHistory = new Message[]{ - messageContents.get("history1"), - messageContents.get("history0") - }; - Assert.assertArrayEquals("Expect messages in reverse order", messages.items(), expectedMessageHistory); - } catch (AblyException e) { - e.printStackTrace(); - fail("channelhistory_types: Unexpected exception"); - } finally { - if(ably != null) - ably.close(); - } - } - - /** - * Publish events with data of various datatypes - */ - @Test - public void channelhistory_types_forward() { - AblyRealtime ably = null; - try { - ClientOptions opts = createOptions(testVars.keys[0].keyStr); - ably = new AblyRealtime(opts); - String channelName = "persisted:channelhistory_types_forward_" + testParams.name; - - /* create a channel */ - final Channel channel = ably.channels.get(channelName); - - /* attach */ - channel.attach(); - (new ChannelWaiter(channel)).waitFor(ChannelState.attached); - assertEquals("Verify attached state reached", channel.state, ChannelState.attached); - - /* publish to the channel */ - CompletionSet msgComplete = new CompletionSet(); - channel.publish("history0", "This is a string message payload", msgComplete.add()); - channel.publish("history1", "This is a byte[] message payload".getBytes(), msgComplete.add()); - - /* wait for the publish callback to be called */ - msgComplete.waitFor(); - assertTrue("Verify success callback was called", msgComplete.errors.isEmpty()); - - /* get the history for this channel */ - PaginatedResult messages = channel.history(new Param[] { new Param("direction", "forwards") }); - assertNotNull("Expected non-null messages", messages); - assertEquals("Expected 2 messages", messages.items().length, 2); - HashMap messageContents = new HashMap(); - - /* verify message contents */ - for(Message message : messages.items()) - messageContents.put(message.name, message); - assertEquals("Expect history0 to be expected String", messageContents.get("history0").data, "This is a string message payload"); - assertEquals("Expect history1 to be expected byte[]", new String((byte[])messageContents.get("history1").data), "This is a byte[] message payload"); - - /* verify message order */ - Message[] expectedMessageHistory = new Message[]{ - messageContents.get("history0"), - messageContents.get("history1") - }; - Assert.assertArrayEquals("Expect messages in sent order", messages.items(), expectedMessageHistory); - } catch (AblyException e) { - e.printStackTrace(); - fail("channelhistory_types_forward: Unexpected exception"); - } finally { - if(ably != null) - ably.close(); - } - } - - /** - * Connect twice to the service, each using the default (binary) protocol. - * Publish messages on one connection to a given channel; then attach - * the second connection to the same channel and verify a complete message - * history can be obtained. - */ - @Test - public void channelhistory_second_channel() { - AblyRealtime txAbly = null, rxAbly = null; - try { - ClientOptions txOpts = createOptions(testVars.keys[0].keyStr); - txAbly = new AblyRealtime(txOpts); - ClientOptions rxOpts = createOptions(testVars.keys[0].keyStr); - rxAbly = new AblyRealtime(rxOpts); - String channelName = "persisted:channelhistory_second_channel_" + testParams.name; - - /* create a channel */ - final Channel txChannel = txAbly.channels.get(channelName); - final Channel rxChannel = rxAbly.channels.get(channelName); - - /* attach sender */ - txChannel.attach(); - (new ChannelWaiter(txChannel)).waitFor(ChannelState.attached); - assertEquals("Verify attached state reached", txChannel.state, ChannelState.attached); - - /* publish to the channel */ - String messageText = "Test message (channelhistory_second_channel)"; - CompletionWaiter msgComplete = new CompletionWaiter(); - txChannel.publish("test_event", messageText, msgComplete); - - /* wait for the publish callback to be called */ - msgComplete.waitFor(); - assertTrue("Verify success callback was called", msgComplete.success); - - /* attach receiver */ - rxChannel.attach(); - (new ChannelWaiter(rxChannel)).waitFor(ChannelState.attached); - assertEquals("Verify attached state reached", rxChannel.state, ChannelState.attached); - - /* get the history for this channel */ - PaginatedResult messages = rxChannel.history(null); - assertNotNull("Expected non-null messages", messages); - assertEquals("Expected 1 message", messages.items().length, 1); - - /* verify message contents */ - assertEquals("Expect correct message text", messages.items()[0].data, messageText); - - } catch (AblyException e) { - e.printStackTrace(); - fail("init0: Unexpected exception instantiating library"); - } finally { - if(txAbly != null) - txAbly.close(); - if(rxAbly != null) - rxAbly.close(); - } - } - - /** - * Send a single message on a channel and verify that it can be - * retrieved using channel.history() after waiting for it to be - * persisted. - */ - @Test - public void channelhistory_wait_b() { - AblyRealtime ably = null; - try { - ClientOptions opts = createOptions(testVars.keys[0].keyStr); - ably = new AblyRealtime(opts); - String channelName = "persisted:channelhistory_wait_b_" + testParams.name; - String messageText = "Test message (channelhistory_wait_b)"; - - /* create a channel */ - final Channel channel = ably.channels.get(channelName); - - /* attach */ - channel.attach(); - (new ChannelWaiter(channel)).waitFor(ChannelState.attached); - assertEquals("Verify attached state reached", channel.state, ChannelState.attached); - - /* publish to the channel */ - CompletionWaiter msgComplete = new CompletionWaiter(); - channel.publish("test_event", messageText, msgComplete); - - /* wait for the publish callback to be called */ - msgComplete.waitFor(); - assertTrue("Verify success callback was called", msgComplete.success); - - /* wait for the history to be persisted */ - try { - Thread.sleep(16000); - } catch(InterruptedException ie) {} - - /* get the history for this channel */ - PaginatedResult messages = channel.history(null); - assertNotNull("Expected non-null messages", messages); - assertEquals("Expected 1 message", messages.items().length, 1); - - /* verify message contents */ - assertEquals("Expect correct message text", messages.items()[0].data, messageText); - } catch (AblyException e) { - e.printStackTrace(); - fail("single_history_binary: Unexpected exception instantiating library"); - } finally { - if(ably != null) - ably.close(); - } - } - - /** - * Send a single message on a channel and verify that it can be - * retrieved using channel.history(direction=forwards) after waiting - * for it to be persisted. - */ - @Test - public void channelhistory_wait_f() { - AblyRealtime ably = null; - try { - ClientOptions opts = createOptions(testVars.keys[0].keyStr); - ably = new AblyRealtime(opts); - String channelName = "persisted:channelhistory_wait_f_" + testParams.name; - String messageText = "Test message (channelhistory_wait_f)"; - - /* create a channel */ - final Channel channel = ably.channels.get(channelName); - - /* attach */ - channel.attach(); - (new ChannelWaiter(channel)).waitFor(ChannelState.attached); - assertEquals("Verify attached state reached", channel.state, ChannelState.attached); - - /* publish to the channel */ - CompletionWaiter msgComplete = new CompletionWaiter(); - channel.publish("test_event", messageText, msgComplete); - - /* wait for the publish callback to be called */ - msgComplete.waitFor(); - assertTrue("Verify success callback was called", msgComplete.success); - - /* wait for the history to be persisted */ - try { - Thread.sleep(16000); - } catch(InterruptedException ie) {} - - /* get the history for this channel */ - PaginatedResult messages = channel.history(new Param[]{ new Param("direction", "forwards") }); - assertNotNull("Expected non-null messages", messages); - assertEquals("Expected 1 message", messages.items().length, 1); - - /* verify message contents */ - assertEquals("Expect correct message text", messages.items()[0].data, messageText); - } catch (AblyException e) { - e.printStackTrace(); - fail("single_history_binary: Unexpected exception instantiating library"); - } finally { - if(ably != null) - ably.close(); - } - } - - /** - * Send a single message on a channel, wait enough time for it to - * persist, then send a second message. Verify that both can be - * retrieved using channel.history() without any further wait. - */ - @Test - public void channelhistory_mixed_b() { - AblyRealtime ably = null; - try { - ClientOptions opts = createOptions(testVars.keys[0].keyStr); - ably = new AblyRealtime(opts); - String channelName = "persisted:channelhistory_mixed_b_" + testParams.name; - String messageText = "Test message (channelhistory_mixed_b)"; - String persistEventName = "test_event (persisted)"; - String liveEventName = "test_event (live)"; - - /* create a channel */ - final Channel channel = ably.channels.get(channelName); - - /* attach */ - channel.attach(); - (new ChannelWaiter(channel)).waitFor(ChannelState.attached); - assertEquals("Verify attached state reached", channel.state, ChannelState.attached); - - /* publish to the channel */ - CompletionWaiter msgComplete = new CompletionWaiter(); - channel.publish(persistEventName, messageText, msgComplete); - - /* wait for the history to be persisted */ - try { - Thread.sleep(16000); - } catch(InterruptedException ie) {} - - /* publish to the channel */ - msgComplete = new CompletionWaiter(); - channel.publish(liveEventName, messageText, msgComplete); - - /* wait for the publish callback to be called */ - msgComplete.waitFor(); - assertTrue("Verify success callback was called", msgComplete.success); - - /* get the history for this channel */ - PaginatedResult messages = channel.history(null); - assertNotNull("Expected non-null messages", messages); - assertEquals("Expected 2 messages", messages.items().length, 2); - - /* verify message contents */ - assertEquals("Expect correct message event", messages.items()[0].name, liveEventName); - assertEquals("Expect correct message text", messages.items()[0].data, messageText); - assertEquals("Expect correct message event", messages.items()[1].name, persistEventName); - assertEquals("Expect correct message text", messages.items()[1].data, messageText); - - } catch (AblyException e) { - e.printStackTrace(); - fail("single_history_binary: Unexpected exception instantiating library"); - } finally { - if(ably != null) - ably.close(); - } - } - - /** - * Send a single message on a channel, wait enough time for it to - * persist, then send a second message. Verify that both can be - * retrieved using channel.history(direction=forwards) without any - * further wait. - */ - @Test - public void channelhistory_mixed_f() { - AblyRealtime ably = null; - try { - ClientOptions opts = createOptions(testVars.keys[0].keyStr); - ably = new AblyRealtime(opts); - String channelName = "persisted:channelhistory_mixed_f_" + testParams.name; - String messageText = "Test message (channelhistory_mixed_f)"; - String persistEventName = "test_event (persisted)"; - String liveEventName = "test_event (live)"; - - /* create a channel */ - final Channel channel = ably.channels.get(channelName); - - /* attach */ - channel.attach(); - (new ChannelWaiter(channel)).waitFor(ChannelState.attached); - assertEquals("Verify attached state reached", channel.state, ChannelState.attached); - - /* publish to the channel */ - CompletionWaiter msgComplete = new CompletionWaiter(); - channel.publish(persistEventName, messageText, msgComplete); - - /* wait for the history to be persisted */ - try { - Thread.sleep(16000); - } catch(InterruptedException ie) {} - - /* publish to the channel */ - msgComplete = new CompletionWaiter(); - channel.publish(liveEventName, messageText, msgComplete); - - /* wait for the publish callback to be called */ - msgComplete.waitFor(); - assertTrue("Verify success callback was called", msgComplete.success); - - /* get the history for this channel */ - PaginatedResult messages = channel.history(new Param[]{ new Param("direction", "forwards") }); - assertNotNull("Expected non-null messages", messages); - assertEquals("Expected 2 messages", messages.items().length, 2); - - /* verify message contents */ - assertEquals("Expect correct message event", messages.items()[0].name, persistEventName); - assertEquals("Expect correct message text", messages.items()[0].data, messageText); - assertEquals("Expect correct message event", messages.items()[1].name, liveEventName); - assertEquals("Expect correct message text", messages.items()[1].data, messageText); - - } catch (AblyException e) { - e.printStackTrace(); - fail("single_history_binary: Unexpected exception instantiating library"); - } finally { - if(ably != null) - ably.close(); - } - } - - /** - * Publish events, get limited history and check expected order (forwards) - */ - @Test - public void channelhistory_limit_f() { - AblyRealtime ably = null; - try { - ClientOptions opts = createOptions(testVars.keys[0].keyStr); - ably = new AblyRealtime(opts); - String channelName = "persisted:channelhistory_limit_f_" + testParams.name; - - /* create a channel */ - final Channel channel = ably.channels.get(channelName); - - /* attach */ - channel.attach(); - (new ChannelWaiter(channel)).waitFor(ChannelState.attached); - assertEquals("Verify attached state reached", channel.state, ChannelState.attached); - - /* publish to the channel */ - CompletionSet msgComplete = new CompletionSet(); - for(int i = 0; i < 50; i++) { - try { - channel.publish("history" + i, String.valueOf(i), msgComplete.add()); - } catch(AblyException e) { - e.printStackTrace(); - fail("channelhistory_limit_f: Unexpected exception"); - return; - } - } - - /* wait for the publish callbacks to be called */ - msgComplete.waitFor(); - assertTrue("Verify success callback was called", msgComplete.errors.isEmpty()); - - /* get the history for this channel */ - PaginatedResult messages = channel.history(new Param[] { new Param("direction", "forwards"), new Param("limit", "25") }); - assertNotNull("Expected non-null messages", messages); - assertEquals("Expected 25 messages", messages.items().length, 25); - HashMap messageContents = new HashMap(); - for(Message message : messages.items()) - messageContents.put(message.name, message); - /* verify message order */ - Message[] expectedMessageHistory = new Message[25]; - for(int i = 0; i < 25; i++) - expectedMessageHistory[i] = messageContents.get("history" + i); - Assert.assertArrayEquals("Expect messages in forward order", messages.items(), expectedMessageHistory); - } catch (AblyException e) { - e.printStackTrace(); - fail("channelhistory_limit_f: Unexpected exception"); - } finally { - if(ably != null) - ably.close(); - } - } - - /** - * Publish events, get limited history and check expected order (backwards) - */ - @Test - public void channelhistory_limit_b() { - AblyRealtime ably = null; - try { - ClientOptions opts = createOptions(testVars.keys[0].keyStr); - ably = new AblyRealtime(opts); - String channelName = "persisted:channelhistory_limit_b_" + testParams.name; - - /* create a channel */ - final Channel channel = ably.channels.get(channelName); - - /* attach */ - channel.attach(); - (new ChannelWaiter(channel)).waitFor(ChannelState.attached); - assertEquals("Verify attached state reached", channel.state, ChannelState.attached); - - /* publish to the channel */ - CompletionSet msgComplete = new CompletionSet(); - for(int i = 0; i < 50; i++) { - try { - channel.publish("history" + i, String.valueOf(i), msgComplete.add()); - } catch(AblyException e) { - e.printStackTrace(); - fail("channelhistory_limit_b: Unexpected exception"); - return; - } - } - - /* wait for the publish callbacks to be called */ - msgComplete.waitFor(); - assertTrue("Verify success callback was called", msgComplete.errors.isEmpty()); - - /* get the history for this channel */ - PaginatedResult messages = channel.history(new Param[] { new Param("direction", "backwards"), new Param("limit", "25") }); - assertNotNull("Expected non-null messages", messages); - assertEquals("Expected 25 messages", messages.items().length, 25); - HashMap messageContents = new HashMap(); - for(Message message : messages.items()) - messageContents.put(message.name, message); - - /* verify message order */ - Message[] expectedMessageHistory = new Message[25]; - for(int i = 0; i < 25; i++) - expectedMessageHistory[i] = messageContents.get("history" + (49 - i)); - Assert.assertArrayEquals("Expect messages in backward order", messages.items(), expectedMessageHistory); - } catch (AblyException e) { - e.printStackTrace(); - fail("channelhistory_limit_b: Unexpected exception"); - return; - } finally { - if(ably != null) - ably.close(); - } - } - - /** - * Publish events and check expected history based on time slice (forwards) - */ - @Test - public void channelhistory_time_f() { - AblyRealtime ably = null; - try { - /* first, publish some messages */ - long intervalStart = 0, intervalEnd = 0; - ClientOptions opts = createOptions(testVars.keys[0].keyStr); - ably = new AblyRealtime(opts); - String channelName = "persisted:channelhistory_time_f_" + testParams.name; - - /* create a channel */ - final Channel channel = ably.channels.get(channelName); - - /* attach */ - channel.attach(); - (new ChannelWaiter(channel)).waitFor(ChannelState.attached); - assertEquals("Verify attached state reached", channel.state, ChannelState.attached); - - /* send batches of messages with shprt inter-message delay */ - CompletionSet msgComplete = new CompletionSet(); - for(int i = 0; i < 20; i++) { - channel.publish("history" + i, String.valueOf(i), msgComplete.add()); - Thread.sleep(100L); - } - Thread.sleep(1000L); - intervalStart = timeOffset + System.currentTimeMillis(); - for(int i = 20; i < 40; i++) { - channel.publish("history" + i, String.valueOf(i), msgComplete.add()); - Thread.sleep(100L); - } - intervalEnd = timeOffset + System.currentTimeMillis() - 1; - Thread.sleep(1000L); - for(int i = 40; i < 60; i++) { - channel.publish("history" + i, String.valueOf(i), msgComplete.add()); - Thread.sleep(100L); - } - - /* wait for message callbacks */ - msgComplete.waitFor(); - - /* get the history for this channel */ - PaginatedResult messages = channel.history(new Param[] { - new Param("direction", "forwards"), - new Param("start", String.valueOf(intervalStart - 500)), - new Param("end", String.valueOf(intervalEnd + 500)) - }); - assertNotNull("Expected non-null messages", messages); - assertEquals("Expected 20 messages", messages.items().length, 20); - HashMap messageContents = new HashMap(); - for(Message message : messages.items()) - messageContents.put(message.name, message); - /* verify message order */ - Message[] expectedMessageHistory = new Message[20]; - for(int i = 20; i < 40; i++) - expectedMessageHistory[i - 20] = messageContents.get("history" + i); - Assert.assertArrayEquals("Expect messages in forward order", messages.items(), expectedMessageHistory); - } catch (AblyException e) { - e.printStackTrace(); - fail("channelhistory_time_f: Unexpected exception"); - } catch (InterruptedException e) { - e.printStackTrace(); - fail("channelhistory_time_f: Unexpected exception"); - } finally { - if(ably != null) - ably.close(); - } - } - - /** - * Publish events and check expected history based on time slice (backwards) - */ - @Test - public void channelhistory_time_b() { - AblyRealtime ably = null; - try { - /* first, publish some messages */ - long intervalStart = 0, intervalEnd = 0; - ClientOptions opts = createOptions(testVars.keys[0].keyStr); - ably = new AblyRealtime(opts); - String channelName = "persisted:channelhistory_time_b_" + testParams.name; - - /* create a channel */ - final Channel channel = ably.channels.get(channelName); - - /* attach */ - channel.attach(); - (new ChannelWaiter(channel)).waitFor(ChannelState.attached); - assertEquals("Verify attached state reached", channel.state, ChannelState.attached); - - /* send batches of messages with shprt inter-message delay */ - CompletionSet msgComplete = new CompletionSet(); - for(int i = 0; i < 20; i++) { - channel.publish("history" + i, String.valueOf(i), msgComplete.add()); - Thread.sleep(100L); - } - Thread.sleep(1000L); - intervalStart = timeOffset + System.currentTimeMillis(); - for(int i = 20; i < 40; i++) { - channel.publish("history" + i, String.valueOf(i), msgComplete.add()); - Thread.sleep(100L); - } - intervalEnd = timeOffset + System.currentTimeMillis() - 1; - Thread.sleep(1000L); - for(int i = 40; i < 60; i++) { - channel.publish("history" + i, String.valueOf(i), msgComplete.add()); - Thread.sleep(100L); - } - - /* wait for message callbacks */ - msgComplete.waitFor(); - - /* get the history for this channel */ - PaginatedResult messages = channel.history(new Param[] { - new Param("direction", "backwards"), - new Param("start", String.valueOf(intervalStart - 500)), - new Param("end", String.valueOf(intervalEnd + 500)) - }); - assertNotNull("Expected non-null messages", messages); - assertEquals("Expected 20 messages", messages.items().length, 20); - HashMap messageContents = new HashMap(); - for(Message message : messages.items()) - messageContents.put(message.name, message); - /* verify message order */ - Message[] expectedMessageHistory = new Message[20]; - for(int i = 20; i < 40; i++) - expectedMessageHistory[i - 20] = messageContents.get("history" + (59 - i)); - Assert.assertArrayEquals("Expect messages in backwards order", messages.items(), expectedMessageHistory); - } catch (AblyException e) { - e.printStackTrace(); - fail("channelhistory_time_b: Unexpected exception"); - } catch (InterruptedException e) { - e.printStackTrace(); - fail("channelhistory_time_b: Unexpected exception"); - } finally { - if(ably != null) - ably.close(); - } - } - - /** - * Check query pagination (forwards) - */ - @Test - public void channelhistory_paginate_f() { - AblyRealtime ably = null; - try { - ClientOptions opts = createOptions(testVars.keys[0].keyStr); - ably = new AblyRealtime(opts); - String channelName = "persisted:channelhistory_paginate_f_" + testParams.name; - - /* create a channel */ - final Channel channel = ably.channels.get(channelName); - - /* attach */ - channel.attach(); - (new ChannelWaiter(channel)).waitFor(ChannelState.attached); - assertEquals("Verify attached state reached", channel.state, ChannelState.attached); - - /* publish to the channel */ - CompletionSet msgComplete = new CompletionSet(); - for(int i = 0; i < 50; i++) { - try { - channel.publish("history" + i, String.valueOf(i), msgComplete.add()); - } catch(AblyException e) { - e.printStackTrace(); - fail("channelhistory_paginate_f: Unexpected exception"); - return; - } - } - - /* wait for the publish callbacks to be called */ - msgComplete.waitFor(); - assertTrue("Verify success callback was called", msgComplete.errors.isEmpty()); - - /* get the history for this channel */ - PaginatedResult messages = channel.history(new Param[] { new Param("direction", "forwards"), new Param("limit", "10") }); - assertNotNull("Expected non-null messages", messages); - assertEquals("Expected 10 messages", messages.items().length, 10); - - /* log all messages */ - HashMap messageContents = new HashMap(); - for(Message message : messages.items()) - messageContents.put(message.name, message); - - /* verify message order */ - Message[] expectedMessageHistory = new Message[10]; - for(int i = 0; i < 10; i++) - expectedMessageHistory[i] = messageContents.get("history" + i); - Assert.assertArrayEquals("Expect messages in forward order", messages.items(), expectedMessageHistory); - - /* get next page */ - messages = messages.next(); - assertNotNull("Expected non-null messages", messages); - assertEquals("Expected 10 messages", messages.items().length, 10); - - /* log all messages */ - for(Message message : messages.items()) - messageContents.put(message.name, message); - - /* verify message order */ - for(int i = 0; i < 10; i++) - expectedMessageHistory[i] = messageContents.get("history" + String.valueOf(i + 10)); - Assert.assertArrayEquals("Expect messages in forward order", messages.items(), expectedMessageHistory); - - /* get next page */ - messages = messages.next(); - assertNotNull("Expected non-null messages", messages); - assertEquals("Expected 10 messages", messages.items().length, 10); - - /* log all messages */ - for(Message message : messages.items()) - messageContents.put(message.name, message); - - /* verify message order */ - for(int i = 0; i < 10; i++) - expectedMessageHistory[i] = messageContents.get("history" + String.valueOf(i + 20)); - Assert.assertArrayEquals("Expect messages in forward order", messages.items(), expectedMessageHistory); - - } catch (AblyException e) { - e.printStackTrace(); - fail("channelhistory_paginate_f: Unexpected exception"); - } finally { - if(ably != null) - ably.close(); - } - } - - /** - * Check query pagination (backwards) - */ - @Test - public void channelhistory_paginate_b() { - AblyRealtime ably = null; - try { - ClientOptions opts = createOptions(testVars.keys[0].keyStr); - ably = new AblyRealtime(opts); - String channelName = "persisted:channelhistory_paginate_b_" + testParams.name; - - /* create a channel */ - final Channel channel = ably.channels.get(channelName); - - /* attach */ - channel.attach(); - (new ChannelWaiter(channel)).waitFor(ChannelState.attached); - assertEquals("Verify attached state reached", channel.state, ChannelState.attached); - - /* publish to the channel */ - CompletionSet msgComplete = new CompletionSet(); - for(int i = 0; i < 50; i++) { - try { - channel.publish("history" + i, String.valueOf(i), msgComplete.add()); - } catch(AblyException e) { - e.printStackTrace(); - fail("channelhistory_paginate_b: Unexpected exception"); - return; - } - } - - /* wait for the publish callbacks to be called */ - msgComplete.waitFor(); - assertTrue("Verify success callback was called", msgComplete.errors.isEmpty()); - - /* get the history for this channel */ - PaginatedResult messages = channel.history(new Param[] { new Param("direction", "backwards"), new Param("limit", "10") }); - assertNotNull("Expected non-null messages", messages); - assertEquals("Expected 10 messages", messages.items().length, 10); - - /* log all messages */ - HashMap messageContents = new HashMap(); - for(Message message : messages.items()) - messageContents.put(message.name, message); - - /* verify message order */ - Message[] expectedMessageHistory = new Message[10]; - for(int i = 0; i < 10; i++) - expectedMessageHistory[i] = messageContents.get("history" + String.valueOf(49 - i)); - Assert.assertArrayEquals("Expect messages in forward order", messages.items(), expectedMessageHistory); - - /* get next page */ - messages = messages.next(); - assertNotNull("Expected non-null messages", messages); - assertEquals("Expected 10 messages", messages.items().length, 10); - - /* log all messages */ - for(Message message : messages.items()) - messageContents.put(message.name, message); - - /* verify message order */ - for(int i = 0; i < 10; i++) - expectedMessageHistory[i] = messageContents.get("history" + String.valueOf(39 - i)); - Assert.assertArrayEquals("Expect messages in forward order", messages.items(), expectedMessageHistory); - - /* get next page */ - messages = messages.next(); - assertNotNull("Expected non-null messages", messages); - assertEquals("Expected 10 messages", messages.items().length, 10); - - /* log all messages */ - for(Message message : messages.items()) - messageContents.put(message.name, message); - - /* verify message order */ - for(int i = 0; i < 10; i++) - expectedMessageHistory[i] = messageContents.get("history" + String.valueOf(29 - i)); - Assert.assertArrayEquals("Expect messages in forward order", messages.items(), expectedMessageHistory); - - } catch (AblyException e) { - e.printStackTrace(); - fail("channelhistory_paginate_b: Unexpected exception"); - } finally { - if(ably != null) - ably.close(); - } - } - - /** - * Check query pagination "rel=first" (forwards) - */ - @Test - public void channelhistory_paginate_first_f() { - AblyRealtime ably = null; - try { - ClientOptions opts = createOptions(testVars.keys[0].keyStr); - ably = new AblyRealtime(opts); - String channelName = "persisted:channelhistory_paginate_first_f_" + testParams.name; - - /* create a channel */ - final Channel channel = ably.channels.get(channelName); - - /* attach */ - channel.attach(); - (new ChannelWaiter(channel)).waitFor(ChannelState.attached); - assertEquals("Verify attached state reached", channel.state, ChannelState.attached); - - /* publish to the channel */ - CompletionSet msgComplete = new CompletionSet(); - for(int i = 0; i < 50; i++) { - try { - channel.publish("history" + i, String.valueOf(i), msgComplete.add()); - } catch(AblyException e) { - e.printStackTrace(); - fail("channelhistory_paginate_f: Unexpected exception"); - return; - } - } - - /* wait for the publish callbacks to be called */ - msgComplete.waitFor(); - assertTrue("Verify success callback was called", msgComplete.errors.isEmpty()); - - /* get the history for this channel */ - PaginatedResult messages = channel.history(new Param[] { new Param("direction", "forwards"), new Param("limit", "10") }); - assertNotNull("Expected non-null messages", messages); - assertEquals("Expected 10 messages", messages.items().length, 10); - - /* log all messages */ - HashMap messageContents = new HashMap(); - for(Message message : messages.items()) - messageContents.put(message.name, message); - - /* verify message order */ - Message[] expectedMessageHistory = new Message[10]; - for(int i = 0; i < 10; i++) - expectedMessageHistory[i] = messageContents.get("history" + i); - Assert.assertArrayEquals("Expect messages in forward order", messages.items(), expectedMessageHistory); - - /* get next page */ - messages = messages.next(); - assertNotNull("Expected non-null messages", messages); - assertEquals("Expected 10 messages", messages.items().length, 10); - - /* log all messages */ - for(Message message : messages.items()) - messageContents.put(message.name, message); - - /* verify message order */ - for(int i = 0; i < 10; i++) - expectedMessageHistory[i] = messageContents.get("history" + String.valueOf(i + 10)); - Assert.assertArrayEquals("Expect messages in forward order", messages.items(), expectedMessageHistory); - - /* get first page */ - messages = messages.first(); - assertNotNull("Expected non-null messages", messages); - assertEquals("Expected 10 messages", messages.items().length, 10); - - /* log all messages */ - for(Message message : messages.items()) - messageContents.put(message.name, message); - - /* verify message order */ - for(int i = 0; i < 10; i++) - expectedMessageHistory[i] = messageContents.get("history" + String.valueOf(i)); - Assert.assertArrayEquals("Expect messages in forward order", messages.items(), expectedMessageHistory); - - } catch (AblyException e) { - e.printStackTrace(); - fail("channelhistory_paginate_first_f: Unexpected exception"); - } finally { - if(ably != null) - ably.close(); - } - } - - /** - * Check query pagination "rel=first" (backwards) - */ - @Test - public void channelhistory_paginate_first_b() { - AblyRealtime ably = null; - try { - ClientOptions opts = createOptions(testVars.keys[0].keyStr); - ably = new AblyRealtime(opts); - String channelName = "persisted:channelhistory_paginate_first_b_" + testParams.name; - - /* create a channel */ - final Channel channel = ably.channels.get(channelName); - - /* attach */ - channel.attach(); - (new ChannelWaiter(channel)).waitFor(ChannelState.attached); - assertEquals("Verify attached state reached", channel.state, ChannelState.attached); - - /* publish to the channel */ - CompletionSet msgComplete = new CompletionSet(); - for(int i = 0; i < 50; i++) { - try { - channel.publish("history" + i, String.valueOf(i), msgComplete.add()); - } catch(AblyException e) { - e.printStackTrace(); - fail("channelhistory_paginate_f: Unexpected exception"); - return; - } - } - - /* wait for the publish callbacks to be called */ - msgComplete.waitFor(); - assertTrue("Verify success callback was called", msgComplete.errors.isEmpty()); - - /* get the history for this channel */ - PaginatedResult messages = channel.history(new Param[] { new Param("direction", "backwards"), new Param("limit", "10") }); - assertNotNull("Expected non-null messages", messages); - assertEquals("Expected 10 messages", messages.items().length, 10); - - /* log all messages */ - HashMap messageContents = new HashMap(); - for(Message message : messages.items()) - messageContents.put(message.name, message); - - /* verify message order */ - Message[] expectedMessageHistory = new Message[10]; - for(int i = 0; i < 10; i++) - expectedMessageHistory[i] = messageContents.get("history" + String.valueOf(49 - i)); - Assert.assertArrayEquals("Expect messages in forward order", messages.items(), expectedMessageHistory); - - /* get next page */ - messages = messages.next(); - assertNotNull("Expected non-null messages", messages); - assertEquals("Expected 10 messages", messages.items().length, 10); - - /* log all messages */ - for(Message message : messages.items()) - messageContents.put(message.name, message); - - /* verify message order */ - for(int i = 0; i < 10; i++) - expectedMessageHistory[i] = messageContents.get("history" + String.valueOf(39 - i)); - Assert.assertArrayEquals("Expect messages in forward order", messages.items(), expectedMessageHistory); - - /* get first page */ - messages = messages.first(); - assertNotNull("Expected non-null messages", messages); - assertEquals("Expected 10 messages", messages.items().length, 10); - - /* log all messages */ - for(Message message : messages.items()) - messageContents.put(message.name, message); - - /* verify message order */ - for(int i = 0; i < 10; i++) - expectedMessageHistory[i] = messageContents.get("history" + String.valueOf(49 - i)); - Assert.assertArrayEquals("Expect messages in forward order", messages.items(), expectedMessageHistory); - - } catch (AblyException e) { - e.printStackTrace(); - fail("channelhistory_paginate_first_b: Unexpected exception"); - } finally { - if(ably != null) - ably.close(); - } - } - - /** - * Connect twice to the service, each using the default (binary) protocol. - * Publish messages on one connection to a given channel; while in progress, - * attach the second connection to the same channel and verify a message - * history up to the point of attachment can be obtained. - */ - @Test - @Ignore("Fails due to issues in sandbox. See https://github.com/ably/realtime/issues/1834 for details.") - public void channelhistory_from_attach() { - AblyRealtime txAbly = null, rxAbly = null; - try { - ClientOptions txOpts = createOptions(testVars.keys[0].keyStr); - txAbly = new AblyRealtime(txOpts); - ClientOptions rxOpts = createOptions(testVars.keys[0].keyStr); - rxAbly = new AblyRealtime(rxOpts); - String channelName = "persisted:channelhistory_from_attach_" + testParams.name; - - /* create a channel */ - final Channel txChannel = txAbly.channels.get(channelName); - final Channel rxChannel = rxAbly.channels.get(channelName); - - /* attach sender */ - txChannel.attach(); - (new ChannelWaiter(txChannel)).waitFor(ChannelState.attached); - assertEquals("Verify attached state reached", txChannel.state, ChannelState.attached); - - /* publish messages to the channel */ - final CompletionSet msgComplete = new CompletionSet(); - Thread publisherThread = new Thread() { - @Override - public void run() { - for(int i = 0; i < 50; i++) { - try { - txChannel.publish("history" + i, String.valueOf(i), msgComplete.add()); - try { - sleep(100L); - } catch(InterruptedException ie) {} - } catch(AblyException e) { - e.printStackTrace(); - fail("channelhistory_from_attach: Unexpected exception"); - return; - } - } - } - }; - publisherThread.start(); - - /* wait 2 seconds */ - try { - Thread.sleep(2000L); - } catch(InterruptedException ie) {} - - /* subscribe; this will trigger the attach */ - MessageWaiter messageWaiter = new MessageWaiter(rxChannel); - - /* get the channel history from the attachSerial when we get the attach indication */ - (new ChannelWaiter(rxChannel)).waitFor(ChannelState.attached); - assertEquals("Verify attached state reached", rxChannel.state, ChannelState.attached); - assertNotNull("Verify attachSerial provided", rxChannel.properties.attachSerial); - - /* wait for the subscription callback to be called on the first received message */ - messageWaiter.waitFor(1); - - /* wait for the publisher thread to complete */ - try { - publisherThread.join(); - } catch (InterruptedException e) { - fail("channelhistory_from_attach: exception in publisher thread"); - } - - /* get the history for this channel */ - PaginatedResult messages = rxChannel.history(new Param[] { new Param("from_serial", rxChannel.properties.attachSerial)}); - assertNotNull("Expected non-null messages", messages); - assertTrue("Expected at least one message", messages.items().length >= 1); - - /* verify that the history and received messages meet */ - int earliestReceivedOnConnection = Integer.valueOf((String)messageWaiter.receivedMessages.get(0).data).intValue(); - int latestReceivedInHistory = Integer.valueOf((String)messages.items()[0].data).intValue(); - assertEquals("Verify that the history and received messages meet", earliestReceivedOnConnection, latestReceivedInHistory + 1); - - } catch (AblyException e) { - e.printStackTrace(); - fail("channelhistory_from_attach: Unexpected exception instantiating library"); - } finally { - if(txAbly != null) - txAbly.close(); - if(rxAbly != null) - rxAbly.close(); - } - } - - /** - * Connect twice to the service. - * Publish messages on one connection to a given channel; while in progress, - * attach the second connection to the same channel and verify a message - * history up to the point of attachment can be obtained. - */ - @Test - public void channelhistory_until_attach() { - AblyRealtime txAbly = null, rxAbly = null; - try { - ClientOptions txOpts = createOptions(testVars.keys[0].keyStr); - txAbly = new AblyRealtime(txOpts); - ClientOptions rxOpts = createOptions(testVars.keys[0].keyStr); - rxAbly = new AblyRealtime(rxOpts); - String channelName = "persisted:channelhistory_until_attach_" + testParams.name; - - /* create a channel */ - final Channel txChannel = txAbly.channels.get(channelName); - final Channel rxChannel = rxAbly.channels.get(channelName); - - /* attach sender */ - txChannel.attach(); - (new ChannelWaiter(txChannel)).waitFor(ChannelState.attached); - assertEquals("Verify attached state reached", txChannel.state, ChannelState.attached); - - /* publish messages to the channel */ - CompletionSet msgComplete = new CompletionSet(); - int messageCount = 25; - for (int i = 0; i < messageCount; i++) { - txChannel.publish("history" + i, String.valueOf(i), msgComplete.add()); - } - - msgComplete.waitFor(); - - /* get the channel history from the attachSerial when we get the attach indication */ - rxChannel.attach(); - new ChannelWaiter(rxChannel).waitFor(ChannelState.attached); - assertEquals("Verify attached state reached", rxChannel.state, ChannelState.attached); - assertNotNull("Verify attachSerial provided", rxChannel.properties.attachSerial); - - /* get the history for this channel */ - PaginatedResult messages = rxChannel.history(new Param[] { new Param("untilAttach", "true") }); - assertNotNull("Expected non-null messages", messages); - assertTrue("Expected at least one message", messages.items().length >= 1); - - /* verify that the history and received messages meet */ - for (int i = 0; i < messageCount; i++) { - /* 0 --> "24" - * 1 --> "23" - * ... - * 24 --> "0" - */ - String actual = (String) messages.items()[messageCount - 1 - i].data; - String expected = String.valueOf(i); - assertThat(actual, is(equalTo(expected))); - } - } catch (AblyException e) { - e.printStackTrace(); - fail("channelhistory_from_attach: Unexpected exception instantiating library"); - } finally { - if(txAbly != null) - txAbly.close(); - if(rxAbly != null) - rxAbly.close(); - } - } - - /** - * Verifies an Exception is thrown, when a channel history is requested - * with parameter {"untilAttach":"true}" before client is attached to the channel - * - * @throws AblyException - */ - @Test(expected=AblyException.class) - public void channelhistory_until_attach_before_attached() throws AblyException { - ClientOptions options = createOptions(testVars.keys[0].keyStr); - AblyRealtime ably = new AblyRealtime(options); - - ably.channels.get("test").history(new Param[]{ new Param("untilAttach", "true") }); - } - - /** - * Verifies an Exception is thrown, when a channel history is requested - * with invalid "untilAttach" parameter value. - * - * @throws AblyException - */ - @Test(expected=AblyException.class) - public void channelhistory_until_attach_invalid_value() throws AblyException { - ClientOptions options = createOptions(testVars.keys[0].keyStr); - AblyRealtime ably = new AblyRealtime(options); - - ably.channels.get("test").history(new Param[]{ new Param("untilAttach", "affirmative")}); - } - - /** - * Publish enough message to fill 2 pages. - * Verify that, - * - {@code PaginatedQuery#isLast} returns false, when we are at the first page. - * - {@code PaginatedQuery#isLast} returns true, when we are at the second page. - */ - @Test - public void channelhistory_islast() throws AblyException { - AblyRealtime ably = null; - try { - ClientOptions opts = createOptions(testVars.keys[0].keyStr); - ably = new AblyRealtime(opts); - String channelName = "persisted:channelhistory_islast_" + testParams.name; - int pageMessageCount = 10; - - /* create a channel */ - final Channel channel = ably.channels.get(channelName); - - /* attach */ - channel.attach(); - new ChannelWaiter(channel).waitFor(ChannelState.attached); - assertEquals("Verify attached state reached", channel.state, ChannelState.attached); - - /* publish to the channel */ - CompletionSet msgComplete = new CompletionSet(); - for (int i = 0; i < (pageMessageCount * 2 - 1); i++) { - channel.publish("history" + i, String.valueOf(i), msgComplete.add()); - } - - /* wait for the publish callbacks to be called */ - msgComplete.waitFor(); - assertTrue("Verify success callback was called", msgComplete.errors.isEmpty()); - - /* get the history for this channel */ - PaginatedResult messages = channel.history(new Param[]{new Param("limit", String.format(Locale.ENGLISH, "%d", pageMessageCount))}); - assertNotNull("Expected non-null messages", messages); - assertEquals("Expected 10 messages", messages.items().length, pageMessageCount); - - /* Verify that current page is the last */ - assertThat(messages.isLast(), is(false)); - - /* get next page */ - messages = messages.next(); - assertNotNull("Expected non-null messages", messages); - assertEquals("Expected 9 messages", messages.items().length, pageMessageCount - 1); - - /* Verify that current page is the last */ - assertThat(messages.isLast(), is(true)); - } finally { - if (ably != null) - ably.close(); - } - } + private AblyRealtime ably; + private long timeOffset; + + @Rule + public Timeout testTimeout = Timeout.seconds(300); + + @Before + public void setUpBefore() throws Exception { + ClientOptions opts = createOptions(testVars.keys[0].keyStr); + ably = new AblyRealtime(opts); + long timeFromService = ably.time(); + timeOffset = timeFromService - System.currentTimeMillis(); + } + + /** + * Send a single message on a channel and verify that it can be + * retrieved using channel.history() without needing to wait for + * it to be persisted. + */ + @Test + public void channelhistory_simple() { + AblyRealtime ably = null; + try { + ClientOptions opts = createOptions(testVars.keys[0].keyStr); + ably = new AblyRealtime(opts); + String channelName = "persisted:channelhistory_simple_" + testParams.name; + String messageText = "Test message (channelhistory_simple)"; + + /* create a channel */ + final Channel channel = ably.channels.get(channelName); + + /* attach */ + channel.attach(); + (new ChannelWaiter(channel)).waitFor(ChannelState.attached); + assertEquals("Verify attached state reached", channel.state, ChannelState.attached); + + /* publish to the channel */ + CompletionWaiter msgComplete = new CompletionWaiter(); + channel.publish("test_event", messageText, msgComplete); + + /* wait for the publish callback to be called */ + msgComplete.waitFor(); + assertTrue("Verify success callback was called", msgComplete.success); + + /* get the history for this channel */ + PaginatedResult messages = channel.history(null); + assertNotNull("Expected non-null messages", messages); + assertEquals("Expected 1 message", messages.items().length, 1); + + /* verify message contents */ + assertEquals("Expect correct message text", messages.items()[0].data, messageText); + } catch (AblyException e) { + e.printStackTrace(); + fail("single_history_binary: Unexpected exception instantiating library"); + } finally { + if(ably != null) + ably.close(); + } + } + + /** + * Send couple of messages without listener on a channel and + * verify that it can be retrieved using channel.history() + * without needing to wait for it to be persisted. + */ + @Test + public void channelhistory_simple_withoutlistener() { + AblyRealtime ably = null; + try { + ClientOptions opts = createOptions(testVars.keys[0].keyStr); + ably = new AblyRealtime(opts); + String channelName = "persisted:channelhistory_simple_withoutlistener_" + testParams.name; + String message1Text = "Test message 1 (channelhistory_simple_withoutlistener)"; + Message message2 = new Message("test_event", "Test message 2 (channelhistory_simple_withoutlistener)"); + Message[] messages34 = new Message[] { + new Message("test_event", "Test message 3 (channelhistory_simple_withoutlistener)"), + new Message("test_event", "Test message 4 (channelhistory_simple_withoutlistener)") + }; + + /* create a channel */ + final Channel channel = ably.channels.get(channelName); + + /* attach */ + channel.attach(); + (new ChannelWaiter(channel)).waitFor(ChannelState.attached); + assertEquals("Verify attached state reached", channel.state, ChannelState.attached); + + /* publish to the channel */ + channel.publish("test_event", message1Text); + channel.publish(message2); + channel.publish(messages34); + + /* Get history for the channel. Wait for no longer than 2 seconds for the history to be populated */ + PaginatedResult messages; + int n = 0; + do { + messages = channel.history(null); + if (messages.items().length < 4) { + try { Thread.sleep(100); } catch (InterruptedException e) {} + } + } while (messages.items().length < 4 && ++n < 20); + + assertNotNull("Expected non-null messages", messages); + assertEquals("Expected 4 message", messages.items().length, 4); + + /* verify we received message history from most recent to older */ + assertEquals("Expect correct message text", messages.items()[0].data, messages34[1].data); + assertEquals("Expect correct message text", messages.items()[1].data, messages34[0].data); + assertEquals("Expect correct message text", messages.items()[2].data, message2.data); + assertEquals("Expect correct message text", messages.items()[3].data, message1Text); + } catch (AblyException e) { + e.printStackTrace(); + fail("channelhistory_simple_binary_withoutlistener: Unexpected exception instantiating library"); + } finally { + if(ably != null) + ably.close(); + } + } + + /** + * Publish events with data of various datatypes + */ + @Test + public void channelhistory_types() { + AblyRealtime ably = null; + try { + ClientOptions opts = createOptions(testVars.keys[0].keyStr); + ably = new AblyRealtime(opts); + String channelName = "persisted:channelhistory_types_" + testParams.name; + + /* create a channel */ + final Channel channel = ably.channels.get(channelName); + + /* attach */ + channel.attach(); + (new ChannelWaiter(channel)).waitFor(ChannelState.attached); + assertEquals("Verify attached state reached", channel.state, ChannelState.attached); + + /* publish to the channel */ + CompletionSet msgComplete = new CompletionSet(); + channel.publish("history0", "This is a string message payload", msgComplete.add()); + channel.publish("history1", "This is a byte[] message payload".getBytes(), msgComplete.add()); + + /* wait for the publish callback to be called */ + msgComplete.waitFor(); + assertTrue("Verify success callback was called", msgComplete.errors.isEmpty()); + + /* get the history for this channel */ + PaginatedResult messages = channel.history(null); + assertNotNull("Expected non-null messages", messages); + assertEquals("Expected 2 messages", messages.items().length, 2); + HashMap messageContents = new HashMap(); + + /* verify message contents */ + for(Message message : messages.items()) + messageContents.put(message.name, message); + assertEquals("Expect history0 to be expected String", messageContents.get("history0").data, "This is a string message payload"); + assertEquals("Expect history1 to be expected byte[]", new String((byte[])messageContents.get("history1").data), "This is a byte[] message payload"); + + /* verify message order */ + Message[] expectedMessageHistory = new Message[]{ + messageContents.get("history1"), + messageContents.get("history0") + }; + Assert.assertArrayEquals("Expect messages in reverse order", messages.items(), expectedMessageHistory); + } catch (AblyException e) { + e.printStackTrace(); + fail("channelhistory_types: Unexpected exception"); + } finally { + if(ably != null) + ably.close(); + } + } + + /** + * Publish events with data of various datatypes + */ + @Test + public void channelhistory_types_forward() { + AblyRealtime ably = null; + try { + ClientOptions opts = createOptions(testVars.keys[0].keyStr); + ably = new AblyRealtime(opts); + String channelName = "persisted:channelhistory_types_forward_" + testParams.name; + + /* create a channel */ + final Channel channel = ably.channels.get(channelName); + + /* attach */ + channel.attach(); + (new ChannelWaiter(channel)).waitFor(ChannelState.attached); + assertEquals("Verify attached state reached", channel.state, ChannelState.attached); + + /* publish to the channel */ + CompletionSet msgComplete = new CompletionSet(); + channel.publish("history0", "This is a string message payload", msgComplete.add()); + channel.publish("history1", "This is a byte[] message payload".getBytes(), msgComplete.add()); + + /* wait for the publish callback to be called */ + msgComplete.waitFor(); + assertTrue("Verify success callback was called", msgComplete.errors.isEmpty()); + + /* get the history for this channel */ + PaginatedResult messages = channel.history(new Param[] { new Param("direction", "forwards") }); + assertNotNull("Expected non-null messages", messages); + assertEquals("Expected 2 messages", messages.items().length, 2); + HashMap messageContents = new HashMap(); + + /* verify message contents */ + for(Message message : messages.items()) + messageContents.put(message.name, message); + assertEquals("Expect history0 to be expected String", messageContents.get("history0").data, "This is a string message payload"); + assertEquals("Expect history1 to be expected byte[]", new String((byte[])messageContents.get("history1").data), "This is a byte[] message payload"); + + /* verify message order */ + Message[] expectedMessageHistory = new Message[]{ + messageContents.get("history0"), + messageContents.get("history1") + }; + Assert.assertArrayEquals("Expect messages in sent order", messages.items(), expectedMessageHistory); + } catch (AblyException e) { + e.printStackTrace(); + fail("channelhistory_types_forward: Unexpected exception"); + } finally { + if(ably != null) + ably.close(); + } + } + + /** + * Connect twice to the service, each using the default (binary) protocol. + * Publish messages on one connection to a given channel; then attach + * the second connection to the same channel and verify a complete message + * history can be obtained. + */ + @Test + public void channelhistory_second_channel() { + AblyRealtime txAbly = null, rxAbly = null; + try { + ClientOptions txOpts = createOptions(testVars.keys[0].keyStr); + txAbly = new AblyRealtime(txOpts); + ClientOptions rxOpts = createOptions(testVars.keys[0].keyStr); + rxAbly = new AblyRealtime(rxOpts); + String channelName = "persisted:channelhistory_second_channel_" + testParams.name; + + /* create a channel */ + final Channel txChannel = txAbly.channels.get(channelName); + final Channel rxChannel = rxAbly.channels.get(channelName); + + /* attach sender */ + txChannel.attach(); + (new ChannelWaiter(txChannel)).waitFor(ChannelState.attached); + assertEquals("Verify attached state reached", txChannel.state, ChannelState.attached); + + /* publish to the channel */ + String messageText = "Test message (channelhistory_second_channel)"; + CompletionWaiter msgComplete = new CompletionWaiter(); + txChannel.publish("test_event", messageText, msgComplete); + + /* wait for the publish callback to be called */ + msgComplete.waitFor(); + assertTrue("Verify success callback was called", msgComplete.success); + + /* attach receiver */ + rxChannel.attach(); + (new ChannelWaiter(rxChannel)).waitFor(ChannelState.attached); + assertEquals("Verify attached state reached", rxChannel.state, ChannelState.attached); + + /* get the history for this channel */ + PaginatedResult messages = rxChannel.history(null); + assertNotNull("Expected non-null messages", messages); + assertEquals("Expected 1 message", messages.items().length, 1); + + /* verify message contents */ + assertEquals("Expect correct message text", messages.items()[0].data, messageText); + + } catch (AblyException e) { + e.printStackTrace(); + fail("init0: Unexpected exception instantiating library"); + } finally { + if(txAbly != null) + txAbly.close(); + if(rxAbly != null) + rxAbly.close(); + } + } + + /** + * Send a single message on a channel and verify that it can be + * retrieved using channel.history() after waiting for it to be + * persisted. + */ + @Test + public void channelhistory_wait_b() { + AblyRealtime ably = null; + try { + ClientOptions opts = createOptions(testVars.keys[0].keyStr); + ably = new AblyRealtime(opts); + String channelName = "persisted:channelhistory_wait_b_" + testParams.name; + String messageText = "Test message (channelhistory_wait_b)"; + + /* create a channel */ + final Channel channel = ably.channels.get(channelName); + + /* attach */ + channel.attach(); + (new ChannelWaiter(channel)).waitFor(ChannelState.attached); + assertEquals("Verify attached state reached", channel.state, ChannelState.attached); + + /* publish to the channel */ + CompletionWaiter msgComplete = new CompletionWaiter(); + channel.publish("test_event", messageText, msgComplete); + + /* wait for the publish callback to be called */ + msgComplete.waitFor(); + assertTrue("Verify success callback was called", msgComplete.success); + + /* wait for the history to be persisted */ + try { + Thread.sleep(16000); + } catch(InterruptedException ie) {} + + /* get the history for this channel */ + PaginatedResult messages = channel.history(null); + assertNotNull("Expected non-null messages", messages); + assertEquals("Expected 1 message", messages.items().length, 1); + + /* verify message contents */ + assertEquals("Expect correct message text", messages.items()[0].data, messageText); + } catch (AblyException e) { + e.printStackTrace(); + fail("single_history_binary: Unexpected exception instantiating library"); + } finally { + if(ably != null) + ably.close(); + } + } + + /** + * Send a single message on a channel and verify that it can be + * retrieved using channel.history(direction=forwards) after waiting + * for it to be persisted. + */ + @Test + public void channelhistory_wait_f() { + AblyRealtime ably = null; + try { + ClientOptions opts = createOptions(testVars.keys[0].keyStr); + ably = new AblyRealtime(opts); + String channelName = "persisted:channelhistory_wait_f_" + testParams.name; + String messageText = "Test message (channelhistory_wait_f)"; + + /* create a channel */ + final Channel channel = ably.channels.get(channelName); + + /* attach */ + channel.attach(); + (new ChannelWaiter(channel)).waitFor(ChannelState.attached); + assertEquals("Verify attached state reached", channel.state, ChannelState.attached); + + /* publish to the channel */ + CompletionWaiter msgComplete = new CompletionWaiter(); + channel.publish("test_event", messageText, msgComplete); + + /* wait for the publish callback to be called */ + msgComplete.waitFor(); + assertTrue("Verify success callback was called", msgComplete.success); + + /* wait for the history to be persisted */ + try { + Thread.sleep(16000); + } catch(InterruptedException ie) {} + + /* get the history for this channel */ + PaginatedResult messages = channel.history(new Param[]{ new Param("direction", "forwards") }); + assertNotNull("Expected non-null messages", messages); + assertEquals("Expected 1 message", messages.items().length, 1); + + /* verify message contents */ + assertEquals("Expect correct message text", messages.items()[0].data, messageText); + } catch (AblyException e) { + e.printStackTrace(); + fail("single_history_binary: Unexpected exception instantiating library"); + } finally { + if(ably != null) + ably.close(); + } + } + + /** + * Send a single message on a channel, wait enough time for it to + * persist, then send a second message. Verify that both can be + * retrieved using channel.history() without any further wait. + */ + @Test + public void channelhistory_mixed_b() { + AblyRealtime ably = null; + try { + ClientOptions opts = createOptions(testVars.keys[0].keyStr); + ably = new AblyRealtime(opts); + String channelName = "persisted:channelhistory_mixed_b_" + testParams.name; + String messageText = "Test message (channelhistory_mixed_b)"; + String persistEventName = "test_event (persisted)"; + String liveEventName = "test_event (live)"; + + /* create a channel */ + final Channel channel = ably.channels.get(channelName); + + /* attach */ + channel.attach(); + (new ChannelWaiter(channel)).waitFor(ChannelState.attached); + assertEquals("Verify attached state reached", channel.state, ChannelState.attached); + + /* publish to the channel */ + CompletionWaiter msgComplete = new CompletionWaiter(); + channel.publish(persistEventName, messageText, msgComplete); + + /* wait for the history to be persisted */ + try { + Thread.sleep(16000); + } catch(InterruptedException ie) {} + + /* publish to the channel */ + msgComplete = new CompletionWaiter(); + channel.publish(liveEventName, messageText, msgComplete); + + /* wait for the publish callback to be called */ + msgComplete.waitFor(); + assertTrue("Verify success callback was called", msgComplete.success); + + /* get the history for this channel */ + PaginatedResult messages = channel.history(null); + assertNotNull("Expected non-null messages", messages); + assertEquals("Expected 2 messages", messages.items().length, 2); + + /* verify message contents */ + assertEquals("Expect correct message event", messages.items()[0].name, liveEventName); + assertEquals("Expect correct message text", messages.items()[0].data, messageText); + assertEquals("Expect correct message event", messages.items()[1].name, persistEventName); + assertEquals("Expect correct message text", messages.items()[1].data, messageText); + + } catch (AblyException e) { + e.printStackTrace(); + fail("single_history_binary: Unexpected exception instantiating library"); + } finally { + if(ably != null) + ably.close(); + } + } + + /** + * Send a single message on a channel, wait enough time for it to + * persist, then send a second message. Verify that both can be + * retrieved using channel.history(direction=forwards) without any + * further wait. + */ + @Test + public void channelhistory_mixed_f() { + AblyRealtime ably = null; + try { + ClientOptions opts = createOptions(testVars.keys[0].keyStr); + ably = new AblyRealtime(opts); + String channelName = "persisted:channelhistory_mixed_f_" + testParams.name; + String messageText = "Test message (channelhistory_mixed_f)"; + String persistEventName = "test_event (persisted)"; + String liveEventName = "test_event (live)"; + + /* create a channel */ + final Channel channel = ably.channels.get(channelName); + + /* attach */ + channel.attach(); + (new ChannelWaiter(channel)).waitFor(ChannelState.attached); + assertEquals("Verify attached state reached", channel.state, ChannelState.attached); + + /* publish to the channel */ + CompletionWaiter msgComplete = new CompletionWaiter(); + channel.publish(persistEventName, messageText, msgComplete); + + /* wait for the history to be persisted */ + try { + Thread.sleep(16000); + } catch(InterruptedException ie) {} + + /* publish to the channel */ + msgComplete = new CompletionWaiter(); + channel.publish(liveEventName, messageText, msgComplete); + + /* wait for the publish callback to be called */ + msgComplete.waitFor(); + assertTrue("Verify success callback was called", msgComplete.success); + + /* get the history for this channel */ + PaginatedResult messages = channel.history(new Param[]{ new Param("direction", "forwards") }); + assertNotNull("Expected non-null messages", messages); + assertEquals("Expected 2 messages", messages.items().length, 2); + + /* verify message contents */ + assertEquals("Expect correct message event", messages.items()[0].name, persistEventName); + assertEquals("Expect correct message text", messages.items()[0].data, messageText); + assertEquals("Expect correct message event", messages.items()[1].name, liveEventName); + assertEquals("Expect correct message text", messages.items()[1].data, messageText); + + } catch (AblyException e) { + e.printStackTrace(); + fail("single_history_binary: Unexpected exception instantiating library"); + } finally { + if(ably != null) + ably.close(); + } + } + + /** + * Publish events, get limited history and check expected order (forwards) + */ + @Test + public void channelhistory_limit_f() { + AblyRealtime ably = null; + try { + ClientOptions opts = createOptions(testVars.keys[0].keyStr); + ably = new AblyRealtime(opts); + String channelName = "persisted:channelhistory_limit_f_" + testParams.name; + + /* create a channel */ + final Channel channel = ably.channels.get(channelName); + + /* attach */ + channel.attach(); + (new ChannelWaiter(channel)).waitFor(ChannelState.attached); + assertEquals("Verify attached state reached", channel.state, ChannelState.attached); + + /* publish to the channel */ + CompletionSet msgComplete = new CompletionSet(); + for(int i = 0; i < 50; i++) { + try { + channel.publish("history" + i, String.valueOf(i), msgComplete.add()); + } catch(AblyException e) { + e.printStackTrace(); + fail("channelhistory_limit_f: Unexpected exception"); + return; + } + } + + /* wait for the publish callbacks to be called */ + msgComplete.waitFor(); + assertTrue("Verify success callback was called", msgComplete.errors.isEmpty()); + + /* get the history for this channel */ + PaginatedResult messages = channel.history(new Param[] { new Param("direction", "forwards"), new Param("limit", "25") }); + assertNotNull("Expected non-null messages", messages); + assertEquals("Expected 25 messages", messages.items().length, 25); + HashMap messageContents = new HashMap(); + for(Message message : messages.items()) + messageContents.put(message.name, message); + /* verify message order */ + Message[] expectedMessageHistory = new Message[25]; + for(int i = 0; i < 25; i++) + expectedMessageHistory[i] = messageContents.get("history" + i); + Assert.assertArrayEquals("Expect messages in forward order", messages.items(), expectedMessageHistory); + } catch (AblyException e) { + e.printStackTrace(); + fail("channelhistory_limit_f: Unexpected exception"); + } finally { + if(ably != null) + ably.close(); + } + } + + /** + * Publish events, get limited history and check expected order (backwards) + */ + @Test + public void channelhistory_limit_b() { + AblyRealtime ably = null; + try { + ClientOptions opts = createOptions(testVars.keys[0].keyStr); + ably = new AblyRealtime(opts); + String channelName = "persisted:channelhistory_limit_b_" + testParams.name; + + /* create a channel */ + final Channel channel = ably.channels.get(channelName); + + /* attach */ + channel.attach(); + (new ChannelWaiter(channel)).waitFor(ChannelState.attached); + assertEquals("Verify attached state reached", channel.state, ChannelState.attached); + + /* publish to the channel */ + CompletionSet msgComplete = new CompletionSet(); + for(int i = 0; i < 50; i++) { + try { + channel.publish("history" + i, String.valueOf(i), msgComplete.add()); + } catch(AblyException e) { + e.printStackTrace(); + fail("channelhistory_limit_b: Unexpected exception"); + return; + } + } + + /* wait for the publish callbacks to be called */ + msgComplete.waitFor(); + assertTrue("Verify success callback was called", msgComplete.errors.isEmpty()); + + /* get the history for this channel */ + PaginatedResult messages = channel.history(new Param[] { new Param("direction", "backwards"), new Param("limit", "25") }); + assertNotNull("Expected non-null messages", messages); + assertEquals("Expected 25 messages", messages.items().length, 25); + HashMap messageContents = new HashMap(); + for(Message message : messages.items()) + messageContents.put(message.name, message); + + /* verify message order */ + Message[] expectedMessageHistory = new Message[25]; + for(int i = 0; i < 25; i++) + expectedMessageHistory[i] = messageContents.get("history" + (49 - i)); + Assert.assertArrayEquals("Expect messages in backward order", messages.items(), expectedMessageHistory); + } catch (AblyException e) { + e.printStackTrace(); + fail("channelhistory_limit_b: Unexpected exception"); + return; + } finally { + if(ably != null) + ably.close(); + } + } + + /** + * Publish events and check expected history based on time slice (forwards) + */ + @Test + public void channelhistory_time_f() { + AblyRealtime ably = null; + try { + /* first, publish some messages */ + long intervalStart = 0, intervalEnd = 0; + ClientOptions opts = createOptions(testVars.keys[0].keyStr); + ably = new AblyRealtime(opts); + String channelName = "persisted:channelhistory_time_f_" + testParams.name; + + /* create a channel */ + final Channel channel = ably.channels.get(channelName); + + /* attach */ + channel.attach(); + (new ChannelWaiter(channel)).waitFor(ChannelState.attached); + assertEquals("Verify attached state reached", channel.state, ChannelState.attached); + + /* send batches of messages with shprt inter-message delay */ + CompletionSet msgComplete = new CompletionSet(); + for(int i = 0; i < 20; i++) { + channel.publish("history" + i, String.valueOf(i), msgComplete.add()); + Thread.sleep(100L); + } + Thread.sleep(1000L); + intervalStart = timeOffset + System.currentTimeMillis(); + for(int i = 20; i < 40; i++) { + channel.publish("history" + i, String.valueOf(i), msgComplete.add()); + Thread.sleep(100L); + } + intervalEnd = timeOffset + System.currentTimeMillis() - 1; + Thread.sleep(1000L); + for(int i = 40; i < 60; i++) { + channel.publish("history" + i, String.valueOf(i), msgComplete.add()); + Thread.sleep(100L); + } + + /* wait for message callbacks */ + msgComplete.waitFor(); + + /* get the history for this channel */ + PaginatedResult messages = channel.history(new Param[] { + new Param("direction", "forwards"), + new Param("start", String.valueOf(intervalStart - 500)), + new Param("end", String.valueOf(intervalEnd + 500)) + }); + assertNotNull("Expected non-null messages", messages); + assertEquals("Expected 20 messages", messages.items().length, 20); + HashMap messageContents = new HashMap(); + for(Message message : messages.items()) + messageContents.put(message.name, message); + /* verify message order */ + Message[] expectedMessageHistory = new Message[20]; + for(int i = 20; i < 40; i++) + expectedMessageHistory[i - 20] = messageContents.get("history" + i); + Assert.assertArrayEquals("Expect messages in forward order", messages.items(), expectedMessageHistory); + } catch (AblyException e) { + e.printStackTrace(); + fail("channelhistory_time_f: Unexpected exception"); + } catch (InterruptedException e) { + e.printStackTrace(); + fail("channelhistory_time_f: Unexpected exception"); + } finally { + if(ably != null) + ably.close(); + } + } + + /** + * Publish events and check expected history based on time slice (backwards) + */ + @Test + public void channelhistory_time_b() { + AblyRealtime ably = null; + try { + /* first, publish some messages */ + long intervalStart = 0, intervalEnd = 0; + ClientOptions opts = createOptions(testVars.keys[0].keyStr); + ably = new AblyRealtime(opts); + String channelName = "persisted:channelhistory_time_b_" + testParams.name; + + /* create a channel */ + final Channel channel = ably.channels.get(channelName); + + /* attach */ + channel.attach(); + (new ChannelWaiter(channel)).waitFor(ChannelState.attached); + assertEquals("Verify attached state reached", channel.state, ChannelState.attached); + + /* send batches of messages with shprt inter-message delay */ + CompletionSet msgComplete = new CompletionSet(); + for(int i = 0; i < 20; i++) { + channel.publish("history" + i, String.valueOf(i), msgComplete.add()); + Thread.sleep(100L); + } + Thread.sleep(1000L); + intervalStart = timeOffset + System.currentTimeMillis(); + for(int i = 20; i < 40; i++) { + channel.publish("history" + i, String.valueOf(i), msgComplete.add()); + Thread.sleep(100L); + } + intervalEnd = timeOffset + System.currentTimeMillis() - 1; + Thread.sleep(1000L); + for(int i = 40; i < 60; i++) { + channel.publish("history" + i, String.valueOf(i), msgComplete.add()); + Thread.sleep(100L); + } + + /* wait for message callbacks */ + msgComplete.waitFor(); + + /* get the history for this channel */ + PaginatedResult messages = channel.history(new Param[] { + new Param("direction", "backwards"), + new Param("start", String.valueOf(intervalStart - 500)), + new Param("end", String.valueOf(intervalEnd + 500)) + }); + assertNotNull("Expected non-null messages", messages); + assertEquals("Expected 20 messages", messages.items().length, 20); + HashMap messageContents = new HashMap(); + for(Message message : messages.items()) + messageContents.put(message.name, message); + /* verify message order */ + Message[] expectedMessageHistory = new Message[20]; + for(int i = 20; i < 40; i++) + expectedMessageHistory[i - 20] = messageContents.get("history" + (59 - i)); + Assert.assertArrayEquals("Expect messages in backwards order", messages.items(), expectedMessageHistory); + } catch (AblyException e) { + e.printStackTrace(); + fail("channelhistory_time_b: Unexpected exception"); + } catch (InterruptedException e) { + e.printStackTrace(); + fail("channelhistory_time_b: Unexpected exception"); + } finally { + if(ably != null) + ably.close(); + } + } + + /** + * Check query pagination (forwards) + */ + @Test + public void channelhistory_paginate_f() { + AblyRealtime ably = null; + try { + ClientOptions opts = createOptions(testVars.keys[0].keyStr); + ably = new AblyRealtime(opts); + String channelName = "persisted:channelhistory_paginate_f_" + testParams.name; + + /* create a channel */ + final Channel channel = ably.channels.get(channelName); + + /* attach */ + channel.attach(); + (new ChannelWaiter(channel)).waitFor(ChannelState.attached); + assertEquals("Verify attached state reached", channel.state, ChannelState.attached); + + /* publish to the channel */ + CompletionSet msgComplete = new CompletionSet(); + for(int i = 0; i < 50; i++) { + try { + channel.publish("history" + i, String.valueOf(i), msgComplete.add()); + } catch(AblyException e) { + e.printStackTrace(); + fail("channelhistory_paginate_f: Unexpected exception"); + return; + } + } + + /* wait for the publish callbacks to be called */ + msgComplete.waitFor(); + assertTrue("Verify success callback was called", msgComplete.errors.isEmpty()); + + /* get the history for this channel */ + PaginatedResult messages = channel.history(new Param[] { new Param("direction", "forwards"), new Param("limit", "10") }); + assertNotNull("Expected non-null messages", messages); + assertEquals("Expected 10 messages", messages.items().length, 10); + + /* log all messages */ + HashMap messageContents = new HashMap(); + for(Message message : messages.items()) + messageContents.put(message.name, message); + + /* verify message order */ + Message[] expectedMessageHistory = new Message[10]; + for(int i = 0; i < 10; i++) + expectedMessageHistory[i] = messageContents.get("history" + i); + Assert.assertArrayEquals("Expect messages in forward order", messages.items(), expectedMessageHistory); + + /* get next page */ + messages = messages.next(); + assertNotNull("Expected non-null messages", messages); + assertEquals("Expected 10 messages", messages.items().length, 10); + + /* log all messages */ + for(Message message : messages.items()) + messageContents.put(message.name, message); + + /* verify message order */ + for(int i = 0; i < 10; i++) + expectedMessageHistory[i] = messageContents.get("history" + String.valueOf(i + 10)); + Assert.assertArrayEquals("Expect messages in forward order", messages.items(), expectedMessageHistory); + + /* get next page */ + messages = messages.next(); + assertNotNull("Expected non-null messages", messages); + assertEquals("Expected 10 messages", messages.items().length, 10); + + /* log all messages */ + for(Message message : messages.items()) + messageContents.put(message.name, message); + + /* verify message order */ + for(int i = 0; i < 10; i++) + expectedMessageHistory[i] = messageContents.get("history" + String.valueOf(i + 20)); + Assert.assertArrayEquals("Expect messages in forward order", messages.items(), expectedMessageHistory); + + } catch (AblyException e) { + e.printStackTrace(); + fail("channelhistory_paginate_f: Unexpected exception"); + } finally { + if(ably != null) + ably.close(); + } + } + + /** + * Check query pagination (backwards) + */ + @Test + public void channelhistory_paginate_b() { + AblyRealtime ably = null; + try { + ClientOptions opts = createOptions(testVars.keys[0].keyStr); + ably = new AblyRealtime(opts); + String channelName = "persisted:channelhistory_paginate_b_" + testParams.name; + + /* create a channel */ + final Channel channel = ably.channels.get(channelName); + + /* attach */ + channel.attach(); + (new ChannelWaiter(channel)).waitFor(ChannelState.attached); + assertEquals("Verify attached state reached", channel.state, ChannelState.attached); + + /* publish to the channel */ + CompletionSet msgComplete = new CompletionSet(); + for(int i = 0; i < 50; i++) { + try { + channel.publish("history" + i, String.valueOf(i), msgComplete.add()); + } catch(AblyException e) { + e.printStackTrace(); + fail("channelhistory_paginate_b: Unexpected exception"); + return; + } + } + + /* wait for the publish callbacks to be called */ + msgComplete.waitFor(); + assertTrue("Verify success callback was called", msgComplete.errors.isEmpty()); + + /* get the history for this channel */ + PaginatedResult messages = channel.history(new Param[] { new Param("direction", "backwards"), new Param("limit", "10") }); + assertNotNull("Expected non-null messages", messages); + assertEquals("Expected 10 messages", messages.items().length, 10); + + /* log all messages */ + HashMap messageContents = new HashMap(); + for(Message message : messages.items()) + messageContents.put(message.name, message); + + /* verify message order */ + Message[] expectedMessageHistory = new Message[10]; + for(int i = 0; i < 10; i++) + expectedMessageHistory[i] = messageContents.get("history" + String.valueOf(49 - i)); + Assert.assertArrayEquals("Expect messages in forward order", messages.items(), expectedMessageHistory); + + /* get next page */ + messages = messages.next(); + assertNotNull("Expected non-null messages", messages); + assertEquals("Expected 10 messages", messages.items().length, 10); + + /* log all messages */ + for(Message message : messages.items()) + messageContents.put(message.name, message); + + /* verify message order */ + for(int i = 0; i < 10; i++) + expectedMessageHistory[i] = messageContents.get("history" + String.valueOf(39 - i)); + Assert.assertArrayEquals("Expect messages in forward order", messages.items(), expectedMessageHistory); + + /* get next page */ + messages = messages.next(); + assertNotNull("Expected non-null messages", messages); + assertEquals("Expected 10 messages", messages.items().length, 10); + + /* log all messages */ + for(Message message : messages.items()) + messageContents.put(message.name, message); + + /* verify message order */ + for(int i = 0; i < 10; i++) + expectedMessageHistory[i] = messageContents.get("history" + String.valueOf(29 - i)); + Assert.assertArrayEquals("Expect messages in forward order", messages.items(), expectedMessageHistory); + + } catch (AblyException e) { + e.printStackTrace(); + fail("channelhistory_paginate_b: Unexpected exception"); + } finally { + if(ably != null) + ably.close(); + } + } + + /** + * Check query pagination "rel=first" (forwards) + */ + @Test + public void channelhistory_paginate_first_f() { + AblyRealtime ably = null; + try { + ClientOptions opts = createOptions(testVars.keys[0].keyStr); + ably = new AblyRealtime(opts); + String channelName = "persisted:channelhistory_paginate_first_f_" + testParams.name; + + /* create a channel */ + final Channel channel = ably.channels.get(channelName); + + /* attach */ + channel.attach(); + (new ChannelWaiter(channel)).waitFor(ChannelState.attached); + assertEquals("Verify attached state reached", channel.state, ChannelState.attached); + + /* publish to the channel */ + CompletionSet msgComplete = new CompletionSet(); + for(int i = 0; i < 50; i++) { + try { + channel.publish("history" + i, String.valueOf(i), msgComplete.add()); + } catch(AblyException e) { + e.printStackTrace(); + fail("channelhistory_paginate_f: Unexpected exception"); + return; + } + } + + /* wait for the publish callbacks to be called */ + msgComplete.waitFor(); + assertTrue("Verify success callback was called", msgComplete.errors.isEmpty()); + + /* get the history for this channel */ + PaginatedResult messages = channel.history(new Param[] { new Param("direction", "forwards"), new Param("limit", "10") }); + assertNotNull("Expected non-null messages", messages); + assertEquals("Expected 10 messages", messages.items().length, 10); + + /* log all messages */ + HashMap messageContents = new HashMap(); + for(Message message : messages.items()) + messageContents.put(message.name, message); + + /* verify message order */ + Message[] expectedMessageHistory = new Message[10]; + for(int i = 0; i < 10; i++) + expectedMessageHistory[i] = messageContents.get("history" + i); + Assert.assertArrayEquals("Expect messages in forward order", messages.items(), expectedMessageHistory); + + /* get next page */ + messages = messages.next(); + assertNotNull("Expected non-null messages", messages); + assertEquals("Expected 10 messages", messages.items().length, 10); + + /* log all messages */ + for(Message message : messages.items()) + messageContents.put(message.name, message); + + /* verify message order */ + for(int i = 0; i < 10; i++) + expectedMessageHistory[i] = messageContents.get("history" + String.valueOf(i + 10)); + Assert.assertArrayEquals("Expect messages in forward order", messages.items(), expectedMessageHistory); + + /* get first page */ + messages = messages.first(); + assertNotNull("Expected non-null messages", messages); + assertEquals("Expected 10 messages", messages.items().length, 10); + + /* log all messages */ + for(Message message : messages.items()) + messageContents.put(message.name, message); + + /* verify message order */ + for(int i = 0; i < 10; i++) + expectedMessageHistory[i] = messageContents.get("history" + String.valueOf(i)); + Assert.assertArrayEquals("Expect messages in forward order", messages.items(), expectedMessageHistory); + + } catch (AblyException e) { + e.printStackTrace(); + fail("channelhistory_paginate_first_f: Unexpected exception"); + } finally { + if(ably != null) + ably.close(); + } + } + + /** + * Check query pagination "rel=first" (backwards) + */ + @Test + public void channelhistory_paginate_first_b() { + AblyRealtime ably = null; + try { + ClientOptions opts = createOptions(testVars.keys[0].keyStr); + ably = new AblyRealtime(opts); + String channelName = "persisted:channelhistory_paginate_first_b_" + testParams.name; + + /* create a channel */ + final Channel channel = ably.channels.get(channelName); + + /* attach */ + channel.attach(); + (new ChannelWaiter(channel)).waitFor(ChannelState.attached); + assertEquals("Verify attached state reached", channel.state, ChannelState.attached); + + /* publish to the channel */ + CompletionSet msgComplete = new CompletionSet(); + for(int i = 0; i < 50; i++) { + try { + channel.publish("history" + i, String.valueOf(i), msgComplete.add()); + } catch(AblyException e) { + e.printStackTrace(); + fail("channelhistory_paginate_f: Unexpected exception"); + return; + } + } + + /* wait for the publish callbacks to be called */ + msgComplete.waitFor(); + assertTrue("Verify success callback was called", msgComplete.errors.isEmpty()); + + /* get the history for this channel */ + PaginatedResult messages = channel.history(new Param[] { new Param("direction", "backwards"), new Param("limit", "10") }); + assertNotNull("Expected non-null messages", messages); + assertEquals("Expected 10 messages", messages.items().length, 10); + + /* log all messages */ + HashMap messageContents = new HashMap(); + for(Message message : messages.items()) + messageContents.put(message.name, message); + + /* verify message order */ + Message[] expectedMessageHistory = new Message[10]; + for(int i = 0; i < 10; i++) + expectedMessageHistory[i] = messageContents.get("history" + String.valueOf(49 - i)); + Assert.assertArrayEquals("Expect messages in forward order", messages.items(), expectedMessageHistory); + + /* get next page */ + messages = messages.next(); + assertNotNull("Expected non-null messages", messages); + assertEquals("Expected 10 messages", messages.items().length, 10); + + /* log all messages */ + for(Message message : messages.items()) + messageContents.put(message.name, message); + + /* verify message order */ + for(int i = 0; i < 10; i++) + expectedMessageHistory[i] = messageContents.get("history" + String.valueOf(39 - i)); + Assert.assertArrayEquals("Expect messages in forward order", messages.items(), expectedMessageHistory); + + /* get first page */ + messages = messages.first(); + assertNotNull("Expected non-null messages", messages); + assertEquals("Expected 10 messages", messages.items().length, 10); + + /* log all messages */ + for(Message message : messages.items()) + messageContents.put(message.name, message); + + /* verify message order */ + for(int i = 0; i < 10; i++) + expectedMessageHistory[i] = messageContents.get("history" + String.valueOf(49 - i)); + Assert.assertArrayEquals("Expect messages in forward order", messages.items(), expectedMessageHistory); + + } catch (AblyException e) { + e.printStackTrace(); + fail("channelhistory_paginate_first_b: Unexpected exception"); + } finally { + if(ably != null) + ably.close(); + } + } + + /** + * Connect twice to the service, each using the default (binary) protocol. + * Publish messages on one connection to a given channel; while in progress, + * attach the second connection to the same channel and verify a message + * history up to the point of attachment can be obtained. + */ + @Test + @Ignore("Fails due to issues in sandbox. See https://github.com/ably/realtime/issues/1834 for details.") + public void channelhistory_from_attach() { + AblyRealtime txAbly = null, rxAbly = null; + try { + ClientOptions txOpts = createOptions(testVars.keys[0].keyStr); + txAbly = new AblyRealtime(txOpts); + ClientOptions rxOpts = createOptions(testVars.keys[0].keyStr); + rxAbly = new AblyRealtime(rxOpts); + String channelName = "persisted:channelhistory_from_attach_" + testParams.name; + + /* create a channel */ + final Channel txChannel = txAbly.channels.get(channelName); + final Channel rxChannel = rxAbly.channels.get(channelName); + + /* attach sender */ + txChannel.attach(); + (new ChannelWaiter(txChannel)).waitFor(ChannelState.attached); + assertEquals("Verify attached state reached", txChannel.state, ChannelState.attached); + + /* publish messages to the channel */ + final CompletionSet msgComplete = new CompletionSet(); + Thread publisherThread = new Thread() { + @Override + public void run() { + for(int i = 0; i < 50; i++) { + try { + txChannel.publish("history" + i, String.valueOf(i), msgComplete.add()); + try { + sleep(100L); + } catch(InterruptedException ie) {} + } catch(AblyException e) { + e.printStackTrace(); + fail("channelhistory_from_attach: Unexpected exception"); + return; + } + } + } + }; + publisherThread.start(); + + /* wait 2 seconds */ + try { + Thread.sleep(2000L); + } catch(InterruptedException ie) {} + + /* subscribe; this will trigger the attach */ + MessageWaiter messageWaiter = new MessageWaiter(rxChannel); + + /* get the channel history from the attachSerial when we get the attach indication */ + (new ChannelWaiter(rxChannel)).waitFor(ChannelState.attached); + assertEquals("Verify attached state reached", rxChannel.state, ChannelState.attached); + assertNotNull("Verify attachSerial provided", rxChannel.properties.attachSerial); + + /* wait for the subscription callback to be called on the first received message */ + messageWaiter.waitFor(1); + + /* wait for the publisher thread to complete */ + try { + publisherThread.join(); + } catch (InterruptedException e) { + fail("channelhistory_from_attach: exception in publisher thread"); + } + + /* get the history for this channel */ + PaginatedResult messages = rxChannel.history(new Param[] { new Param("from_serial", rxChannel.properties.attachSerial)}); + assertNotNull("Expected non-null messages", messages); + assertTrue("Expected at least one message", messages.items().length >= 1); + + /* verify that the history and received messages meet */ + int earliestReceivedOnConnection = Integer.valueOf((String)messageWaiter.receivedMessages.get(0).data).intValue(); + int latestReceivedInHistory = Integer.valueOf((String)messages.items()[0].data).intValue(); + assertEquals("Verify that the history and received messages meet", earliestReceivedOnConnection, latestReceivedInHistory + 1); + + } catch (AblyException e) { + e.printStackTrace(); + fail("channelhistory_from_attach: Unexpected exception instantiating library"); + } finally { + if(txAbly != null) + txAbly.close(); + if(rxAbly != null) + rxAbly.close(); + } + } + + /** + * Connect twice to the service. + * Publish messages on one connection to a given channel; while in progress, + * attach the second connection to the same channel and verify a message + * history up to the point of attachment can be obtained. + */ + @Test + public void channelhistory_until_attach() { + AblyRealtime txAbly = null, rxAbly = null; + try { + ClientOptions txOpts = createOptions(testVars.keys[0].keyStr); + txAbly = new AblyRealtime(txOpts); + ClientOptions rxOpts = createOptions(testVars.keys[0].keyStr); + rxAbly = new AblyRealtime(rxOpts); + String channelName = "persisted:channelhistory_until_attach_" + testParams.name; + + /* create a channel */ + final Channel txChannel = txAbly.channels.get(channelName); + final Channel rxChannel = rxAbly.channels.get(channelName); + + /* attach sender */ + txChannel.attach(); + (new ChannelWaiter(txChannel)).waitFor(ChannelState.attached); + assertEquals("Verify attached state reached", txChannel.state, ChannelState.attached); + + /* publish messages to the channel */ + CompletionSet msgComplete = new CompletionSet(); + int messageCount = 25; + for (int i = 0; i < messageCount; i++) { + txChannel.publish("history" + i, String.valueOf(i), msgComplete.add()); + } + + msgComplete.waitFor(); + + /* get the channel history from the attachSerial when we get the attach indication */ + rxChannel.attach(); + new ChannelWaiter(rxChannel).waitFor(ChannelState.attached); + assertEquals("Verify attached state reached", rxChannel.state, ChannelState.attached); + assertNotNull("Verify attachSerial provided", rxChannel.properties.attachSerial); + + /* get the history for this channel */ + PaginatedResult messages = rxChannel.history(new Param[] { new Param("untilAttach", "true") }); + assertNotNull("Expected non-null messages", messages); + assertTrue("Expected at least one message", messages.items().length >= 1); + + /* verify that the history and received messages meet */ + for (int i = 0; i < messageCount; i++) { + /* 0 --> "24" + * 1 --> "23" + * ... + * 24 --> "0" + */ + String actual = (String) messages.items()[messageCount - 1 - i].data; + String expected = String.valueOf(i); + assertThat(actual, is(equalTo(expected))); + } + } catch (AblyException e) { + e.printStackTrace(); + fail("channelhistory_from_attach: Unexpected exception instantiating library"); + } finally { + if(txAbly != null) + txAbly.close(); + if(rxAbly != null) + rxAbly.close(); + } + } + + /** + * Verifies an Exception is thrown, when a channel history is requested + * with parameter {"untilAttach":"true}" before client is attached to the channel + * + * @throws AblyException + */ + @Test(expected=AblyException.class) + public void channelhistory_until_attach_before_attached() throws AblyException { + ClientOptions options = createOptions(testVars.keys[0].keyStr); + AblyRealtime ably = new AblyRealtime(options); + + ably.channels.get("test").history(new Param[]{ new Param("untilAttach", "true") }); + } + + /** + * Verifies an Exception is thrown, when a channel history is requested + * with invalid "untilAttach" parameter value. + * + * @throws AblyException + */ + @Test(expected=AblyException.class) + public void channelhistory_until_attach_invalid_value() throws AblyException { + ClientOptions options = createOptions(testVars.keys[0].keyStr); + AblyRealtime ably = new AblyRealtime(options); + + ably.channels.get("test").history(new Param[]{ new Param("untilAttach", "affirmative")}); + } + + /** + * Publish enough message to fill 2 pages. + * Verify that, + * - {@code PaginatedQuery#isLast} returns false, when we are at the first page. + * - {@code PaginatedQuery#isLast} returns true, when we are at the second page. + */ + @Test + public void channelhistory_islast() throws AblyException { + AblyRealtime ably = null; + try { + ClientOptions opts = createOptions(testVars.keys[0].keyStr); + ably = new AblyRealtime(opts); + String channelName = "persisted:channelhistory_islast_" + testParams.name; + int pageMessageCount = 10; + + /* create a channel */ + final Channel channel = ably.channels.get(channelName); + + /* attach */ + channel.attach(); + new ChannelWaiter(channel).waitFor(ChannelState.attached); + assertEquals("Verify attached state reached", channel.state, ChannelState.attached); + + /* publish to the channel */ + CompletionSet msgComplete = new CompletionSet(); + for (int i = 0; i < (pageMessageCount * 2 - 1); i++) { + channel.publish("history" + i, String.valueOf(i), msgComplete.add()); + } + + /* wait for the publish callbacks to be called */ + msgComplete.waitFor(); + assertTrue("Verify success callback was called", msgComplete.errors.isEmpty()); + + /* get the history for this channel */ + PaginatedResult messages = channel.history(new Param[]{new Param("limit", String.format(Locale.ENGLISH, "%d", pageMessageCount))}); + assertNotNull("Expected non-null messages", messages); + assertEquals("Expected 10 messages", messages.items().length, pageMessageCount); + + /* Verify that current page is the last */ + assertThat(messages.isLast(), is(false)); + + /* get next page */ + messages = messages.next(); + assertNotNull("Expected non-null messages", messages); + assertEquals("Expected 9 messages", messages.items().length, pageMessageCount - 1); + + /* Verify that current page is the last */ + assertThat(messages.isLast(), is(true)); + } finally { + if (ably != null) + ably.close(); + } + } } diff --git a/lib/src/test/java/io/ably/lib/test/realtime/RealtimeChannelTest.java b/lib/src/test/java/io/ably/lib/test/realtime/RealtimeChannelTest.java index 579b80ac7..da88b0211 100644 --- a/lib/src/test/java/io/ably/lib/test/realtime/RealtimeChannelTest.java +++ b/lib/src/test/java/io/ably/lib/test/realtime/RealtimeChannelTest.java @@ -30,1885 +30,1885 @@ public class RealtimeChannelTest extends ParameterizedTest { - private Comparator messageComparator = new Comparator() { - @Override - public int compare(Message o1, Message o2) { - int result = o1.name.compareTo(o2.name); - return (result == 0)?(((String) o1.data).compareTo((String) o2.data)):(result); - } - }; - - - /** - * Connect to the service and attach to a channel, - * confirming that the attached state is reached. - */ - @Test - public void attach() { - String channelName = "attach_" + testParams.name; - AblyRealtime ably = null; - try { - ClientOptions opts = createOptions(testVars.keys[0].keyStr); - ably = new AblyRealtime(opts); - - /* wait until connected */ - (new ConnectionWaiter(ably.connection)).waitFor(ConnectionState.connected); - assertEquals("Verify connected state reached", ably.connection.state, ConnectionState.connected); - - /* create a channel and attach */ - final Channel channel = ably.channels.get(channelName); - channel.attach(); - (new ChannelWaiter(channel)).waitFor(ChannelState.attached); - assertEquals("Verify attached state reached", channel.state, ChannelState.attached); - - } catch (AblyException e) { - e.printStackTrace(); - fail("init0: Unexpected exception instantiating library"); - } finally { - if(ably != null) - ably.close(); - } - } - - /** - * Connect to the service using the default (binary) protocol - * and attach before the connected state is reached. - */ - @Test - public void attach_before_connect() { - String channelName = "attach_before_connect_" + testParams.name; - AblyRealtime ably = null; - try { - ClientOptions opts = createOptions(testVars.keys[0].keyStr); - ably = new AblyRealtime(opts); - - /* create a channel and attach */ - final Channel channel = ably.channels.get(channelName); - channel.attach(); - (new ChannelWaiter(channel)).waitFor(ChannelState.attached); - assertEquals("Verify attached state reached", channel.state, ChannelState.attached); - - } catch (AblyException e) { - e.printStackTrace(); - fail("init0: Unexpected exception instantiating library"); - } finally { - if(ably != null) - ably.close(); - } - } - - /** - * Connect to the service using the default (binary) protocol - * and attach, then detach - */ - @Test - public void attach_detach() { - String channelName = "attach_detach_" + testParams.name; - AblyRealtime ably = null; - try { - ClientOptions opts = createOptions(testVars.keys[0].keyStr); - ably = new AblyRealtime(opts); - - /* create a channel and attach */ - final Channel channel = ably.channels.get(channelName); - channel.attach(); - (new ChannelWaiter(channel)).waitFor(ChannelState.attached); - assertEquals("Verify attached state reached", channel.state, ChannelState.attached); - - /* detach */ - channel.detach(); - (new ChannelWaiter(channel)).waitFor(ChannelState.detached); - assertEquals("Verify detached state reached", channel.state, ChannelState.detached); - - } catch (AblyException e) { - e.printStackTrace(); - fail("init0: Unexpected exception instantiating library"); - } finally { - if(ably != null) - ably.close(); - } - } - - /*@Test*/ - public void attach_with_channel_params_channels_get() { - String channelName = "attach_with_channel_params_channels_get_" + testParams.name; - AblyRealtime ably = null; - try { - ClientOptions opts = createOptions(testVars.keys[0].keyStr); - ably = new AblyRealtime(opts); - - /* wait until connected */ - (new ConnectionWaiter(ably.connection)).waitFor(ConnectionState.connected); - assertEquals("Verify connected state reached", ConnectionState.connected, ably.connection.state); - - ChannelOptions options = new ChannelOptions(); - options.params = new HashMap(); - options.params.put("modes", "subscribe"); - options.params.put("delta", "vcdiff"); - - /* create a channel and attach */ - final Channel channel = ably.channels.get(channelName, options); - channel.attach(); - (new ChannelWaiter(channel)).waitFor(ChannelState.attached); - assertEquals("Verify attached state reached", ChannelState.attached, channel.state); - assertEquals("Verify channel params", channel.getParams(), options.params); - assertArrayEquals("Verify channel modes", new ChannelMode[] { ChannelMode.subscribe }, channel.getModes()); - } catch (AblyException e) { - e.printStackTrace(); - fail("init0: Unexpected exception instantiating library"); - } finally { - if(ably != null) - ably.close(); - } - } - - /*@Test*/ - public void attach_with_channel_params_set_options() { - String channelName = "attach_with_channel_params_set_options_" + testParams.name; - AblyRealtime ably = null; - try { - ClientOptions opts = createOptions(testVars.keys[0].keyStr); - ably = new AblyRealtime(opts); - - /* wait until connected */ - (new ConnectionWaiter(ably.connection)).waitFor(ConnectionState.connected); - assertEquals("Verify connected state reached", ConnectionState.connected, ably.connection.state); - - ChannelOptions options = new ChannelOptions(); - options.params.put("modes", "subscribe"); - options.params.put("delta", "vcdiff"); - - /* create a channel and attach */ - final Channel channel = ably.channels.get(channelName); - channel.setOptions(options); - channel.attach(); - (new ChannelWaiter(channel)).waitFor(ChannelState.attached); - assertEquals("Verify attached state reached", ChannelState.attached, channel.state); - assertEquals("Verify channel params", channel.getParams(), options.params); - assertArrayEquals("Verify channel modes", new ChannelMode[] { ChannelMode.subscribe }, channel.getModes()); - } catch (AblyException e) { - e.printStackTrace(); - fail("init0: Unexpected exception instantiating library"); - } finally { - if(ably != null) - ably.close(); - } - } - - /*@Test*/ - public void channels_get_should_throw_when_would_cause_reattach() { - String channelName = "channels_get_should_throw_when_would_cause_reattach_" + testParams.name; - AblyRealtime ably = null; - try { - ClientOptions opts = createOptions(testVars.keys[0].keyStr); - ably = new AblyRealtime(opts); - - /* wait until connected */ - (new ConnectionWaiter(ably.connection)).waitFor(ConnectionState.connected); - assertEquals("Verify connected state reached", ConnectionState.connected, ably.connection.state); - - ChannelOptions options = new ChannelOptions(); - options.params.put("modes", "subscribe"); - options.params.put("delta", "vcdiff"); - - /* create a channel and attach */ - final Channel channel = ably.channels.get(channelName, options); - channel.attach(); - (new ChannelWaiter(channel)).waitFor(ChannelState.attached); - - try { - ably.channels.get(channelName, options); - } catch (AblyException e) { - assertEquals("Verify error code", 400, e.errorInfo.code); - assertEquals("Verify error status code", 40000, e.errorInfo.statusCode); - assertTrue("Verify error message", e.errorInfo.message.contains("setOptions")); - } - } catch (AblyException e) { - e.printStackTrace(); - fail("init0: Unexpected exception instantiating library"); - } finally { - if(ably != null) - ably.close(); - } - } - - /*@Test*/ - public void attach_with_channel_params_modes_and_channel_modes() { - String channelName = "attach_with_channel_params_modes_and_channel_modes_" + testParams.name; - AblyRealtime ably = null; - try { - ClientOptions opts = createOptions(testVars.keys[0].keyStr); - ably = new AblyRealtime(opts); - - /* wait until connected */ - (new ConnectionWaiter(ably.connection)).waitFor(ConnectionState.connected); - assertEquals("Verify connected state reached", ConnectionState.connected, ably.connection.state); - - ChannelOptions options = new ChannelOptions(); - options.params = new HashMap(); - options.params.put("modes", "presence,subscribe"); - options.modes = new ChannelMode[] { - ChannelMode.publish, - ChannelMode.presence_subscribe - }; - - /* create a channel and attach */ - final Channel channel = ably.channels.get(channelName, options); - channel.attach(); - (new ChannelWaiter(channel)).waitFor(ChannelState.attached); - assertEquals("Verify attached state reached", ChannelState.attached, channel.state); - assertEquals("Verify channel params", channel.getParams(), options.params); - assertArrayEquals("Verify channel modes", new ChannelMode[] { ChannelMode.subscribe, ChannelMode.presence }, channel.getModes()); - } catch (AblyException e) { - e.printStackTrace(); - fail("init0: Unexpected exception instantiating library"); - } finally { - if(ably != null) - ably.close(); - } - } - - /*@Test*/ - public void attach_with_channel_modes() { - String channelName = "attach_with_channel_modes_" + testParams.name; - AblyRealtime ably = null; - try { - ClientOptions opts = createOptions(testVars.keys[0].keyStr); - ably = new AblyRealtime(opts); - - /* wait until connected */ - (new ConnectionWaiter(ably.connection)).waitFor(ConnectionState.connected); - assertEquals("Verify connected state reached", ConnectionState.connected, ably.connection.state); - - ChannelOptions options = new ChannelOptions(); - options.modes = new ChannelMode[] { - ChannelMode.publish, - ChannelMode.presence_subscribe, - }; - - /* create a channel and attach */ - final Channel channel = ably.channels.get(channelName, options); - channel.attach(); - (new ChannelWaiter(channel)).waitFor(ChannelState.attached); - assertEquals("Verify attached state reached", ChannelState.attached, channel.state); - assertEquals("Verify channel modes", channel.getModes(), options.modes); - } catch (AblyException e) { - e.printStackTrace(); - fail("init0: Unexpected exception instantiating library"); - } finally { - if(ably != null) - ably.close(); - } - } - - /*@Test*/ - public void attach_with_params_delta_and_channel_modes() { - String channelName = "attach_with_params_delta_and_channel_modes_" + testParams.name; - AblyRealtime ably = null; - try { - ClientOptions opts = createOptions(testVars.keys[0].keyStr); - ably = new AblyRealtime(opts); - - /* wait until connected */ - (new ConnectionWaiter(ably.connection)).waitFor(ConnectionState.connected); - assertEquals("Verify connected state reached", ConnectionState.connected, ably.connection.state); - - ChannelOptions options = new ChannelOptions(); - options.params = new HashMap(); - options.params.put("delta", "vcdiff"); - options.modes = new ChannelMode[] { - ChannelMode.publish, - ChannelMode.subscribe, - ChannelMode.presence_subscribe, - }; - - /* create a channel and attach */ - final Channel channel = ably.channels.get(channelName, options); - channel.attach(); - (new ChannelWaiter(channel)).waitFor(ChannelState.attached); - assertEquals("Verify attached state reached", ChannelState.attached, channel.state); - options.params.put("modes", "publish,subscribe,presence_subscribe"); - assertEquals("Verify channel params", channel.getParams(), options.params); - assertEquals("Verify channel modes", channel.getModes(), options.modes); - } catch (AblyException e) { - e.printStackTrace(); - fail("init0: Unexpected exception instantiating library"); - } finally { - if(ably != null) - ably.close(); - } - } - - /** - * Connect to the service and attach, then subscribe and unsubscribe - */ - @Test - public void subscribe_unsubscribe() { - String channelName = "subscribe_unsubscribe_" + testParams.name; - AblyRealtime ably = null; - try { - ClientOptions opts = createOptions(testVars.keys[0].keyStr); - ably = new AblyRealtime(opts); - - /* create a channel and attach */ - final Channel channel = ably.channels.get(channelName); - channel.attach(); - (new ChannelWaiter(channel)).waitFor(ChannelState.attached); - assertEquals("Verify attached state reached", channel.state, ChannelState.attached); - - /* subscribe */ - MessageListener testListener = new MessageListener() { - @Override - public void onMessage(Message message) { - }}; - channel.subscribe("test_event", testListener); - /* unsubscribe */ - channel.unsubscribe("test_event", testListener); - } catch (AblyException e) { - e.printStackTrace(); - fail("init0: Unexpected exception instantiating library"); - } finally { - if(ably != null) - ably.close(); - } - } - - /** - *

- * Verifies that unsubscribe call with no argument removes all listeners, - * and any of the previously subscribed listeners doesn't receive any message - * after that. - *

- *

- * Spec: RTL8a - *

- */ - @Test - public void unsubscribe_all() throws AblyException { - /* Ably instance that will emit messages */ - AblyRealtime ably1 = null; - /* Ably instance that will receive messages */ - AblyRealtime ably2 = null; - - String channelName = "test.channel.unsubscribe.all" + System.currentTimeMillis(); - Message[] messages = new Message[] { - new Message("name1", "Lorem ipsum dolor sit amet"), - new Message("name2", "Consectetur adipiscing elit."), - new Message("name3", "Pellentesque nulla lorem"), - new Message("name4", "Efficitur ac consequat a, commodo ut orci."), - }; - - try { - ClientOptions option1 = createOptions(testVars.keys[0].keyStr); - option1.clientId = "emitter client"; - ClientOptions option2 = createOptions(testVars.keys[0].keyStr); - option2.clientId = "receiver client"; - - ably1 = new AblyRealtime(option1); - ably2 = new AblyRealtime(option2); - - Channel channel1 = ably1.channels.get(channelName); - channel1.attach(); - new ChannelWaiter(channel1).waitFor(ChannelState.attached); - - Channel channel2 = ably2.channels.get(channelName); - channel2.attach(); - new ChannelWaiter(channel2).waitFor(ChannelState.attached); - - /* Create a listener that collect received messages */ - ArrayList receivedMessageStack = new ArrayList<>(); - MessageListener listener = new MessageListener() { - List messageStack; - - @Override - public void onMessage(Message message) { - messageStack.add(message); - } - - public MessageListener setMessageStack(List messageStack) { - this.messageStack = messageStack; - return this; - } - }.setMessageStack(receivedMessageStack); - - /* Subscribe using various alternatives of {@code Channel#subscribe()} */ - channel2.subscribe(listener); - channel2.subscribe(messages[0].name, listener); - channel2.subscribe(new String[] {messages[1].name, messages[2].name}, listener); - - /* Unsubscribe */ - channel2.unsubscribe(); - - /* Start emitting channel with ably client 1 (emitter) */ - Helpers.CompletionWaiter waiter = new Helpers.CompletionWaiter(); - channel1.publish(messages, waiter); - waiter.waitFor(); - - /* Validate that we didn't received anything - */ - assertThat(receivedMessageStack, Matchers.is(Matchers.emptyCollectionOf(Message.class))); - } finally { - if (ably1 != null) ably1.close(); - if (ably2 != null) ably2.close(); - } - } - - /** - *

- * Validates channel removes a subscriber, - * when {@code Channel#unsubscribe()} gets called with a listener argument. - *

- * - * @throws AblyException - */ - @Test - public void unsubscribe_single() throws AblyException { - /* Ably instance that will emit messages */ - AblyRealtime ably1 = null; - /* Ably instance that will receive messages */ - AblyRealtime ably2 = null; - - String channelName = "test.channel.unsubscribe.single" + System.currentTimeMillis(); - Message[] messages = new Message[] { - new Message("name1", "Lorem ipsum dolor sit amet"), - new Message("name2", "Consectetur adipiscing elit."), - new Message("name3", "Pellentesque nulla lorem"), - new Message("name4", "Efficitur ac consequat a, commodo ut orci."), - }; - - try { - ClientOptions option1 = createOptions(testVars.keys[0].keyStr); - option1.clientId = "emitter client"; - ClientOptions option2 = createOptions(testVars.keys[0].keyStr); - option2.clientId = "receiver client"; - - ably1 = new AblyRealtime(option1); - ably2 = new AblyRealtime(option2); - - Channel channel1 = ably1.channels.get(channelName); - channel1.attach(); - new ChannelWaiter(channel1).waitFor(ChannelState.attached); - - Channel channel2 = ably2.channels.get(channelName); - channel2.attach(); - new ChannelWaiter(channel2).waitFor(ChannelState.attached); - - /* Create a listener that collect received messages */ - ArrayList receivedMessageStack = new ArrayList<>(); - MessageListener listener = new MessageListener() { - List messageStack; - - @Override - public void onMessage(Message message) { - messageStack.add(message); - } - - public MessageListener setMessageStack(List messageStack) { - this.messageStack = messageStack; - return this; - } - }.setMessageStack(receivedMessageStack); - - /* Subscribe using various alternatives of {@code Channel#subscribe()} */ - channel2.subscribe(listener); - channel2.subscribe(messages[0].name, listener); - channel2.subscribe(new String[] {messages[1].name, messages[2].name}, listener); - - /* Unsubscribe */ - channel2.unsubscribe(listener); - - /* Start emitting channel with ably client 1 (emitter) */ - Helpers.CompletionWaiter waiter = new Helpers.CompletionWaiter(); - channel1.publish(messages, waiter); - waiter.waitFor(); - - /* Validate that we didn't received anything - */ - assertThat(receivedMessageStack, Matchers.is(Matchers.emptyCollectionOf(Message.class))); - } finally { - if (ably1 != null) ably1.close(); - if (ably2 != null) ably2.close(); - } - } - - /** - *

- * Validates a client can observe channel messages of other client, - * when they entered to the same channel and observing client subscribed - * to all messages. - *

- * - * @throws AblyException - */ - @Test - public void subscribe_all() throws AblyException { - /* Ably instance that will emit channel messages */ - AblyRealtime ably1 = null; - /* Ably instance that will receive channel messages */ - AblyRealtime ably2 = null; - - String channelName = "test.channel.subscribe.all" + System.currentTimeMillis(); - Message[] messages = new Message[]{ - new Message("name1", "Lorem ipsum dolor sit amet,"), - new Message("name2", "Consectetur adipiscing elit."), - new Message("name3", "Pellentesque nulla lorem.") - }; - - try { - ClientOptions option1 = createOptions(testVars.keys[0].keyStr); - option1.clientId = "emitter client"; - ClientOptions option2 = createOptions(testVars.keys[0].keyStr); - option2.clientId = "receiver client"; - - ably1 = new AblyRealtime(option1); - ably2 = new AblyRealtime(option2); - - Channel channel1 = ably1.channels.get(channelName); - channel1.attach(); - new ChannelWaiter(channel1).waitFor(ChannelState.attached); - - Channel channel2 = ably2.channels.get(channelName); - channel2.attach(); - new ChannelWaiter(channel2).waitFor(ChannelState.attached); - - /* Create a listener that collects received messages */ - ArrayList receivedMessageStack = new ArrayList<>(); - MessageListener listener = new MessageListener() { - List messageStack; - - @Override - public void onMessage(Message message) { - messageStack.add(message); - } - - public MessageListener setMessageStack(List messageStack) { - this.messageStack = messageStack; - return this; - } - }.setMessageStack(receivedMessageStack); - - channel2.subscribe(listener); - - /* Start emitting channel with ably client 1 (emitter) */ - channel1.publish(messages, null); - - /* Wait until receiver client (ably2) observes {@code } - * is emitted from emitter client (ably1) - */ - new Helpers.MessageWaiter(channel2).waitFor(messages.length); - - /* Validate that, - * - we received every message that has been published - */ - assertThat(receivedMessageStack.size(), is(equalTo(messages.length))); - - Collections.sort(receivedMessageStack, messageComparator); - for (int i = 0; i < messages.length; i++) { - Message message = messages[i]; - if(Collections.binarySearch(receivedMessageStack, message, messageComparator) < 0) { - fail("Unable to find expected message: " + message); - } - } - } finally { - if (ably1 != null) ably1.close(); - if (ably2 != null) ably2.close(); - } - } - - /** - *

- * Validates a client can observe channel messages of other client, - * when they entered to the same channel and observing client subscribed - * to multiple messages. - *

- * - * @throws AblyException - */ - @Test - public void subscribe_multiple() throws AblyException { - /* Ably instance that will emit channel messages */ - AblyRealtime ably1 = null; - /* Ably instance that will receive channel messages */ - AblyRealtime ably2 = null; - - String channelName = "test.channel.subscribe.multiple" + System.currentTimeMillis(); - Message[] messages = new Message[] { - new Message("name1", "Lorem ipsum dolor sit amet,"), - new Message("name2", "Consectetur adipiscing elit."), - new Message("name3", "Pellentesque nulla lorem.") - }; - - String[] messageNames = new String[] { - messages[0].name, - messages[1].name, - messages[2].name - }; - - try { - ClientOptions option1 = createOptions(testVars.keys[0].keyStr); - option1.clientId = "emitter client"; - ClientOptions option2 = createOptions(testVars.keys[0].keyStr); - option2.clientId = "receiver client"; - - ably1 = new AblyRealtime(option1); - ably2 = new AblyRealtime(option2); - - Channel channel1 = ably1.channels.get(channelName); - channel1.attach(); - new ChannelWaiter(channel1).waitFor(ChannelState.attached); - - Channel channel2 = ably2.channels.get(channelName); - channel2.attach(); - new ChannelWaiter(channel2).waitFor(ChannelState.attached); - - /* Create a listener that collect received messages */ - ArrayList receivedMessageStack = new ArrayList<>(); - MessageListener listener = new MessageListener() { - List messageStack; - - @Override - public void onMessage(Message message) { - messageStack.add(message); - } - - public MessageListener setMessageStack(List messageStack) { - this.messageStack = messageStack; - return this; - } - }.setMessageStack(receivedMessageStack); - channel2.subscribe(messageNames, listener); - - /* Start emitting channel with ably client 1 (emitter) */ - channel1.publish("nonTrackedMessageName", "This message should be ignore by second client (ably2).", null); - channel1.publish(messages, null); - channel1.publish("nonTrackedMessageName", "This message should be ignore by second client (ably2).", null); - - /* Wait until receiver client (ably2) observes {@code Message} - * on subscribed channel (channel2) emitted by emitter client (ably1) - */ - new Helpers.MessageWaiter(channel2).waitFor(messages.length + 2); - - /* Validate that, - * - we received specific messages - */ - assertThat(receivedMessageStack.size(), is(equalTo(messages.length))); - - Collections.sort(receivedMessageStack, messageComparator); - for (int i = 0; i < messages.length; i++) { - Message message = messages[i]; - if(Collections.binarySearch(receivedMessageStack, message, messageComparator) < 0) { - fail("Unable to find expected message: " + message); - } - } - } finally { - if (ably1 != null) ably1.close(); - if (ably2 != null) ably2.close(); - } - } - - /** - *

- * Validates a client can observe channel messages of other client, - * when they entered to the same channel and observing client subscribed - * to a single message. - *

- * - * @throws AblyException - */ - @Test - public void subscribe_single() throws AblyException { - /* Ably instance that will emit channel messages */ - AblyRealtime ably1 = null; - /* Ably instance that will receive channel messages */ - AblyRealtime ably2 = null; - - String channelName = "test.channel.subscribe.single" + System.currentTimeMillis(); - String messageName = "name"; - Message[] messages = new Message[] { - new Message(messageName, "Lorem ipsum dolor sit amet,"), - new Message(messageName, "Consectetur adipiscing elit."), - new Message(messageName, "Pellentesque nulla lorem.") - }; - - try { - ClientOptions option1 = createOptions(testVars.keys[0].keyStr); - option1.clientId = "emitter client"; - ClientOptions option2 = createOptions(testVars.keys[0].keyStr); - option2.clientId = "receiver client"; - - ably1 = new AblyRealtime(option1); - ably2 = new AblyRealtime(option2); - - Channel channel1 = ably1.channels.get(channelName); - channel1.attach(); - new ChannelWaiter(channel1).waitFor(ChannelState.attached); - - Channel channel2 = ably2.channels.get(channelName); - channel2.attach(); - new ChannelWaiter(channel2).waitFor(ChannelState.attached); - - ArrayList receivedMessageStack = new ArrayList<>(); - MessageListener listener = new MessageListener() { - List messageStack; - - @Override - public void onMessage(Message message) { - messageStack.add(message); - } - - public MessageListener setMessageStack(List messageStack) { - this.messageStack = messageStack; - return this; - } - }.setMessageStack(receivedMessageStack); - channel2.subscribe(messageName, listener); - - /* Start emitting channel with ably client 1 (emitter) */ - channel1.publish("nonTrackedMessageName", "This message should be ignore by second client (ably2).", null); - channel1.publish(messages, null); - channel1.publish("nonTrackedMessageName", "This message should be ignore by second client (ably2).", null); - - /* Wait until receiver client (ably2) observes {@code Message} - * on subscribed channel (channel2) emitted by emitter client (ably1) - */ - new Helpers.MessageWaiter(channel2).waitFor(messages.length + 2); - - /* Validate that, - * - received same amount of emitted specific message - * - received messages are the ones we emitted - */ - assertThat(receivedMessageStack.size(), is(equalTo(messages.length))); - - Collections.sort(receivedMessageStack, messageComparator); - for (int i = 0; i < messages.length; i++) { - Message message = messages[i]; - if(Collections.binarySearch(receivedMessageStack, message, messageComparator) < 0) { - fail("Unable to find expected message: " + message); - } - } - } finally { - if (ably1 != null) ably1.close(); - if (ably2 != null) ably2.close(); - } - } - - - /** - * Connect to the service using the default (binary) protocol - * and attempt to attach to a channel with credentials that do - * not have access, confirming that the failed state is reached. - */ - @Test - public void attach_fail() { - AblyRealtime ably = null; - try { - ClientOptions opts = createOptions(testVars.keys[1].keyStr); - ably = new AblyRealtime(opts); - - /* wait until connected */ - (new ConnectionWaiter(ably.connection)).waitFor(ConnectionState.connected); - assertEquals("Verify connected state reached", ably.connection.state, ConnectionState.connected); - - /* create a channel and attach */ - final Channel channel = ably.channels.get("attach_fail"); - channel.attach(); - ErrorInfo fail = (new ChannelWaiter(channel)).waitFor(ChannelState.failed); - assertEquals("Verify failed state reached", channel.state, ChannelState.failed); - assertEquals("Verify reason code gives correct failure reason", fail.statusCode, 401); - - } catch (AblyException e) { - e.printStackTrace(); - fail("init0: Unexpected exception instantiating library"); - } finally { - if(ably != null) - ably.close(); - } - } - - /** - * When client attaches to a channel successfully, verify - * attach {@code CompletionListener#onSuccess()} gets called. - */ - @Test - public void attach_success_callback() { - AblyRealtime ably = null; - try { - ClientOptions opts = createOptions(testVars.keys[0].keyStr); - ably = new AblyRealtime(opts); - - /* wait until connected */ - (new ConnectionWaiter(ably.connection)).waitFor(ConnectionState.connected); - assertEquals("Verify connected state reached", ably.connection.state, ConnectionState.connected); - - /* create a channel and attach */ - final Channel channel = ably.channels.get("attach_success"); - Helpers.CompletionWaiter waiter = new Helpers.CompletionWaiter(); - channel.attach(waiter); - new ChannelWaiter(channel).waitFor(ChannelState.attached); - assertEquals("Verify failed state reached", channel.state, ChannelState.attached); - - /* Verify onSuccess callback gets called */ - waiter.waitFor(); - assertThat(waiter.success, is(true)); - } catch (AblyException e) { - e.printStackTrace(); - fail("init0: Unexpected exception instantiating library"); - } finally { - if(ably != null) - ably.close(); - } - } - - /** - * When client failed to attach to a channel, verify - * attach {@code CompletionListener#onError(ErrorInfo)} - * gets called. - */ - @Test - public void attach_fail_callback() { - AblyRealtime ably = null; - try { - ClientOptions opts = createOptions(testVars.keys[1].keyStr); - ably = new AblyRealtime(opts); - - /* wait until connected */ - (new ConnectionWaiter(ably.connection)).waitFor(ConnectionState.connected); - assertEquals("Verify connected state reached", ably.connection.state, ConnectionState.connected); - - /* create a channel and attach */ - final Channel channel = ably.channels.get("attach_fail"); - Helpers.CompletionWaiter waiter = new Helpers.CompletionWaiter(); - channel.attach(waiter); - ErrorInfo fail = (new ChannelWaiter(channel)).waitFor(ChannelState.failed); - assertEquals("Verify failed state reached", channel.state, ChannelState.failed); - assertEquals("Verify reason code gives correct failure reason", fail.statusCode, 401); - - /* Verify error callback gets called with correct status code */ - waiter.waitFor(); - assertThat(waiter.error.statusCode, is(equalTo(401))); - } catch (AblyException e) { - e.printStackTrace(); - fail("init0: Unexpected exception instantiating library"); - } finally { - if(ably != null) - ably.close(); - } - } - - /** - * When client detaches from a channel successfully after initialized state, - * verify attach {@code CompletionListener#onSuccess()} gets called. - */ - @Test - public void detach_success_callback_initialized() { - AblyRealtime ably = null; - try { - ClientOptions opts = createOptions(testVars.keys[0].keyStr); - ably = new AblyRealtime(opts); - - /* wait until connected */ - (new ConnectionWaiter(ably.connection)).waitFor(ConnectionState.connected); - assertEquals("Verify connected state reached", ably.connection.state, ConnectionState.connected); - - /* create a channel and attach */ - final Channel channel = ably.channels.get("detach_success"); - assertEquals("Verify failed state reached", channel.state, ChannelState.initialized); - - /* detach */ - Helpers.CompletionWaiter waiter = new Helpers.CompletionWaiter(); - channel.detach(waiter); - - /* Verify onSuccess callback gets called */ - waiter.waitFor(); - assertThat(waiter.success, is(true)); - } catch (AblyException e) { - e.printStackTrace(); - fail("init0: Unexpected exception instantiating library"); - } finally { - if(ably != null) - ably.close(); - } - } - - /** - * When client detaches from a channel successfully after attached state, - * verify attach {@code CompletionListener#onSuccess()} gets called. - */ - @Test - public void detach_success_callback_attached() throws AblyException { - AblyRealtime ably = null; - try { - ClientOptions opts = createOptions(testVars.keys[0].keyStr); - ably = new AblyRealtime(opts); - - /* wait until connected */ - (new ConnectionWaiter(ably.connection)).waitFor(ConnectionState.connected); - assertEquals("Verify connected state reached", ably.connection.state, ConnectionState.connected); - - /* create a channel and attach */ - final Channel channel = ably.channels.get("detach_success"); - channel.attach(); - new ChannelWaiter(channel).waitFor(ChannelState.attached); - assertEquals("Verify attached state reached", channel.state, ChannelState.attached); - - /* detach */ - Helpers.CompletionWaiter waiter = new Helpers.CompletionWaiter(); - channel.detach(waiter); - - /* Verify onSuccess callback gets called */ - waiter.waitFor(); - assertThat(waiter.success, is(true)); - } finally { - if(ably != null) - ably.close(); - } - } - - /** - * When client detaches from a channel successfully after detaching state, - * verify attach {@code CompletionListener#onSuccess()} gets called. - */ - @Test - public void detach_success_callback_detaching() throws AblyException { - AblyRealtime ably = null; - try { - ClientOptions opts = createOptions(testVars.keys[0].keyStr); - ably = new AblyRealtime(opts); - - /* wait until connected */ - (new ConnectionWaiter(ably.connection)).waitFor(ConnectionState.connected); - assertEquals("Verify connected state reached", ably.connection.state, ConnectionState.connected); - - /* create a channel and attach */ - final Channel channel = ably.channels.get("detach_success"); - channel.attach(); - new ChannelWaiter(channel).waitFor(ChannelState.attached); - assertEquals("Verify attached state reached", channel.state, ChannelState.attached); - - /* detach */ - channel.detach(); - assertEquals("Verify detaching state reached", channel.state, ChannelState.detaching); - Helpers.CompletionWaiter waiter = new Helpers.CompletionWaiter(); - channel.detach(waiter); - - /* Verify onSuccess callback gets called */ - waiter.waitFor(); - assertThat(waiter.success, is(true)); - } finally { - if(ably != null) - ably.close(); - } - } - - /** - * When client detaches from a channel successfully after detached state, - * verify attach {@code CompletionListener#onSuccess()} gets called. - */ - @Test - public void detach_success_callback_detached() throws AblyException { - AblyRealtime ably = null; - try { - ClientOptions opts = createOptions(testVars.keys[0].keyStr); - ably = new AblyRealtime(opts); - - /* wait until connected */ - (new ConnectionWaiter(ably.connection)).waitFor(ConnectionState.connected); - assertEquals("Verify connected state reached", ably.connection.state, ConnectionState.connected); - - /* create a channel and attach */ - final Channel channel = ably.channels.get("detach_success"); - channel.attach(); - new ChannelWaiter(channel).waitFor(ChannelState.attached); - assertEquals("Verify attached state reached", channel.state, ChannelState.attached); - - /* detach */ - channel.detach(); - new ChannelWaiter(channel).waitFor(ChannelState.detached); - - Helpers.CompletionWaiter waiter = new Helpers.CompletionWaiter(); - channel.detach(waiter); - - /* Verify onSuccess callback gets called */ - waiter.waitFor(); - assertThat(waiter.success, is(true)); - } finally { - if(ably != null) - ably.close(); - } - } - - /** - *

- * Validate publish will succeed without triggering an attach when connected - * if not already attached - *

- *

- * Spec: RTL6c1 - *

- * - */ - @Test - public void transient_publish_connected() throws AblyException { - AblyRealtime pubAbly = null, subAbly = null; - String channelName = "transient_publish_connected_" + testParams.name; - try { - ClientOptions opts = createOptions(testVars.keys[0].keyStr); - subAbly = new AblyRealtime(opts); - - /* wait until connected */ - new ConnectionWaiter(subAbly.connection).waitFor(ConnectionState.connected); - assertEquals("Verify connected state reached", subAbly.connection.state, ConnectionState.connected); - - /* create a channel and subscribe */ - final Channel subChannel = subAbly.channels.get(channelName); - Helpers.MessageWaiter messageWaiter = new Helpers.MessageWaiter(subChannel); - new ChannelWaiter(subChannel).waitFor(ChannelState.attached); - - pubAbly = new AblyRealtime(opts); - new ConnectionWaiter(pubAbly.connection).waitFor(ConnectionState.connected); - Helpers.CompletionWaiter completionWaiter = new Helpers.CompletionWaiter(); - final Channel pubChannel = pubAbly.channels.get(channelName); - pubChannel.publish("Lorem", "Ipsum!", completionWaiter); - assertEquals("Verify channel remains in initialized state", pubChannel.state, ChannelState.initialized); - - ErrorInfo errorInfo = completionWaiter.waitFor(); - assertEquals("Verify channel remains in initialized state", pubChannel.state, ChannelState.initialized); - - messageWaiter.waitFor(1); - assertEquals("Verify expected message received", messageWaiter.receivedMessages.get(0).name, "Lorem"); - } finally { - if(pubAbly != null) { - pubAbly.close(); - } - if(subAbly != null) { - subAbly.close(); - } - } - } - - /** - *

- * Validate publish will succeed without triggering an attach when connecting - * if not already attached - *

- *

- * Spec: RTL6c2 - *

- * - */ - @Test - public void transient_publish_connecting() throws AblyException { - AblyRealtime pubAbly = null, subAbly = null; - String channelName = "transient_publish_connecting_" + testParams.name; - try { - ClientOptions opts = createOptions(testVars.keys[0].keyStr); - subAbly = new AblyRealtime(opts); - - /* wait until connected */ - new ConnectionWaiter(subAbly.connection).waitFor(ConnectionState.connected); - assertEquals("Verify connected state reached", subAbly.connection.state, ConnectionState.connected); - - /* create a channel and subscribe */ - final Channel subChannel = subAbly.channels.get(channelName); - Helpers.ChannelWaiter channelWaiter = new Helpers.ChannelWaiter(subChannel); - Helpers.MessageWaiter messageWaiter = new Helpers.MessageWaiter(subChannel); - channelWaiter.waitFor(ChannelState.attached); - - pubAbly = new AblyRealtime(opts); - final Channel pubChannel = pubAbly.channels.get(channelName); - Helpers.CompletionWaiter completionWaiter = new Helpers.CompletionWaiter(); - pubChannel.publish("Lorem", "Ipsum!", completionWaiter); - assertEquals("Verify channel remains in initialized state", pubChannel.state, ChannelState.initialized); - - ErrorInfo errorInfo = completionWaiter.waitFor(); - assertEquals("Verify channel remains in initialized state", pubChannel.state, ChannelState.initialized); - - messageWaiter.waitFor(1); - assertEquals("Verify expected message received", messageWaiter.receivedMessages.get(0).name, "Lorem"); - } finally { - if(pubAbly != null) { - pubAbly.close(); - } - if(subAbly != null) { - subAbly.close(); - } - } - } - - /** - *

- * Validate publish will fail when connection is failed - *

- *

- * Spec: RTL6c4 - *

- * - */ - @Test - public void transient_publish_connection_failed() { - AblyRealtime pubAbly = null; - String channelName = "transient_publish_connection_failed_" + testParams.name; - try { - ClientOptions opts = createOptions("not:a.key"); - pubAbly = new AblyRealtime(opts); - new ConnectionWaiter(pubAbly.connection).waitFor(ConnectionState.failed); - assertEquals("Verify failed state reached", pubAbly.connection.state, ConnectionState.failed); - - final Channel pubChannel = pubAbly.channels.get(channelName); - Helpers.CompletionWaiter completionWaiter = new Helpers.CompletionWaiter(); - try { - pubChannel.publish("Lorem", "Ipsum!", completionWaiter); - fail("failed to raise expected exception"); - } catch(AblyException e) { - } - } catch(AblyException e) { - fail("unexpected exception"); - } finally { - if(pubAbly != null) { - pubAbly.close(); - } - } - } - - /** - *

- * Validate publish will fail when channel is failed - *

- *

- * Spec: RTL6c4 - *

- * - */ - @Test - public void transient_publish_channel_failed() { - AblyRealtime pubAbly = null; - String channelName = "transient_publish_channel_failed_" + testParams.name; - try { - ClientOptions opts = createOptions(testVars.keys[1].keyStr); - pubAbly = new AblyRealtime(opts); - new ConnectionWaiter(pubAbly.connection).waitFor(ConnectionState.connected); - assertEquals("Verify connected state reached", pubAbly.connection.state, ConnectionState.connected); - - final Channel pubChannel = pubAbly.channels.get(channelName); - Helpers.ChannelWaiter channelWaiter = new Helpers.ChannelWaiter(pubChannel); - pubChannel.attach(); - channelWaiter.waitFor(ChannelState.failed); - - Helpers.CompletionWaiter completionWaiter = new Helpers.CompletionWaiter(); - try { - pubChannel.publish("Lorem", "Ipsum!", completionWaiter); - fail("failed to raise expected exception"); - } catch(AblyException e) { - assertEquals(pubChannel.state, ChannelState.failed); - } - } catch(AblyException e) { - fail("unexpected exception"); - } finally { - if(pubAbly != null) { - pubAbly.close(); - } - } - } - - /** - *

- * Validate subscribe will result in an error, when the channel moves - * to the FAILED state before the operation succeeds - *

- *

- * Spec: RTL7c - *

- * - * @throws AblyException - */ - @Test - public void attach_implicit_subscribe_fail() throws AblyException { - AblyRealtime ably = null; - try { - ClientOptions opts = createOptions(testVars.keys[1].keyStr); - ably = new AblyRealtime(opts); - - /* wait until connected */ - new ConnectionWaiter(ably.connection).waitFor(ConnectionState.connected); - assertEquals("Verify connected state reached", ably.connection.state, ConnectionState.connected); - - /* create a channel and subscribe */ - final Channel channel = ably.channels.get("subscribe_fail"); - channel.subscribe(null); - assertEquals("Verify attaching state reached", channel.state, ChannelState.attaching); - - ErrorInfo fail = new ChannelWaiter(channel).waitFor(ChannelState.failed); - assertEquals("Verify failed state reached", channel.state, ChannelState.failed); - assertEquals("Verify reason code gives correct failure reason", fail.statusCode, 401); - } finally { - if(ably != null) - ably.close(); - } - } - - @Test - public void ensure_detach_with_error_does_not_move_to_failed() { - AblyRealtime ably = null; - try { - ClientOptions opts = createOptions(testVars.keys[0].keyStr); - ably = new AblyRealtime(opts); - - /* wait until connected */ - (new ConnectionWaiter(ably.connection)).waitFor(ConnectionState.connected); - assertEquals("Verify connected state reached", ably.connection.state, ConnectionState.connected); - - /* create a channel and attach */ - final Channel channel = ably.channels.get("test"); - channel.attach(); - (new ChannelWaiter(channel)).waitFor(ChannelState.attached); - assertEquals("Verify attached state reached", channel.state, ChannelState.attached); - - ProtocolMessage protoMessage = new ProtocolMessage(ProtocolMessage.Action.detach, "test"); - protoMessage.error = new ErrorInfo("test error", 123); - - ConnectionManager connectionManager = ably.connection.connectionManager; - connectionManager.onMessage(null, protoMessage); - - /* Because of (RTL13) channel should now be in either attaching or attached state */ - assertNotEquals("channel state shouldn't be failed", channel.state, ChannelState.failed); - } catch (AblyException e) { - e.printStackTrace(); - fail("init0: Unexpected exception instantiating library"); - } finally { - if(ably != null) - ably.close(); - } - } - - @Test - public void channel_state_on_connection_suspended() { - AblyRealtime ably = null; - try { - ClientOptions opts = createOptions(testVars.keys[0].keyStr); - - ably = new AblyRealtime(opts); - - /* wait until connected */ - (new ConnectionWaiter(ably.connection)).waitFor(ConnectionState.connected); - assertEquals("Verify connected state reached", ably.connection.state, ConnectionState.connected); - - /* create a channel and attach */ - final String channelName = "test_state_channel"; - final Channel channel = ably.channels.get(channelName); - channel.attach(); - - ChannelWaiter channelWaiter = new ChannelWaiter(channel); - channelWaiter.waitFor(ChannelState.attached); - assertEquals("Verify attached state reached", channel.state, ChannelState.attached); - - /* switch to suspended state */ - ably.connection.connectionManager.requestState(ConnectionState.suspended); - - channelWaiter.waitFor(ChannelState.suspended); - assertEquals("Verify suspended state reached", channel.state, ChannelState.suspended); - - /* switch to connected state */ - ably.connection.connectionManager.requestState(ConnectionState.connected); - - channelWaiter.waitFor(ChannelState.attached); - assertEquals("Verify attached state reached", channel.state, ChannelState.attached); - - /* switch to failed state */ - ably.connection.connectionManager.requestState(ConnectionState.failed); - - channelWaiter.waitFor(ChannelState.failed); - assertEquals("Verify failed state reached", channel.state, ChannelState.failed); - - } catch (AblyException e) { - e.printStackTrace(); - fail("Unexpected exception"); - } finally { - if (ably != null) - ably.close(); - } - } - - /* - * Establish connection, attach channel, simulate sending attached and detached messages - * from the server, test correct behaviour - * - * Tests RTL12, RTL13a - */ - @Test - public void channel_server_initiated_attached_detached() throws AblyException { - AblyRealtime ably = null; - long oldRealtimeTimeout = Defaults.realtimeRequestTimeout; - final String channelName = "channel_server_initiated_attach_detach"; - - try { - ClientOptions opts = createOptions(testVars.keys[0].keyStr); - - /* Make test faster */ - Defaults.realtimeRequestTimeout = 1000; - opts.channelRetryTimeout = 1000; - - ably = new AblyRealtime(opts); - - Channel channel = ably.channels.get(channelName); - ChannelWaiter channelWaiter = new ChannelWaiter(channel); - - channel.attach(); - channelWaiter.waitFor(ChannelState.attached); - - final int[] updateEventsEmitted = new int[]{0}; - final boolean[] resumedFlag = new boolean[]{true}; - channel.on(ChannelEvent.update, new ChannelStateListener() { - @Override - public void onChannelStateChanged(ChannelStateChange stateChange) { - updateEventsEmitted[0]++; - resumedFlag[0] = stateChange.resumed; - } - }); - - /* Inject attached message as if received from the server */ - ProtocolMessage attachedMessage = new ProtocolMessage() {{ - action = Action.attached; - channel = channelName; - flags |= Flag.resumed.getMask(); - }}; - ably.connection.connectionManager.onMessage(null, attachedMessage); - - /* Inject detached message as if from the server */ - ProtocolMessage detachedMessage = new ProtocolMessage() {{ - action = Action.detached; - channel = channelName; - }}; - ably.connection.connectionManager.onMessage(null, detachedMessage); - - /* Channel should transition to attaching, then to attached */ - channelWaiter.waitFor(ChannelState.attaching); - channelWaiter.waitFor(ChannelState.attached); - - /* Verify received UPDATE message on channel */ - assertEquals("Verify exactly one UPDATE event was emitted on the channel", updateEventsEmitted[0], 1); - assertTrue("Verify resumed flag set in UPDATE event", resumedFlag[0]); - } finally { - if (ably != null) - ably.close(); - Defaults.realtimeRequestTimeout = oldRealtimeTimeout; - } - } - - /* - * Establish connection, attach channel, disconnection and failed resume - * verify that subsequent attaches are performed, and give rise to update events - * - * Tests RTN15c3 - */ - @Test - public void channel_resume_lost_continuity() throws AblyException { - AblyRealtime ably = null; - final String attachedChannelName = "channel_resume_lost_continuity_attached"; - final String suspendedChannelName = "channel_resume_lost_continuity_suspended"; - - try { - ClientOptions opts = createOptions(testVars.keys[0].keyStr); - ably = new AblyRealtime(opts); - - /* prepare channels */ - Channel attachedChannel = ably.channels.get(attachedChannelName); - ChannelWaiter attachedChannelWaiter = new ChannelWaiter(attachedChannel); - attachedChannel.attach(); - attachedChannelWaiter.waitFor(ChannelState.attached); - - Channel suspendedChannel = ably.channels.get(suspendedChannelName); - suspendedChannel.state = ChannelState.suspended; - ChannelWaiter suspendedChannelWaiter = new ChannelWaiter(suspendedChannel); - - final boolean[] suspendedStateReached = new boolean[2]; - final boolean[] attachingStateReached = new boolean[2]; - final boolean[] attachedStateReached = new boolean[2]; - final boolean[] resumedFlag = new boolean[]{true, true}; - attachedChannel.on(new ChannelStateListener() { - @Override - public void onChannelStateChanged(ChannelStateChange stateChange) { - switch(stateChange.current) { - case suspended: - suspendedStateReached[0] = true; - break; - case attaching: - attachingStateReached[0] = true; - break; - case attached: - attachedStateReached[0] = true; - resumedFlag[0] = stateChange.resumed; - break; - default: - break; - } - } - }); - suspendedChannel.on(new ChannelStateListener() { - @Override - public void onChannelStateChanged(ChannelStateChange stateChange) { - switch(stateChange.current) { - case attaching: - attachingStateReached[1] = true; - break; - case attached: - attachedStateReached[1] = true; - resumedFlag[1] = stateChange.resumed; - break; - default: - break; - } - } - }); - - /* disconnect, and sabotage the resume */ - String originalConnectionId = ably.connection.id; - ably.connection.key = "_____!ably___test_fake-key____"; - ably.connection.id = "ably___tes"; - ConnectionWaiter connectionWaiter = new ConnectionWaiter(ably.connection); - - /* suppress automatic retries by the connection manager */ - try { - Method method = ably.connection.connectionManager.getClass().getDeclaredMethod("disconnectAndSuppressRetries"); - method.setAccessible(true); - method.invoke(ably.connection.connectionManager); - } catch (NoSuchMethodException|IllegalAccessException|InvocationTargetException e) { - fail("Unexpected exception in suppressing retries"); - } - - connectionWaiter.waitFor(ConnectionState.disconnected); - assertEquals("Verify disconnected state is reached", ConnectionState.disconnected, ably.connection.state); - - /* wait */ - try { Thread.sleep(2000L); } catch(InterruptedException e) {} - - /* wait for connection to be reestablished */ - System.out.println("channel_resume_lost_continuity: initiating reconnection (resume)"); - ably.connection.connect(); - connectionWaiter.waitFor(ConnectionState.connected); - - /* verify a new connection was assigned */ - assertNotEquals("A new connection was created", originalConnectionId, ably.connection.id); - - /* previously suspended channel should transition to attaching, then to attached */ - suspendedChannelWaiter.waitFor(ChannelState.attached); - - /* previously attached channel should remain attached */ - attachedChannelWaiter.waitFor(ChannelState.attached); - - /* - * Verify each channel undergoes relevant events: - * - previously attached channel does attaching, attached, without visiting suspended; - * - previously suspended channel does attaching, attached - */ - assertEquals("Verify channel was not suspended", suspendedStateReached[0], false); - assertEquals("Verify channel was attaching", attachingStateReached[0], true); - assertEquals("Verify channel was attached", attachedStateReached[0], true); - assertFalse("Verify resumed flag set false in ATTACHED event", resumedFlag[0]); - - assertEquals("Verify channel was attaching", attachingStateReached[1], true); - assertEquals("Verify channel was attached", attachedStateReached[1], true); - assertFalse("Verify resumed flag set false in ATTACHED event", resumedFlag[1]); - } finally { - if (ably != null) - ably.close(); - } - } - - /* - * Initiate connection, block send on transport to simulate network packet loss, try to attach, wait for - * channel to eventually attach when send is re-enabled on transport. - * - * Then suspend connection, resume it and immediately block sending packets failing channel - * reattach. Verify that the channel goes back to suspended state on timeout with correct error code. - * - * Try to detach channel while blocking send, channel should go back to attached state through detaching - * - * Tests features RTL4c, RTL4f, RTL5f, RTL5e - */ - @Test - public void channel_attach_retry_failed() { - AblyRealtime ably = null; - String channelName = "channel_attach_retry_failed_" + testParams.name; - long oldRealtimeTimeout = Defaults.realtimeRequestTimeout; - try { - DebugOptions opts = new DebugOptions(testVars.keys[0].keyStr); - fillInOptions(opts); - opts.channelRetryTimeout = 1000; - - /* Mock transport to block send */ - final MockWebsocketFactory mockTransport = new MockWebsocketFactory(); - opts.transportFactory = mockTransport; - mockTransport.allowSend(); - - /* Reduce timeout for test to run faster */ - Defaults.realtimeRequestTimeout = 1000; - - ably = new AblyRealtime(opts); - ConnectionWaiter connectionWaiter = new ConnectionWaiter(ably.connection); - connectionWaiter.waitFor(ConnectionState.connected); - - /* Block send() and attach */ - mockTransport.blockSend(); - - Channel channel = ably.channels.get(channelName); - ChannelWaiter channelWaiter = new ChannelWaiter(channel); - channel.attach(); - - channelWaiter.waitFor(ChannelState.attaching); - - /* Should get to suspended soon because send() is blocked */ - channelWaiter.waitFor(ChannelState.suspended); - - /* Re-enable send() and wait for channel to attach */ - mockTransport.allowSend(); - channelWaiter.waitFor(ChannelState.attached); - - /* Suspend connection: channel state should change to suspended */ - ably.connection.connectionManager.requestState(ConnectionState.suspended); - channelWaiter.waitFor(ChannelState.suspended); - - /* Reconnect and immediately block transport's send(). This should fail channel reattach */ - ably.connection.once(ConnectionState.connected, new ConnectionStateListener() { - @Override - public void onConnectionStateChanged(ConnectionStateChange state) { - mockTransport.blockSend(); - } - }); - ably.connection.connectionManager.requestState(ConnectionState.connected); - - /* Channel should move to attaching state */ - channelWaiter.waitFor(ChannelState.attaching); - /* - * Then within realtimeRequestTimeout interval it should get back to suspended - */ - channelWaiter.waitFor(ChannelState.suspended); - - /* In channelRetryTimeout we should get back to attaching state again */ - channelWaiter.waitFor(ChannelState.attaching); - /* And then suspended in another 1 second */ - channelWaiter.waitFor(ChannelState.suspended); - - /* Enable message sending again */ - mockTransport.allowSend(); - - /* And wait for attached state of the channel */ - channelWaiter.waitFor(ChannelState.attached); - - final ErrorInfo[] errorDetaching = new ErrorInfo[] {null}; - - /* Block and detach */ - mockTransport.blockSend(); - channel.detach(new CompletionListener() { - @Override - public void onSuccess() { - fail("Detach succeeded"); - } - - @Override - public void onError(ErrorInfo reason) { - synchronized (errorDetaching) { - errorDetaching[0] = reason; - errorDetaching.notify(); - } - } - }); - - /* Should get to detaching first */ - channelWaiter.waitFor(ChannelState.detaching); - /* And then back to attached on timeout */ - channelWaiter.waitFor(ChannelState.attached); - try { - synchronized (errorDetaching) { - if (errorDetaching[0] != null) - errorDetaching.wait(1000); - } - } catch (InterruptedException e) {} - - assertNotNull("Verify detach operation failed", errorDetaching[0]); - - } catch(AblyException e) { - e.printStackTrace(); - fail("Unexpected exception"); - } finally { - if (ably != null) - ably.close(); - /* Restore default values to run other tests */ - Defaults.realtimeRequestTimeout = oldRealtimeTimeout; - } - } - - /* - * Initiate connection, and attach to a channel. Simulate a server-initiated detach of the channel - * and verify that the client attempts to reattach. Block the transport to prevent that from - * succeeding. Verify that the channel enters the suspended state with the appropriate error. - * - * Tests features RTL13b - */ - @Test - public void channel_reattach_failed_timeout() { - AblyRealtime ably = null; - final String channelName = "channel_reattach_failed_timeout_" + testParams.name; - long oldRealtimeTimeout = Defaults.realtimeRequestTimeout; - try { - DebugOptions opts = new DebugOptions(testVars.keys[0].keyStr); - fillInOptions(opts); - opts.channelRetryTimeout = 1000; - - /* Mock transport to block send */ - final MockWebsocketFactory mockTransport = new MockWebsocketFactory(); - opts.transportFactory = mockTransport; - mockTransport.allowSend(); - - /* Reduce timeout for test to run faster */ - Defaults.realtimeRequestTimeout = 1000; - - ably = new AblyRealtime(opts); - ConnectionWaiter connectionWaiter = new ConnectionWaiter(ably.connection); - connectionWaiter.waitFor(ConnectionState.connected); - - Channel channel = ably.channels.get(channelName); - ChannelWaiter channelWaiter = new ChannelWaiter(channel); - channel.attach(); - - channelWaiter.waitFor(ChannelState.attached); - - /* Block send() */ - mockTransport.blockSend(); - - /* Inject detached message as if from the server */ - ProtocolMessage detachedMessage = new ProtocolMessage() {{ - action = Action.detached; - channel = channelName; - }}; - ably.connection.connectionManager.onMessage(null, detachedMessage); - - /* Should get to suspended soon because send() is blocked */ - ErrorInfo suspendReason = channelWaiter.waitFor(ChannelState.suspended); - assertEquals("Verify the suspended event contains the detach reason", 91200, suspendReason.code); - - /* Unblock send(), and expect a transition to attached */ - mockTransport.allowSend(); - channelWaiter.waitFor(ChannelState.attached); - - } catch(AblyException e) { - e.printStackTrace(); - fail("channel_reattach_failed_timeout: unexpected exception"); - } finally { - if (ably != null) { - ably.close(); - } - /* Restore default values to run other tests */ - Defaults.realtimeRequestTimeout = oldRealtimeTimeout; - } - } - - /* - * Initiate connection, and attach to a channel. Simulate a server-initiated detach of the channel - * and verify that the client attempts to reattach. Block the transport and respond instead with - * an attach error. Verify that the channel enters the suspended state with the appropriate error. - * - * Tests features RTL13b - */ - @Test - public void channel_reattach_failed_error() { - AblyRealtime ably = null; - final String channelName = "channel_reattach_failed_error_" + testParams.name; - final int errorCode = 12345; - long oldRealtimeTimeout = Defaults.realtimeRequestTimeout; - try { - DebugOptions opts = new DebugOptions(testVars.keys[0].keyStr); - fillInOptions(opts); - opts.channelRetryTimeout = 1000; - - /* Mock transport to block send */ - final MockWebsocketFactory mockTransport = new MockWebsocketFactory(); - opts.transportFactory = mockTransport; - mockTransport.allowSend(); - - /* Reduce timeout for test to run faster */ - Defaults.realtimeRequestTimeout = 5000; - - ably = new AblyRealtime(opts); - ConnectionWaiter connectionWaiter = new ConnectionWaiter(ably.connection); - connectionWaiter.waitFor(ConnectionState.connected); - - Channel channel = ably.channels.get(channelName); - ChannelWaiter channelWaiter = new ChannelWaiter(channel); - channel.attach(); - - channelWaiter.waitFor(ChannelState.attached); - - /* Block send() */ - mockTransport.blockSend(); - - /* Inject detached message as if from the server */ - ProtocolMessage detachedMessage = new ProtocolMessage() {{ - action = Action.detached; - channel = channelName; - error = new ErrorInfo("Test error", errorCode); - }}; - ably.connection.connectionManager.onMessage(null, detachedMessage); - - /* wait for the client reattempt attachment */ - channelWaiter.waitFor(ChannelState.attaching); - - /* Inject detached+error message as if from the server */ - ProtocolMessage errorMessage = new ProtocolMessage() {{ - action = Action.detached; - channel = channelName; - error = new ErrorInfo("Test error", errorCode); - }}; - ably.connection.connectionManager.onMessage(null, errorMessage); - - /* Should get to suspended soon because there was an error response to the attach attempt */ - ErrorInfo suspendReason = channelWaiter.waitFor(ChannelState.suspended); - assertEquals("Verify the suspended event contains the detach reason", errorCode, suspendReason.code); - - /* Unblock send(), and expect a transition to attached */ - mockTransport.allowSend(); - channelWaiter.waitFor(ChannelState.attached); - - } catch(AblyException e) { - e.printStackTrace(); - fail("Unexpected exception"); - } finally { - if (ably != null) - ably.close(); - /* Restore default values to run other tests */ - Defaults.realtimeRequestTimeout = oldRealtimeTimeout; - } - } - - /** - * Initiate an attach when not connected; verify that the given listener is called - * with the attach error - */ - @Test - public void attach_exception_listener_called() { - try { - final String channelName = "attach_exception_listener_called_" + testParams.name; - - /* init Ably */ - ClientOptions opts = createOptions(testVars.keys[0].keyStr); - AblyRealtime ably = new AblyRealtime(opts); - - /* wait until connected */ - (new ConnectionWaiter(ably.connection)).waitFor(ConnectionState.connected); - assertEquals("Verify connected state reached", ably.connection.state, ConnectionState.connected); - - /* create a channel; put into failed state */ - ably.connection.connectionManager.requestState(new ConnectionManager.StateIndication(ConnectionState.failed, new ErrorInfo("Test error", 400, 12345))); - (new ConnectionWaiter(ably.connection)).waitFor(ConnectionState.failed); - assertEquals("Verify failed state reached", ably.connection.state, ConnectionState.failed); - - /* attempt to attach */ - Channel channel = ably.channels.get(channelName); - final ErrorInfo[] listenerError = new ErrorInfo[1]; - synchronized(listenerError) { - channel.attach(new CompletionListener() { - @Override - public void onSuccess() { - synchronized (listenerError) { - listenerError.notify(); - } - fail("Unexpected attach success"); - } - - @Override - public void onError(ErrorInfo reason) { - synchronized (listenerError) { - listenerError[0] = reason; - listenerError.notify(); - } - } - }); - - /* wait until the listener is called */ - while(listenerError[0] == null) { - try { listenerError.wait(); } catch(InterruptedException e) {} - } - } - - /* verify that the listener was called with an error */ - assertNotNull("Verify the error callback was called", listenerError[0]); - assertEquals("Verify the given error is indicated", listenerError[0].code, 12345); - - /* tidy */ - ably.close(); - } catch(AblyException e) { - fail(e.getMessage()); - } - - } - - @Test - public void no_messages_when_channel_state_not_attached() { - - AblyRealtime senderReceiver = null; - final String testMessage1 = "{ foo: \"bar\", count: 1, status: \"active\" }"; - final String testMessage2 = "{ foo: \"bar\", count: 2, status: \"active\" }"; - - String testName = "no_messages_when_channel_state_not_attached"; - try { - - DebugOptions common_opts = createOptions(testVars.keys[0].keyStr); - common_opts.protocolListener = new DetachingProtocolListener(); - senderReceiver = new AblyRealtime(common_opts); - - Channel sender_channel = senderReceiver.channels.get(testName); - ((DetachingProtocolListener)common_opts.protocolListener).theChannel = sender_channel; - - - sender_channel.attach(); - (new ChannelWaiter(sender_channel)).waitFor(ChannelState.attached); - - Helpers.MessageWaiter messageWaiter_1 = new Helpers.MessageWaiter(sender_channel); - - sender_channel.publish("1", testMessage1); - - messageWaiter_1.waitFor(1); - assertEquals("Verify rewound message", testMessage1, messageWaiter_1.receivedMessages.get(0).data); - messageWaiter_1.reset(); - - sender_channel.publish("2", testMessage2); - messageWaiter_1.waitFor(1, 7000); - assertEquals("Verify no message received on attach_rewind", 0, messageWaiter_1.receivedMessages.size()); - - } catch(Exception e) { - fail(testName + ": Unexpected exception " + e.getMessage()); - e.printStackTrace(); - } finally { - if(senderReceiver != null) - senderReceiver.close(); - } - } - - class DetachingProtocolListener implements DebugOptions.RawProtocolListener { - - public Channel theChannel; - boolean messageReceived; - - public DetachingProtocolListener() { - messageReceived = false; - } - - @Override - public void onRawConnect(String url) {} - @Override - public void onRawConnectRequested(String url) {} - @Override - public void onRawMessageSend(ProtocolMessage message) { - } - @Override - public void onRawMessageRecv(ProtocolMessage message) { - if(message.action == ProtocolMessage.Action.message) { - if (!messageReceived) { - messageReceived = true; - return; - } - - theChannel.state = ChannelState.attaching; - } - } - }; + private Comparator messageComparator = new Comparator() { + @Override + public int compare(Message o1, Message o2) { + int result = o1.name.compareTo(o2.name); + return (result == 0)?(((String) o1.data).compareTo((String) o2.data)):(result); + } + }; + + + /** + * Connect to the service and attach to a channel, + * confirming that the attached state is reached. + */ + @Test + public void attach() { + String channelName = "attach_" + testParams.name; + AblyRealtime ably = null; + try { + ClientOptions opts = createOptions(testVars.keys[0].keyStr); + ably = new AblyRealtime(opts); + + /* wait until connected */ + (new ConnectionWaiter(ably.connection)).waitFor(ConnectionState.connected); + assertEquals("Verify connected state reached", ably.connection.state, ConnectionState.connected); + + /* create a channel and attach */ + final Channel channel = ably.channels.get(channelName); + channel.attach(); + (new ChannelWaiter(channel)).waitFor(ChannelState.attached); + assertEquals("Verify attached state reached", channel.state, ChannelState.attached); + + } catch (AblyException e) { + e.printStackTrace(); + fail("init0: Unexpected exception instantiating library"); + } finally { + if(ably != null) + ably.close(); + } + } + + /** + * Connect to the service using the default (binary) protocol + * and attach before the connected state is reached. + */ + @Test + public void attach_before_connect() { + String channelName = "attach_before_connect_" + testParams.name; + AblyRealtime ably = null; + try { + ClientOptions opts = createOptions(testVars.keys[0].keyStr); + ably = new AblyRealtime(opts); + + /* create a channel and attach */ + final Channel channel = ably.channels.get(channelName); + channel.attach(); + (new ChannelWaiter(channel)).waitFor(ChannelState.attached); + assertEquals("Verify attached state reached", channel.state, ChannelState.attached); + + } catch (AblyException e) { + e.printStackTrace(); + fail("init0: Unexpected exception instantiating library"); + } finally { + if(ably != null) + ably.close(); + } + } + + /** + * Connect to the service using the default (binary) protocol + * and attach, then detach + */ + @Test + public void attach_detach() { + String channelName = "attach_detach_" + testParams.name; + AblyRealtime ably = null; + try { + ClientOptions opts = createOptions(testVars.keys[0].keyStr); + ably = new AblyRealtime(opts); + + /* create a channel and attach */ + final Channel channel = ably.channels.get(channelName); + channel.attach(); + (new ChannelWaiter(channel)).waitFor(ChannelState.attached); + assertEquals("Verify attached state reached", channel.state, ChannelState.attached); + + /* detach */ + channel.detach(); + (new ChannelWaiter(channel)).waitFor(ChannelState.detached); + assertEquals("Verify detached state reached", channel.state, ChannelState.detached); + + } catch (AblyException e) { + e.printStackTrace(); + fail("init0: Unexpected exception instantiating library"); + } finally { + if(ably != null) + ably.close(); + } + } + + /*@Test*/ + public void attach_with_channel_params_channels_get() { + String channelName = "attach_with_channel_params_channels_get_" + testParams.name; + AblyRealtime ably = null; + try { + ClientOptions opts = createOptions(testVars.keys[0].keyStr); + ably = new AblyRealtime(opts); + + /* wait until connected */ + (new ConnectionWaiter(ably.connection)).waitFor(ConnectionState.connected); + assertEquals("Verify connected state reached", ConnectionState.connected, ably.connection.state); + + ChannelOptions options = new ChannelOptions(); + options.params = new HashMap(); + options.params.put("modes", "subscribe"); + options.params.put("delta", "vcdiff"); + + /* create a channel and attach */ + final Channel channel = ably.channels.get(channelName, options); + channel.attach(); + (new ChannelWaiter(channel)).waitFor(ChannelState.attached); + assertEquals("Verify attached state reached", ChannelState.attached, channel.state); + assertEquals("Verify channel params", channel.getParams(), options.params); + assertArrayEquals("Verify channel modes", new ChannelMode[] { ChannelMode.subscribe }, channel.getModes()); + } catch (AblyException e) { + e.printStackTrace(); + fail("init0: Unexpected exception instantiating library"); + } finally { + if(ably != null) + ably.close(); + } + } + + /*@Test*/ + public void attach_with_channel_params_set_options() { + String channelName = "attach_with_channel_params_set_options_" + testParams.name; + AblyRealtime ably = null; + try { + ClientOptions opts = createOptions(testVars.keys[0].keyStr); + ably = new AblyRealtime(opts); + + /* wait until connected */ + (new ConnectionWaiter(ably.connection)).waitFor(ConnectionState.connected); + assertEquals("Verify connected state reached", ConnectionState.connected, ably.connection.state); + + ChannelOptions options = new ChannelOptions(); + options.params.put("modes", "subscribe"); + options.params.put("delta", "vcdiff"); + + /* create a channel and attach */ + final Channel channel = ably.channels.get(channelName); + channel.setOptions(options); + channel.attach(); + (new ChannelWaiter(channel)).waitFor(ChannelState.attached); + assertEquals("Verify attached state reached", ChannelState.attached, channel.state); + assertEquals("Verify channel params", channel.getParams(), options.params); + assertArrayEquals("Verify channel modes", new ChannelMode[] { ChannelMode.subscribe }, channel.getModes()); + } catch (AblyException e) { + e.printStackTrace(); + fail("init0: Unexpected exception instantiating library"); + } finally { + if(ably != null) + ably.close(); + } + } + + /*@Test*/ + public void channels_get_should_throw_when_would_cause_reattach() { + String channelName = "channels_get_should_throw_when_would_cause_reattach_" + testParams.name; + AblyRealtime ably = null; + try { + ClientOptions opts = createOptions(testVars.keys[0].keyStr); + ably = new AblyRealtime(opts); + + /* wait until connected */ + (new ConnectionWaiter(ably.connection)).waitFor(ConnectionState.connected); + assertEquals("Verify connected state reached", ConnectionState.connected, ably.connection.state); + + ChannelOptions options = new ChannelOptions(); + options.params.put("modes", "subscribe"); + options.params.put("delta", "vcdiff"); + + /* create a channel and attach */ + final Channel channel = ably.channels.get(channelName, options); + channel.attach(); + (new ChannelWaiter(channel)).waitFor(ChannelState.attached); + + try { + ably.channels.get(channelName, options); + } catch (AblyException e) { + assertEquals("Verify error code", 400, e.errorInfo.code); + assertEquals("Verify error status code", 40000, e.errorInfo.statusCode); + assertTrue("Verify error message", e.errorInfo.message.contains("setOptions")); + } + } catch (AblyException e) { + e.printStackTrace(); + fail("init0: Unexpected exception instantiating library"); + } finally { + if(ably != null) + ably.close(); + } + } + + /*@Test*/ + public void attach_with_channel_params_modes_and_channel_modes() { + String channelName = "attach_with_channel_params_modes_and_channel_modes_" + testParams.name; + AblyRealtime ably = null; + try { + ClientOptions opts = createOptions(testVars.keys[0].keyStr); + ably = new AblyRealtime(opts); + + /* wait until connected */ + (new ConnectionWaiter(ably.connection)).waitFor(ConnectionState.connected); + assertEquals("Verify connected state reached", ConnectionState.connected, ably.connection.state); + + ChannelOptions options = new ChannelOptions(); + options.params = new HashMap(); + options.params.put("modes", "presence,subscribe"); + options.modes = new ChannelMode[] { + ChannelMode.publish, + ChannelMode.presence_subscribe + }; + + /* create a channel and attach */ + final Channel channel = ably.channels.get(channelName, options); + channel.attach(); + (new ChannelWaiter(channel)).waitFor(ChannelState.attached); + assertEquals("Verify attached state reached", ChannelState.attached, channel.state); + assertEquals("Verify channel params", channel.getParams(), options.params); + assertArrayEquals("Verify channel modes", new ChannelMode[] { ChannelMode.subscribe, ChannelMode.presence }, channel.getModes()); + } catch (AblyException e) { + e.printStackTrace(); + fail("init0: Unexpected exception instantiating library"); + } finally { + if(ably != null) + ably.close(); + } + } + + /*@Test*/ + public void attach_with_channel_modes() { + String channelName = "attach_with_channel_modes_" + testParams.name; + AblyRealtime ably = null; + try { + ClientOptions opts = createOptions(testVars.keys[0].keyStr); + ably = new AblyRealtime(opts); + + /* wait until connected */ + (new ConnectionWaiter(ably.connection)).waitFor(ConnectionState.connected); + assertEquals("Verify connected state reached", ConnectionState.connected, ably.connection.state); + + ChannelOptions options = new ChannelOptions(); + options.modes = new ChannelMode[] { + ChannelMode.publish, + ChannelMode.presence_subscribe, + }; + + /* create a channel and attach */ + final Channel channel = ably.channels.get(channelName, options); + channel.attach(); + (new ChannelWaiter(channel)).waitFor(ChannelState.attached); + assertEquals("Verify attached state reached", ChannelState.attached, channel.state); + assertEquals("Verify channel modes", channel.getModes(), options.modes); + } catch (AblyException e) { + e.printStackTrace(); + fail("init0: Unexpected exception instantiating library"); + } finally { + if(ably != null) + ably.close(); + } + } + + /*@Test*/ + public void attach_with_params_delta_and_channel_modes() { + String channelName = "attach_with_params_delta_and_channel_modes_" + testParams.name; + AblyRealtime ably = null; + try { + ClientOptions opts = createOptions(testVars.keys[0].keyStr); + ably = new AblyRealtime(opts); + + /* wait until connected */ + (new ConnectionWaiter(ably.connection)).waitFor(ConnectionState.connected); + assertEquals("Verify connected state reached", ConnectionState.connected, ably.connection.state); + + ChannelOptions options = new ChannelOptions(); + options.params = new HashMap(); + options.params.put("delta", "vcdiff"); + options.modes = new ChannelMode[] { + ChannelMode.publish, + ChannelMode.subscribe, + ChannelMode.presence_subscribe, + }; + + /* create a channel and attach */ + final Channel channel = ably.channels.get(channelName, options); + channel.attach(); + (new ChannelWaiter(channel)).waitFor(ChannelState.attached); + assertEquals("Verify attached state reached", ChannelState.attached, channel.state); + options.params.put("modes", "publish,subscribe,presence_subscribe"); + assertEquals("Verify channel params", channel.getParams(), options.params); + assertEquals("Verify channel modes", channel.getModes(), options.modes); + } catch (AblyException e) { + e.printStackTrace(); + fail("init0: Unexpected exception instantiating library"); + } finally { + if(ably != null) + ably.close(); + } + } + + /** + * Connect to the service and attach, then subscribe and unsubscribe + */ + @Test + public void subscribe_unsubscribe() { + String channelName = "subscribe_unsubscribe_" + testParams.name; + AblyRealtime ably = null; + try { + ClientOptions opts = createOptions(testVars.keys[0].keyStr); + ably = new AblyRealtime(opts); + + /* create a channel and attach */ + final Channel channel = ably.channels.get(channelName); + channel.attach(); + (new ChannelWaiter(channel)).waitFor(ChannelState.attached); + assertEquals("Verify attached state reached", channel.state, ChannelState.attached); + + /* subscribe */ + MessageListener testListener = new MessageListener() { + @Override + public void onMessage(Message message) { + }}; + channel.subscribe("test_event", testListener); + /* unsubscribe */ + channel.unsubscribe("test_event", testListener); + } catch (AblyException e) { + e.printStackTrace(); + fail("init0: Unexpected exception instantiating library"); + } finally { + if(ably != null) + ably.close(); + } + } + + /** + *

+ * Verifies that unsubscribe call with no argument removes all listeners, + * and any of the previously subscribed listeners doesn't receive any message + * after that. + *

+ *

+ * Spec: RTL8a + *

+ */ + @Test + public void unsubscribe_all() throws AblyException { + /* Ably instance that will emit messages */ + AblyRealtime ably1 = null; + /* Ably instance that will receive messages */ + AblyRealtime ably2 = null; + + String channelName = "test.channel.unsubscribe.all" + System.currentTimeMillis(); + Message[] messages = new Message[] { + new Message("name1", "Lorem ipsum dolor sit amet"), + new Message("name2", "Consectetur adipiscing elit."), + new Message("name3", "Pellentesque nulla lorem"), + new Message("name4", "Efficitur ac consequat a, commodo ut orci."), + }; + + try { + ClientOptions option1 = createOptions(testVars.keys[0].keyStr); + option1.clientId = "emitter client"; + ClientOptions option2 = createOptions(testVars.keys[0].keyStr); + option2.clientId = "receiver client"; + + ably1 = new AblyRealtime(option1); + ably2 = new AblyRealtime(option2); + + Channel channel1 = ably1.channels.get(channelName); + channel1.attach(); + new ChannelWaiter(channel1).waitFor(ChannelState.attached); + + Channel channel2 = ably2.channels.get(channelName); + channel2.attach(); + new ChannelWaiter(channel2).waitFor(ChannelState.attached); + + /* Create a listener that collect received messages */ + ArrayList receivedMessageStack = new ArrayList<>(); + MessageListener listener = new MessageListener() { + List messageStack; + + @Override + public void onMessage(Message message) { + messageStack.add(message); + } + + public MessageListener setMessageStack(List messageStack) { + this.messageStack = messageStack; + return this; + } + }.setMessageStack(receivedMessageStack); + + /* Subscribe using various alternatives of {@code Channel#subscribe()} */ + channel2.subscribe(listener); + channel2.subscribe(messages[0].name, listener); + channel2.subscribe(new String[] {messages[1].name, messages[2].name}, listener); + + /* Unsubscribe */ + channel2.unsubscribe(); + + /* Start emitting channel with ably client 1 (emitter) */ + Helpers.CompletionWaiter waiter = new Helpers.CompletionWaiter(); + channel1.publish(messages, waiter); + waiter.waitFor(); + + /* Validate that we didn't received anything + */ + assertThat(receivedMessageStack, Matchers.is(Matchers.emptyCollectionOf(Message.class))); + } finally { + if (ably1 != null) ably1.close(); + if (ably2 != null) ably2.close(); + } + } + + /** + *

+ * Validates channel removes a subscriber, + * when {@code Channel#unsubscribe()} gets called with a listener argument. + *

+ * + * @throws AblyException + */ + @Test + public void unsubscribe_single() throws AblyException { + /* Ably instance that will emit messages */ + AblyRealtime ably1 = null; + /* Ably instance that will receive messages */ + AblyRealtime ably2 = null; + + String channelName = "test.channel.unsubscribe.single" + System.currentTimeMillis(); + Message[] messages = new Message[] { + new Message("name1", "Lorem ipsum dolor sit amet"), + new Message("name2", "Consectetur adipiscing elit."), + new Message("name3", "Pellentesque nulla lorem"), + new Message("name4", "Efficitur ac consequat a, commodo ut orci."), + }; + + try { + ClientOptions option1 = createOptions(testVars.keys[0].keyStr); + option1.clientId = "emitter client"; + ClientOptions option2 = createOptions(testVars.keys[0].keyStr); + option2.clientId = "receiver client"; + + ably1 = new AblyRealtime(option1); + ably2 = new AblyRealtime(option2); + + Channel channel1 = ably1.channels.get(channelName); + channel1.attach(); + new ChannelWaiter(channel1).waitFor(ChannelState.attached); + + Channel channel2 = ably2.channels.get(channelName); + channel2.attach(); + new ChannelWaiter(channel2).waitFor(ChannelState.attached); + + /* Create a listener that collect received messages */ + ArrayList receivedMessageStack = new ArrayList<>(); + MessageListener listener = new MessageListener() { + List messageStack; + + @Override + public void onMessage(Message message) { + messageStack.add(message); + } + + public MessageListener setMessageStack(List messageStack) { + this.messageStack = messageStack; + return this; + } + }.setMessageStack(receivedMessageStack); + + /* Subscribe using various alternatives of {@code Channel#subscribe()} */ + channel2.subscribe(listener); + channel2.subscribe(messages[0].name, listener); + channel2.subscribe(new String[] {messages[1].name, messages[2].name}, listener); + + /* Unsubscribe */ + channel2.unsubscribe(listener); + + /* Start emitting channel with ably client 1 (emitter) */ + Helpers.CompletionWaiter waiter = new Helpers.CompletionWaiter(); + channel1.publish(messages, waiter); + waiter.waitFor(); + + /* Validate that we didn't received anything + */ + assertThat(receivedMessageStack, Matchers.is(Matchers.emptyCollectionOf(Message.class))); + } finally { + if (ably1 != null) ably1.close(); + if (ably2 != null) ably2.close(); + } + } + + /** + *

+ * Validates a client can observe channel messages of other client, + * when they entered to the same channel and observing client subscribed + * to all messages. + *

+ * + * @throws AblyException + */ + @Test + public void subscribe_all() throws AblyException { + /* Ably instance that will emit channel messages */ + AblyRealtime ably1 = null; + /* Ably instance that will receive channel messages */ + AblyRealtime ably2 = null; + + String channelName = "test.channel.subscribe.all" + System.currentTimeMillis(); + Message[] messages = new Message[]{ + new Message("name1", "Lorem ipsum dolor sit amet,"), + new Message("name2", "Consectetur adipiscing elit."), + new Message("name3", "Pellentesque nulla lorem.") + }; + + try { + ClientOptions option1 = createOptions(testVars.keys[0].keyStr); + option1.clientId = "emitter client"; + ClientOptions option2 = createOptions(testVars.keys[0].keyStr); + option2.clientId = "receiver client"; + + ably1 = new AblyRealtime(option1); + ably2 = new AblyRealtime(option2); + + Channel channel1 = ably1.channels.get(channelName); + channel1.attach(); + new ChannelWaiter(channel1).waitFor(ChannelState.attached); + + Channel channel2 = ably2.channels.get(channelName); + channel2.attach(); + new ChannelWaiter(channel2).waitFor(ChannelState.attached); + + /* Create a listener that collects received messages */ + ArrayList receivedMessageStack = new ArrayList<>(); + MessageListener listener = new MessageListener() { + List messageStack; + + @Override + public void onMessage(Message message) { + messageStack.add(message); + } + + public MessageListener setMessageStack(List messageStack) { + this.messageStack = messageStack; + return this; + } + }.setMessageStack(receivedMessageStack); + + channel2.subscribe(listener); + + /* Start emitting channel with ably client 1 (emitter) */ + channel1.publish(messages, null); + + /* Wait until receiver client (ably2) observes {@code } + * is emitted from emitter client (ably1) + */ + new Helpers.MessageWaiter(channel2).waitFor(messages.length); + + /* Validate that, + * - we received every message that has been published + */ + assertThat(receivedMessageStack.size(), is(equalTo(messages.length))); + + Collections.sort(receivedMessageStack, messageComparator); + for (int i = 0; i < messages.length; i++) { + Message message = messages[i]; + if(Collections.binarySearch(receivedMessageStack, message, messageComparator) < 0) { + fail("Unable to find expected message: " + message); + } + } + } finally { + if (ably1 != null) ably1.close(); + if (ably2 != null) ably2.close(); + } + } + + /** + *

+ * Validates a client can observe channel messages of other client, + * when they entered to the same channel and observing client subscribed + * to multiple messages. + *

+ * + * @throws AblyException + */ + @Test + public void subscribe_multiple() throws AblyException { + /* Ably instance that will emit channel messages */ + AblyRealtime ably1 = null; + /* Ably instance that will receive channel messages */ + AblyRealtime ably2 = null; + + String channelName = "test.channel.subscribe.multiple" + System.currentTimeMillis(); + Message[] messages = new Message[] { + new Message("name1", "Lorem ipsum dolor sit amet,"), + new Message("name2", "Consectetur adipiscing elit."), + new Message("name3", "Pellentesque nulla lorem.") + }; + + String[] messageNames = new String[] { + messages[0].name, + messages[1].name, + messages[2].name + }; + + try { + ClientOptions option1 = createOptions(testVars.keys[0].keyStr); + option1.clientId = "emitter client"; + ClientOptions option2 = createOptions(testVars.keys[0].keyStr); + option2.clientId = "receiver client"; + + ably1 = new AblyRealtime(option1); + ably2 = new AblyRealtime(option2); + + Channel channel1 = ably1.channels.get(channelName); + channel1.attach(); + new ChannelWaiter(channel1).waitFor(ChannelState.attached); + + Channel channel2 = ably2.channels.get(channelName); + channel2.attach(); + new ChannelWaiter(channel2).waitFor(ChannelState.attached); + + /* Create a listener that collect received messages */ + ArrayList receivedMessageStack = new ArrayList<>(); + MessageListener listener = new MessageListener() { + List messageStack; + + @Override + public void onMessage(Message message) { + messageStack.add(message); + } + + public MessageListener setMessageStack(List messageStack) { + this.messageStack = messageStack; + return this; + } + }.setMessageStack(receivedMessageStack); + channel2.subscribe(messageNames, listener); + + /* Start emitting channel with ably client 1 (emitter) */ + channel1.publish("nonTrackedMessageName", "This message should be ignore by second client (ably2).", null); + channel1.publish(messages, null); + channel1.publish("nonTrackedMessageName", "This message should be ignore by second client (ably2).", null); + + /* Wait until receiver client (ably2) observes {@code Message} + * on subscribed channel (channel2) emitted by emitter client (ably1) + */ + new Helpers.MessageWaiter(channel2).waitFor(messages.length + 2); + + /* Validate that, + * - we received specific messages + */ + assertThat(receivedMessageStack.size(), is(equalTo(messages.length))); + + Collections.sort(receivedMessageStack, messageComparator); + for (int i = 0; i < messages.length; i++) { + Message message = messages[i]; + if(Collections.binarySearch(receivedMessageStack, message, messageComparator) < 0) { + fail("Unable to find expected message: " + message); + } + } + } finally { + if (ably1 != null) ably1.close(); + if (ably2 != null) ably2.close(); + } + } + + /** + *

+ * Validates a client can observe channel messages of other client, + * when they entered to the same channel and observing client subscribed + * to a single message. + *

+ * + * @throws AblyException + */ + @Test + public void subscribe_single() throws AblyException { + /* Ably instance that will emit channel messages */ + AblyRealtime ably1 = null; + /* Ably instance that will receive channel messages */ + AblyRealtime ably2 = null; + + String channelName = "test.channel.subscribe.single" + System.currentTimeMillis(); + String messageName = "name"; + Message[] messages = new Message[] { + new Message(messageName, "Lorem ipsum dolor sit amet,"), + new Message(messageName, "Consectetur adipiscing elit."), + new Message(messageName, "Pellentesque nulla lorem.") + }; + + try { + ClientOptions option1 = createOptions(testVars.keys[0].keyStr); + option1.clientId = "emitter client"; + ClientOptions option2 = createOptions(testVars.keys[0].keyStr); + option2.clientId = "receiver client"; + + ably1 = new AblyRealtime(option1); + ably2 = new AblyRealtime(option2); + + Channel channel1 = ably1.channels.get(channelName); + channel1.attach(); + new ChannelWaiter(channel1).waitFor(ChannelState.attached); + + Channel channel2 = ably2.channels.get(channelName); + channel2.attach(); + new ChannelWaiter(channel2).waitFor(ChannelState.attached); + + ArrayList receivedMessageStack = new ArrayList<>(); + MessageListener listener = new MessageListener() { + List messageStack; + + @Override + public void onMessage(Message message) { + messageStack.add(message); + } + + public MessageListener setMessageStack(List messageStack) { + this.messageStack = messageStack; + return this; + } + }.setMessageStack(receivedMessageStack); + channel2.subscribe(messageName, listener); + + /* Start emitting channel with ably client 1 (emitter) */ + channel1.publish("nonTrackedMessageName", "This message should be ignore by second client (ably2).", null); + channel1.publish(messages, null); + channel1.publish("nonTrackedMessageName", "This message should be ignore by second client (ably2).", null); + + /* Wait until receiver client (ably2) observes {@code Message} + * on subscribed channel (channel2) emitted by emitter client (ably1) + */ + new Helpers.MessageWaiter(channel2).waitFor(messages.length + 2); + + /* Validate that, + * - received same amount of emitted specific message + * - received messages are the ones we emitted + */ + assertThat(receivedMessageStack.size(), is(equalTo(messages.length))); + + Collections.sort(receivedMessageStack, messageComparator); + for (int i = 0; i < messages.length; i++) { + Message message = messages[i]; + if(Collections.binarySearch(receivedMessageStack, message, messageComparator) < 0) { + fail("Unable to find expected message: " + message); + } + } + } finally { + if (ably1 != null) ably1.close(); + if (ably2 != null) ably2.close(); + } + } + + + /** + * Connect to the service using the default (binary) protocol + * and attempt to attach to a channel with credentials that do + * not have access, confirming that the failed state is reached. + */ + @Test + public void attach_fail() { + AblyRealtime ably = null; + try { + ClientOptions opts = createOptions(testVars.keys[1].keyStr); + ably = new AblyRealtime(opts); + + /* wait until connected */ + (new ConnectionWaiter(ably.connection)).waitFor(ConnectionState.connected); + assertEquals("Verify connected state reached", ably.connection.state, ConnectionState.connected); + + /* create a channel and attach */ + final Channel channel = ably.channels.get("attach_fail"); + channel.attach(); + ErrorInfo fail = (new ChannelWaiter(channel)).waitFor(ChannelState.failed); + assertEquals("Verify failed state reached", channel.state, ChannelState.failed); + assertEquals("Verify reason code gives correct failure reason", fail.statusCode, 401); + + } catch (AblyException e) { + e.printStackTrace(); + fail("init0: Unexpected exception instantiating library"); + } finally { + if(ably != null) + ably.close(); + } + } + + /** + * When client attaches to a channel successfully, verify + * attach {@code CompletionListener#onSuccess()} gets called. + */ + @Test + public void attach_success_callback() { + AblyRealtime ably = null; + try { + ClientOptions opts = createOptions(testVars.keys[0].keyStr); + ably = new AblyRealtime(opts); + + /* wait until connected */ + (new ConnectionWaiter(ably.connection)).waitFor(ConnectionState.connected); + assertEquals("Verify connected state reached", ably.connection.state, ConnectionState.connected); + + /* create a channel and attach */ + final Channel channel = ably.channels.get("attach_success"); + Helpers.CompletionWaiter waiter = new Helpers.CompletionWaiter(); + channel.attach(waiter); + new ChannelWaiter(channel).waitFor(ChannelState.attached); + assertEquals("Verify failed state reached", channel.state, ChannelState.attached); + + /* Verify onSuccess callback gets called */ + waiter.waitFor(); + assertThat(waiter.success, is(true)); + } catch (AblyException e) { + e.printStackTrace(); + fail("init0: Unexpected exception instantiating library"); + } finally { + if(ably != null) + ably.close(); + } + } + + /** + * When client failed to attach to a channel, verify + * attach {@code CompletionListener#onError(ErrorInfo)} + * gets called. + */ + @Test + public void attach_fail_callback() { + AblyRealtime ably = null; + try { + ClientOptions opts = createOptions(testVars.keys[1].keyStr); + ably = new AblyRealtime(opts); + + /* wait until connected */ + (new ConnectionWaiter(ably.connection)).waitFor(ConnectionState.connected); + assertEquals("Verify connected state reached", ably.connection.state, ConnectionState.connected); + + /* create a channel and attach */ + final Channel channel = ably.channels.get("attach_fail"); + Helpers.CompletionWaiter waiter = new Helpers.CompletionWaiter(); + channel.attach(waiter); + ErrorInfo fail = (new ChannelWaiter(channel)).waitFor(ChannelState.failed); + assertEquals("Verify failed state reached", channel.state, ChannelState.failed); + assertEquals("Verify reason code gives correct failure reason", fail.statusCode, 401); + + /* Verify error callback gets called with correct status code */ + waiter.waitFor(); + assertThat(waiter.error.statusCode, is(equalTo(401))); + } catch (AblyException e) { + e.printStackTrace(); + fail("init0: Unexpected exception instantiating library"); + } finally { + if(ably != null) + ably.close(); + } + } + + /** + * When client detaches from a channel successfully after initialized state, + * verify attach {@code CompletionListener#onSuccess()} gets called. + */ + @Test + public void detach_success_callback_initialized() { + AblyRealtime ably = null; + try { + ClientOptions opts = createOptions(testVars.keys[0].keyStr); + ably = new AblyRealtime(opts); + + /* wait until connected */ + (new ConnectionWaiter(ably.connection)).waitFor(ConnectionState.connected); + assertEquals("Verify connected state reached", ably.connection.state, ConnectionState.connected); + + /* create a channel and attach */ + final Channel channel = ably.channels.get("detach_success"); + assertEquals("Verify failed state reached", channel.state, ChannelState.initialized); + + /* detach */ + Helpers.CompletionWaiter waiter = new Helpers.CompletionWaiter(); + channel.detach(waiter); + + /* Verify onSuccess callback gets called */ + waiter.waitFor(); + assertThat(waiter.success, is(true)); + } catch (AblyException e) { + e.printStackTrace(); + fail("init0: Unexpected exception instantiating library"); + } finally { + if(ably != null) + ably.close(); + } + } + + /** + * When client detaches from a channel successfully after attached state, + * verify attach {@code CompletionListener#onSuccess()} gets called. + */ + @Test + public void detach_success_callback_attached() throws AblyException { + AblyRealtime ably = null; + try { + ClientOptions opts = createOptions(testVars.keys[0].keyStr); + ably = new AblyRealtime(opts); + + /* wait until connected */ + (new ConnectionWaiter(ably.connection)).waitFor(ConnectionState.connected); + assertEquals("Verify connected state reached", ably.connection.state, ConnectionState.connected); + + /* create a channel and attach */ + final Channel channel = ably.channels.get("detach_success"); + channel.attach(); + new ChannelWaiter(channel).waitFor(ChannelState.attached); + assertEquals("Verify attached state reached", channel.state, ChannelState.attached); + + /* detach */ + Helpers.CompletionWaiter waiter = new Helpers.CompletionWaiter(); + channel.detach(waiter); + + /* Verify onSuccess callback gets called */ + waiter.waitFor(); + assertThat(waiter.success, is(true)); + } finally { + if(ably != null) + ably.close(); + } + } + + /** + * When client detaches from a channel successfully after detaching state, + * verify attach {@code CompletionListener#onSuccess()} gets called. + */ + @Test + public void detach_success_callback_detaching() throws AblyException { + AblyRealtime ably = null; + try { + ClientOptions opts = createOptions(testVars.keys[0].keyStr); + ably = new AblyRealtime(opts); + + /* wait until connected */ + (new ConnectionWaiter(ably.connection)).waitFor(ConnectionState.connected); + assertEquals("Verify connected state reached", ably.connection.state, ConnectionState.connected); + + /* create a channel and attach */ + final Channel channel = ably.channels.get("detach_success"); + channel.attach(); + new ChannelWaiter(channel).waitFor(ChannelState.attached); + assertEquals("Verify attached state reached", channel.state, ChannelState.attached); + + /* detach */ + channel.detach(); + assertEquals("Verify detaching state reached", channel.state, ChannelState.detaching); + Helpers.CompletionWaiter waiter = new Helpers.CompletionWaiter(); + channel.detach(waiter); + + /* Verify onSuccess callback gets called */ + waiter.waitFor(); + assertThat(waiter.success, is(true)); + } finally { + if(ably != null) + ably.close(); + } + } + + /** + * When client detaches from a channel successfully after detached state, + * verify attach {@code CompletionListener#onSuccess()} gets called. + */ + @Test + public void detach_success_callback_detached() throws AblyException { + AblyRealtime ably = null; + try { + ClientOptions opts = createOptions(testVars.keys[0].keyStr); + ably = new AblyRealtime(opts); + + /* wait until connected */ + (new ConnectionWaiter(ably.connection)).waitFor(ConnectionState.connected); + assertEquals("Verify connected state reached", ably.connection.state, ConnectionState.connected); + + /* create a channel and attach */ + final Channel channel = ably.channels.get("detach_success"); + channel.attach(); + new ChannelWaiter(channel).waitFor(ChannelState.attached); + assertEquals("Verify attached state reached", channel.state, ChannelState.attached); + + /* detach */ + channel.detach(); + new ChannelWaiter(channel).waitFor(ChannelState.detached); + + Helpers.CompletionWaiter waiter = new Helpers.CompletionWaiter(); + channel.detach(waiter); + + /* Verify onSuccess callback gets called */ + waiter.waitFor(); + assertThat(waiter.success, is(true)); + } finally { + if(ably != null) + ably.close(); + } + } + + /** + *

+ * Validate publish will succeed without triggering an attach when connected + * if not already attached + *

+ *

+ * Spec: RTL6c1 + *

+ * + */ + @Test + public void transient_publish_connected() throws AblyException { + AblyRealtime pubAbly = null, subAbly = null; + String channelName = "transient_publish_connected_" + testParams.name; + try { + ClientOptions opts = createOptions(testVars.keys[0].keyStr); + subAbly = new AblyRealtime(opts); + + /* wait until connected */ + new ConnectionWaiter(subAbly.connection).waitFor(ConnectionState.connected); + assertEquals("Verify connected state reached", subAbly.connection.state, ConnectionState.connected); + + /* create a channel and subscribe */ + final Channel subChannel = subAbly.channels.get(channelName); + Helpers.MessageWaiter messageWaiter = new Helpers.MessageWaiter(subChannel); + new ChannelWaiter(subChannel).waitFor(ChannelState.attached); + + pubAbly = new AblyRealtime(opts); + new ConnectionWaiter(pubAbly.connection).waitFor(ConnectionState.connected); + Helpers.CompletionWaiter completionWaiter = new Helpers.CompletionWaiter(); + final Channel pubChannel = pubAbly.channels.get(channelName); + pubChannel.publish("Lorem", "Ipsum!", completionWaiter); + assertEquals("Verify channel remains in initialized state", pubChannel.state, ChannelState.initialized); + + ErrorInfo errorInfo = completionWaiter.waitFor(); + assertEquals("Verify channel remains in initialized state", pubChannel.state, ChannelState.initialized); + + messageWaiter.waitFor(1); + assertEquals("Verify expected message received", messageWaiter.receivedMessages.get(0).name, "Lorem"); + } finally { + if(pubAbly != null) { + pubAbly.close(); + } + if(subAbly != null) { + subAbly.close(); + } + } + } + + /** + *

+ * Validate publish will succeed without triggering an attach when connecting + * if not already attached + *

+ *

+ * Spec: RTL6c2 + *

+ * + */ + @Test + public void transient_publish_connecting() throws AblyException { + AblyRealtime pubAbly = null, subAbly = null; + String channelName = "transient_publish_connecting_" + testParams.name; + try { + ClientOptions opts = createOptions(testVars.keys[0].keyStr); + subAbly = new AblyRealtime(opts); + + /* wait until connected */ + new ConnectionWaiter(subAbly.connection).waitFor(ConnectionState.connected); + assertEquals("Verify connected state reached", subAbly.connection.state, ConnectionState.connected); + + /* create a channel and subscribe */ + final Channel subChannel = subAbly.channels.get(channelName); + Helpers.ChannelWaiter channelWaiter = new Helpers.ChannelWaiter(subChannel); + Helpers.MessageWaiter messageWaiter = new Helpers.MessageWaiter(subChannel); + channelWaiter.waitFor(ChannelState.attached); + + pubAbly = new AblyRealtime(opts); + final Channel pubChannel = pubAbly.channels.get(channelName); + Helpers.CompletionWaiter completionWaiter = new Helpers.CompletionWaiter(); + pubChannel.publish("Lorem", "Ipsum!", completionWaiter); + assertEquals("Verify channel remains in initialized state", pubChannel.state, ChannelState.initialized); + + ErrorInfo errorInfo = completionWaiter.waitFor(); + assertEquals("Verify channel remains in initialized state", pubChannel.state, ChannelState.initialized); + + messageWaiter.waitFor(1); + assertEquals("Verify expected message received", messageWaiter.receivedMessages.get(0).name, "Lorem"); + } finally { + if(pubAbly != null) { + pubAbly.close(); + } + if(subAbly != null) { + subAbly.close(); + } + } + } + + /** + *

+ * Validate publish will fail when connection is failed + *

+ *

+ * Spec: RTL6c4 + *

+ * + */ + @Test + public void transient_publish_connection_failed() { + AblyRealtime pubAbly = null; + String channelName = "transient_publish_connection_failed_" + testParams.name; + try { + ClientOptions opts = createOptions("not:a.key"); + pubAbly = new AblyRealtime(opts); + new ConnectionWaiter(pubAbly.connection).waitFor(ConnectionState.failed); + assertEquals("Verify failed state reached", pubAbly.connection.state, ConnectionState.failed); + + final Channel pubChannel = pubAbly.channels.get(channelName); + Helpers.CompletionWaiter completionWaiter = new Helpers.CompletionWaiter(); + try { + pubChannel.publish("Lorem", "Ipsum!", completionWaiter); + fail("failed to raise expected exception"); + } catch(AblyException e) { + } + } catch(AblyException e) { + fail("unexpected exception"); + } finally { + if(pubAbly != null) { + pubAbly.close(); + } + } + } + + /** + *

+ * Validate publish will fail when channel is failed + *

+ *

+ * Spec: RTL6c4 + *

+ * + */ + @Test + public void transient_publish_channel_failed() { + AblyRealtime pubAbly = null; + String channelName = "transient_publish_channel_failed_" + testParams.name; + try { + ClientOptions opts = createOptions(testVars.keys[1].keyStr); + pubAbly = new AblyRealtime(opts); + new ConnectionWaiter(pubAbly.connection).waitFor(ConnectionState.connected); + assertEquals("Verify connected state reached", pubAbly.connection.state, ConnectionState.connected); + + final Channel pubChannel = pubAbly.channels.get(channelName); + Helpers.ChannelWaiter channelWaiter = new Helpers.ChannelWaiter(pubChannel); + pubChannel.attach(); + channelWaiter.waitFor(ChannelState.failed); + + Helpers.CompletionWaiter completionWaiter = new Helpers.CompletionWaiter(); + try { + pubChannel.publish("Lorem", "Ipsum!", completionWaiter); + fail("failed to raise expected exception"); + } catch(AblyException e) { + assertEquals(pubChannel.state, ChannelState.failed); + } + } catch(AblyException e) { + fail("unexpected exception"); + } finally { + if(pubAbly != null) { + pubAbly.close(); + } + } + } + + /** + *

+ * Validate subscribe will result in an error, when the channel moves + * to the FAILED state before the operation succeeds + *

+ *

+ * Spec: RTL7c + *

+ * + * @throws AblyException + */ + @Test + public void attach_implicit_subscribe_fail() throws AblyException { + AblyRealtime ably = null; + try { + ClientOptions opts = createOptions(testVars.keys[1].keyStr); + ably = new AblyRealtime(opts); + + /* wait until connected */ + new ConnectionWaiter(ably.connection).waitFor(ConnectionState.connected); + assertEquals("Verify connected state reached", ably.connection.state, ConnectionState.connected); + + /* create a channel and subscribe */ + final Channel channel = ably.channels.get("subscribe_fail"); + channel.subscribe(null); + assertEquals("Verify attaching state reached", channel.state, ChannelState.attaching); + + ErrorInfo fail = new ChannelWaiter(channel).waitFor(ChannelState.failed); + assertEquals("Verify failed state reached", channel.state, ChannelState.failed); + assertEquals("Verify reason code gives correct failure reason", fail.statusCode, 401); + } finally { + if(ably != null) + ably.close(); + } + } + + @Test + public void ensure_detach_with_error_does_not_move_to_failed() { + AblyRealtime ably = null; + try { + ClientOptions opts = createOptions(testVars.keys[0].keyStr); + ably = new AblyRealtime(opts); + + /* wait until connected */ + (new ConnectionWaiter(ably.connection)).waitFor(ConnectionState.connected); + assertEquals("Verify connected state reached", ably.connection.state, ConnectionState.connected); + + /* create a channel and attach */ + final Channel channel = ably.channels.get("test"); + channel.attach(); + (new ChannelWaiter(channel)).waitFor(ChannelState.attached); + assertEquals("Verify attached state reached", channel.state, ChannelState.attached); + + ProtocolMessage protoMessage = new ProtocolMessage(ProtocolMessage.Action.detach, "test"); + protoMessage.error = new ErrorInfo("test error", 123); + + ConnectionManager connectionManager = ably.connection.connectionManager; + connectionManager.onMessage(null, protoMessage); + + /* Because of (RTL13) channel should now be in either attaching or attached state */ + assertNotEquals("channel state shouldn't be failed", channel.state, ChannelState.failed); + } catch (AblyException e) { + e.printStackTrace(); + fail("init0: Unexpected exception instantiating library"); + } finally { + if(ably != null) + ably.close(); + } + } + + @Test + public void channel_state_on_connection_suspended() { + AblyRealtime ably = null; + try { + ClientOptions opts = createOptions(testVars.keys[0].keyStr); + + ably = new AblyRealtime(opts); + + /* wait until connected */ + (new ConnectionWaiter(ably.connection)).waitFor(ConnectionState.connected); + assertEquals("Verify connected state reached", ably.connection.state, ConnectionState.connected); + + /* create a channel and attach */ + final String channelName = "test_state_channel"; + final Channel channel = ably.channels.get(channelName); + channel.attach(); + + ChannelWaiter channelWaiter = new ChannelWaiter(channel); + channelWaiter.waitFor(ChannelState.attached); + assertEquals("Verify attached state reached", channel.state, ChannelState.attached); + + /* switch to suspended state */ + ably.connection.connectionManager.requestState(ConnectionState.suspended); + + channelWaiter.waitFor(ChannelState.suspended); + assertEquals("Verify suspended state reached", channel.state, ChannelState.suspended); + + /* switch to connected state */ + ably.connection.connectionManager.requestState(ConnectionState.connected); + + channelWaiter.waitFor(ChannelState.attached); + assertEquals("Verify attached state reached", channel.state, ChannelState.attached); + + /* switch to failed state */ + ably.connection.connectionManager.requestState(ConnectionState.failed); + + channelWaiter.waitFor(ChannelState.failed); + assertEquals("Verify failed state reached", channel.state, ChannelState.failed); + + } catch (AblyException e) { + e.printStackTrace(); + fail("Unexpected exception"); + } finally { + if (ably != null) + ably.close(); + } + } + + /* + * Establish connection, attach channel, simulate sending attached and detached messages + * from the server, test correct behaviour + * + * Tests RTL12, RTL13a + */ + @Test + public void channel_server_initiated_attached_detached() throws AblyException { + AblyRealtime ably = null; + long oldRealtimeTimeout = Defaults.realtimeRequestTimeout; + final String channelName = "channel_server_initiated_attach_detach"; + + try { + ClientOptions opts = createOptions(testVars.keys[0].keyStr); + + /* Make test faster */ + Defaults.realtimeRequestTimeout = 1000; + opts.channelRetryTimeout = 1000; + + ably = new AblyRealtime(opts); + + Channel channel = ably.channels.get(channelName); + ChannelWaiter channelWaiter = new ChannelWaiter(channel); + + channel.attach(); + channelWaiter.waitFor(ChannelState.attached); + + final int[] updateEventsEmitted = new int[]{0}; + final boolean[] resumedFlag = new boolean[]{true}; + channel.on(ChannelEvent.update, new ChannelStateListener() { + @Override + public void onChannelStateChanged(ChannelStateChange stateChange) { + updateEventsEmitted[0]++; + resumedFlag[0] = stateChange.resumed; + } + }); + + /* Inject attached message as if received from the server */ + ProtocolMessage attachedMessage = new ProtocolMessage() {{ + action = Action.attached; + channel = channelName; + flags |= Flag.resumed.getMask(); + }}; + ably.connection.connectionManager.onMessage(null, attachedMessage); + + /* Inject detached message as if from the server */ + ProtocolMessage detachedMessage = new ProtocolMessage() {{ + action = Action.detached; + channel = channelName; + }}; + ably.connection.connectionManager.onMessage(null, detachedMessage); + + /* Channel should transition to attaching, then to attached */ + channelWaiter.waitFor(ChannelState.attaching); + channelWaiter.waitFor(ChannelState.attached); + + /* Verify received UPDATE message on channel */ + assertEquals("Verify exactly one UPDATE event was emitted on the channel", updateEventsEmitted[0], 1); + assertTrue("Verify resumed flag set in UPDATE event", resumedFlag[0]); + } finally { + if (ably != null) + ably.close(); + Defaults.realtimeRequestTimeout = oldRealtimeTimeout; + } + } + + /* + * Establish connection, attach channel, disconnection and failed resume + * verify that subsequent attaches are performed, and give rise to update events + * + * Tests RTN15c3 + */ + @Test + public void channel_resume_lost_continuity() throws AblyException { + AblyRealtime ably = null; + final String attachedChannelName = "channel_resume_lost_continuity_attached"; + final String suspendedChannelName = "channel_resume_lost_continuity_suspended"; + + try { + ClientOptions opts = createOptions(testVars.keys[0].keyStr); + ably = new AblyRealtime(opts); + + /* prepare channels */ + Channel attachedChannel = ably.channels.get(attachedChannelName); + ChannelWaiter attachedChannelWaiter = new ChannelWaiter(attachedChannel); + attachedChannel.attach(); + attachedChannelWaiter.waitFor(ChannelState.attached); + + Channel suspendedChannel = ably.channels.get(suspendedChannelName); + suspendedChannel.state = ChannelState.suspended; + ChannelWaiter suspendedChannelWaiter = new ChannelWaiter(suspendedChannel); + + final boolean[] suspendedStateReached = new boolean[2]; + final boolean[] attachingStateReached = new boolean[2]; + final boolean[] attachedStateReached = new boolean[2]; + final boolean[] resumedFlag = new boolean[]{true, true}; + attachedChannel.on(new ChannelStateListener() { + @Override + public void onChannelStateChanged(ChannelStateChange stateChange) { + switch(stateChange.current) { + case suspended: + suspendedStateReached[0] = true; + break; + case attaching: + attachingStateReached[0] = true; + break; + case attached: + attachedStateReached[0] = true; + resumedFlag[0] = stateChange.resumed; + break; + default: + break; + } + } + }); + suspendedChannel.on(new ChannelStateListener() { + @Override + public void onChannelStateChanged(ChannelStateChange stateChange) { + switch(stateChange.current) { + case attaching: + attachingStateReached[1] = true; + break; + case attached: + attachedStateReached[1] = true; + resumedFlag[1] = stateChange.resumed; + break; + default: + break; + } + } + }); + + /* disconnect, and sabotage the resume */ + String originalConnectionId = ably.connection.id; + ably.connection.key = "_____!ably___test_fake-key____"; + ably.connection.id = "ably___tes"; + ConnectionWaiter connectionWaiter = new ConnectionWaiter(ably.connection); + + /* suppress automatic retries by the connection manager */ + try { + Method method = ably.connection.connectionManager.getClass().getDeclaredMethod("disconnectAndSuppressRetries"); + method.setAccessible(true); + method.invoke(ably.connection.connectionManager); + } catch (NoSuchMethodException|IllegalAccessException|InvocationTargetException e) { + fail("Unexpected exception in suppressing retries"); + } + + connectionWaiter.waitFor(ConnectionState.disconnected); + assertEquals("Verify disconnected state is reached", ConnectionState.disconnected, ably.connection.state); + + /* wait */ + try { Thread.sleep(2000L); } catch(InterruptedException e) {} + + /* wait for connection to be reestablished */ + System.out.println("channel_resume_lost_continuity: initiating reconnection (resume)"); + ably.connection.connect(); + connectionWaiter.waitFor(ConnectionState.connected); + + /* verify a new connection was assigned */ + assertNotEquals("A new connection was created", originalConnectionId, ably.connection.id); + + /* previously suspended channel should transition to attaching, then to attached */ + suspendedChannelWaiter.waitFor(ChannelState.attached); + + /* previously attached channel should remain attached */ + attachedChannelWaiter.waitFor(ChannelState.attached); + + /* + * Verify each channel undergoes relevant events: + * - previously attached channel does attaching, attached, without visiting suspended; + * - previously suspended channel does attaching, attached + */ + assertEquals("Verify channel was not suspended", suspendedStateReached[0], false); + assertEquals("Verify channel was attaching", attachingStateReached[0], true); + assertEquals("Verify channel was attached", attachedStateReached[0], true); + assertFalse("Verify resumed flag set false in ATTACHED event", resumedFlag[0]); + + assertEquals("Verify channel was attaching", attachingStateReached[1], true); + assertEquals("Verify channel was attached", attachedStateReached[1], true); + assertFalse("Verify resumed flag set false in ATTACHED event", resumedFlag[1]); + } finally { + if (ably != null) + ably.close(); + } + } + + /* + * Initiate connection, block send on transport to simulate network packet loss, try to attach, wait for + * channel to eventually attach when send is re-enabled on transport. + * + * Then suspend connection, resume it and immediately block sending packets failing channel + * reattach. Verify that the channel goes back to suspended state on timeout with correct error code. + * + * Try to detach channel while blocking send, channel should go back to attached state through detaching + * + * Tests features RTL4c, RTL4f, RTL5f, RTL5e + */ + @Test + public void channel_attach_retry_failed() { + AblyRealtime ably = null; + String channelName = "channel_attach_retry_failed_" + testParams.name; + long oldRealtimeTimeout = Defaults.realtimeRequestTimeout; + try { + DebugOptions opts = new DebugOptions(testVars.keys[0].keyStr); + fillInOptions(opts); + opts.channelRetryTimeout = 1000; + + /* Mock transport to block send */ + final MockWebsocketFactory mockTransport = new MockWebsocketFactory(); + opts.transportFactory = mockTransport; + mockTransport.allowSend(); + + /* Reduce timeout for test to run faster */ + Defaults.realtimeRequestTimeout = 1000; + + ably = new AblyRealtime(opts); + ConnectionWaiter connectionWaiter = new ConnectionWaiter(ably.connection); + connectionWaiter.waitFor(ConnectionState.connected); + + /* Block send() and attach */ + mockTransport.blockSend(); + + Channel channel = ably.channels.get(channelName); + ChannelWaiter channelWaiter = new ChannelWaiter(channel); + channel.attach(); + + channelWaiter.waitFor(ChannelState.attaching); + + /* Should get to suspended soon because send() is blocked */ + channelWaiter.waitFor(ChannelState.suspended); + + /* Re-enable send() and wait for channel to attach */ + mockTransport.allowSend(); + channelWaiter.waitFor(ChannelState.attached); + + /* Suspend connection: channel state should change to suspended */ + ably.connection.connectionManager.requestState(ConnectionState.suspended); + channelWaiter.waitFor(ChannelState.suspended); + + /* Reconnect and immediately block transport's send(). This should fail channel reattach */ + ably.connection.once(ConnectionState.connected, new ConnectionStateListener() { + @Override + public void onConnectionStateChanged(ConnectionStateChange state) { + mockTransport.blockSend(); + } + }); + ably.connection.connectionManager.requestState(ConnectionState.connected); + + /* Channel should move to attaching state */ + channelWaiter.waitFor(ChannelState.attaching); + /* + * Then within realtimeRequestTimeout interval it should get back to suspended + */ + channelWaiter.waitFor(ChannelState.suspended); + + /* In channelRetryTimeout we should get back to attaching state again */ + channelWaiter.waitFor(ChannelState.attaching); + /* And then suspended in another 1 second */ + channelWaiter.waitFor(ChannelState.suspended); + + /* Enable message sending again */ + mockTransport.allowSend(); + + /* And wait for attached state of the channel */ + channelWaiter.waitFor(ChannelState.attached); + + final ErrorInfo[] errorDetaching = new ErrorInfo[] {null}; + + /* Block and detach */ + mockTransport.blockSend(); + channel.detach(new CompletionListener() { + @Override + public void onSuccess() { + fail("Detach succeeded"); + } + + @Override + public void onError(ErrorInfo reason) { + synchronized (errorDetaching) { + errorDetaching[0] = reason; + errorDetaching.notify(); + } + } + }); + + /* Should get to detaching first */ + channelWaiter.waitFor(ChannelState.detaching); + /* And then back to attached on timeout */ + channelWaiter.waitFor(ChannelState.attached); + try { + synchronized (errorDetaching) { + if (errorDetaching[0] != null) + errorDetaching.wait(1000); + } + } catch (InterruptedException e) {} + + assertNotNull("Verify detach operation failed", errorDetaching[0]); + + } catch(AblyException e) { + e.printStackTrace(); + fail("Unexpected exception"); + } finally { + if (ably != null) + ably.close(); + /* Restore default values to run other tests */ + Defaults.realtimeRequestTimeout = oldRealtimeTimeout; + } + } + + /* + * Initiate connection, and attach to a channel. Simulate a server-initiated detach of the channel + * and verify that the client attempts to reattach. Block the transport to prevent that from + * succeeding. Verify that the channel enters the suspended state with the appropriate error. + * + * Tests features RTL13b + */ + @Test + public void channel_reattach_failed_timeout() { + AblyRealtime ably = null; + final String channelName = "channel_reattach_failed_timeout_" + testParams.name; + long oldRealtimeTimeout = Defaults.realtimeRequestTimeout; + try { + DebugOptions opts = new DebugOptions(testVars.keys[0].keyStr); + fillInOptions(opts); + opts.channelRetryTimeout = 1000; + + /* Mock transport to block send */ + final MockWebsocketFactory mockTransport = new MockWebsocketFactory(); + opts.transportFactory = mockTransport; + mockTransport.allowSend(); + + /* Reduce timeout for test to run faster */ + Defaults.realtimeRequestTimeout = 1000; + + ably = new AblyRealtime(opts); + ConnectionWaiter connectionWaiter = new ConnectionWaiter(ably.connection); + connectionWaiter.waitFor(ConnectionState.connected); + + Channel channel = ably.channels.get(channelName); + ChannelWaiter channelWaiter = new ChannelWaiter(channel); + channel.attach(); + + channelWaiter.waitFor(ChannelState.attached); + + /* Block send() */ + mockTransport.blockSend(); + + /* Inject detached message as if from the server */ + ProtocolMessage detachedMessage = new ProtocolMessage() {{ + action = Action.detached; + channel = channelName; + }}; + ably.connection.connectionManager.onMessage(null, detachedMessage); + + /* Should get to suspended soon because send() is blocked */ + ErrorInfo suspendReason = channelWaiter.waitFor(ChannelState.suspended); + assertEquals("Verify the suspended event contains the detach reason", 91200, suspendReason.code); + + /* Unblock send(), and expect a transition to attached */ + mockTransport.allowSend(); + channelWaiter.waitFor(ChannelState.attached); + + } catch(AblyException e) { + e.printStackTrace(); + fail("channel_reattach_failed_timeout: unexpected exception"); + } finally { + if (ably != null) { + ably.close(); + } + /* Restore default values to run other tests */ + Defaults.realtimeRequestTimeout = oldRealtimeTimeout; + } + } + + /* + * Initiate connection, and attach to a channel. Simulate a server-initiated detach of the channel + * and verify that the client attempts to reattach. Block the transport and respond instead with + * an attach error. Verify that the channel enters the suspended state with the appropriate error. + * + * Tests features RTL13b + */ + @Test + public void channel_reattach_failed_error() { + AblyRealtime ably = null; + final String channelName = "channel_reattach_failed_error_" + testParams.name; + final int errorCode = 12345; + long oldRealtimeTimeout = Defaults.realtimeRequestTimeout; + try { + DebugOptions opts = new DebugOptions(testVars.keys[0].keyStr); + fillInOptions(opts); + opts.channelRetryTimeout = 1000; + + /* Mock transport to block send */ + final MockWebsocketFactory mockTransport = new MockWebsocketFactory(); + opts.transportFactory = mockTransport; + mockTransport.allowSend(); + + /* Reduce timeout for test to run faster */ + Defaults.realtimeRequestTimeout = 5000; + + ably = new AblyRealtime(opts); + ConnectionWaiter connectionWaiter = new ConnectionWaiter(ably.connection); + connectionWaiter.waitFor(ConnectionState.connected); + + Channel channel = ably.channels.get(channelName); + ChannelWaiter channelWaiter = new ChannelWaiter(channel); + channel.attach(); + + channelWaiter.waitFor(ChannelState.attached); + + /* Block send() */ + mockTransport.blockSend(); + + /* Inject detached message as if from the server */ + ProtocolMessage detachedMessage = new ProtocolMessage() {{ + action = Action.detached; + channel = channelName; + error = new ErrorInfo("Test error", errorCode); + }}; + ably.connection.connectionManager.onMessage(null, detachedMessage); + + /* wait for the client reattempt attachment */ + channelWaiter.waitFor(ChannelState.attaching); + + /* Inject detached+error message as if from the server */ + ProtocolMessage errorMessage = new ProtocolMessage() {{ + action = Action.detached; + channel = channelName; + error = new ErrorInfo("Test error", errorCode); + }}; + ably.connection.connectionManager.onMessage(null, errorMessage); + + /* Should get to suspended soon because there was an error response to the attach attempt */ + ErrorInfo suspendReason = channelWaiter.waitFor(ChannelState.suspended); + assertEquals("Verify the suspended event contains the detach reason", errorCode, suspendReason.code); + + /* Unblock send(), and expect a transition to attached */ + mockTransport.allowSend(); + channelWaiter.waitFor(ChannelState.attached); + + } catch(AblyException e) { + e.printStackTrace(); + fail("Unexpected exception"); + } finally { + if (ably != null) + ably.close(); + /* Restore default values to run other tests */ + Defaults.realtimeRequestTimeout = oldRealtimeTimeout; + } + } + + /** + * Initiate an attach when not connected; verify that the given listener is called + * with the attach error + */ + @Test + public void attach_exception_listener_called() { + try { + final String channelName = "attach_exception_listener_called_" + testParams.name; + + /* init Ably */ + ClientOptions opts = createOptions(testVars.keys[0].keyStr); + AblyRealtime ably = new AblyRealtime(opts); + + /* wait until connected */ + (new ConnectionWaiter(ably.connection)).waitFor(ConnectionState.connected); + assertEquals("Verify connected state reached", ably.connection.state, ConnectionState.connected); + + /* create a channel; put into failed state */ + ably.connection.connectionManager.requestState(new ConnectionManager.StateIndication(ConnectionState.failed, new ErrorInfo("Test error", 400, 12345))); + (new ConnectionWaiter(ably.connection)).waitFor(ConnectionState.failed); + assertEquals("Verify failed state reached", ably.connection.state, ConnectionState.failed); + + /* attempt to attach */ + Channel channel = ably.channels.get(channelName); + final ErrorInfo[] listenerError = new ErrorInfo[1]; + synchronized(listenerError) { + channel.attach(new CompletionListener() { + @Override + public void onSuccess() { + synchronized (listenerError) { + listenerError.notify(); + } + fail("Unexpected attach success"); + } + + @Override + public void onError(ErrorInfo reason) { + synchronized (listenerError) { + listenerError[0] = reason; + listenerError.notify(); + } + } + }); + + /* wait until the listener is called */ + while(listenerError[0] == null) { + try { listenerError.wait(); } catch(InterruptedException e) {} + } + } + + /* verify that the listener was called with an error */ + assertNotNull("Verify the error callback was called", listenerError[0]); + assertEquals("Verify the given error is indicated", listenerError[0].code, 12345); + + /* tidy */ + ably.close(); + } catch(AblyException e) { + fail(e.getMessage()); + } + + } + + @Test + public void no_messages_when_channel_state_not_attached() { + + AblyRealtime senderReceiver = null; + final String testMessage1 = "{ foo: \"bar\", count: 1, status: \"active\" }"; + final String testMessage2 = "{ foo: \"bar\", count: 2, status: \"active\" }"; + + String testName = "no_messages_when_channel_state_not_attached"; + try { + + DebugOptions common_opts = createOptions(testVars.keys[0].keyStr); + common_opts.protocolListener = new DetachingProtocolListener(); + senderReceiver = new AblyRealtime(common_opts); + + Channel sender_channel = senderReceiver.channels.get(testName); + ((DetachingProtocolListener)common_opts.protocolListener).theChannel = sender_channel; + + + sender_channel.attach(); + (new ChannelWaiter(sender_channel)).waitFor(ChannelState.attached); + + Helpers.MessageWaiter messageWaiter_1 = new Helpers.MessageWaiter(sender_channel); + + sender_channel.publish("1", testMessage1); + + messageWaiter_1.waitFor(1); + assertEquals("Verify rewound message", testMessage1, messageWaiter_1.receivedMessages.get(0).data); + messageWaiter_1.reset(); + + sender_channel.publish("2", testMessage2); + messageWaiter_1.waitFor(1, 7000); + assertEquals("Verify no message received on attach_rewind", 0, messageWaiter_1.receivedMessages.size()); + + } catch(Exception e) { + fail(testName + ": Unexpected exception " + e.getMessage()); + e.printStackTrace(); + } finally { + if(senderReceiver != null) + senderReceiver.close(); + } + } + + class DetachingProtocolListener implements DebugOptions.RawProtocolListener { + + public Channel theChannel; + boolean messageReceived; + + public DetachingProtocolListener() { + messageReceived = false; + } + + @Override + public void onRawConnect(String url) {} + @Override + public void onRawConnectRequested(String url) {} + @Override + public void onRawMessageSend(ProtocolMessage message) { + } + @Override + public void onRawMessageRecv(ProtocolMessage message) { + if(message.action == ProtocolMessage.Action.message) { + if (!messageReceived) { + messageReceived = true; + return; + } + + theChannel.state = ChannelState.attaching; + } + } + }; } diff --git a/lib/src/test/java/io/ably/lib/test/realtime/RealtimeConnectFailTest.java b/lib/src/test/java/io/ably/lib/test/realtime/RealtimeConnectFailTest.java index 28e11afc7..8709da80b 100644 --- a/lib/src/test/java/io/ably/lib/test/realtime/RealtimeConnectFailTest.java +++ b/lib/src/test/java/io/ably/lib/test/realtime/RealtimeConnectFailTest.java @@ -36,513 +36,513 @@ public class RealtimeConnectFailTest extends ParameterizedTest { - @Rule - public Timeout testTimeout = Timeout.seconds(300); - - /** - * Verify that the connection enters the failed state, after attempting - * to connect with invalid app - * Spec: RTN4f - */ - @Test - public void connect_fail_notfound_error() throws AblyException { - AblyRealtime ably = null; - try { - ClientOptions opts = createOptions("not_an_app.invalid_key_id:invalid_key_value"); - ably = new AblyRealtime(opts); - ConnectionWaiter connectionWaiter = new ConnectionWaiter(ably.connection); - - ErrorInfo fail = connectionWaiter.waitFor(ConnectionState.failed); - assertEquals("Verify failed state is reached", ConnectionState.failed, ably.connection.state); - // assertEquals("Verify correct error code is given", 404, fail.statusCode); - } finally { - ably.close(); - } - } - - /** - * Verify that the connection enters the failed state, after attempting - * to connect with invalid key - */ - @Test - public void connect_fail_authorized_error() throws AblyException { - AblyRealtime ably = null; - try { - ClientOptions opts = createOptions(testVars.appId + ".invalid_key_id:invalid_key_value"); - ably = new AblyRealtime(opts); - ConnectionWaiter connectionWaiter = new ConnectionWaiter(ably.connection); - - ErrorInfo fail = connectionWaiter.waitFor(ConnectionState.failed); - assertEquals("Verify failed state is reached", ConnectionState.failed, ably.connection.state); - assertEquals("Verify correct error code is given", 401, fail.statusCode); - } finally { - ably.close(); - } - } - - /** - * Verify that the connection enters the disconnected state, after attempting - * to connect to a non-existent ws host - */ - @Test - public void connect_fail_disconnected() throws AblyException { - ClientOptions opts = createOptions(testVars.keys[0].keyStr); - opts.realtimeHost = "non.existent.host"; - opts.environment = null; - AblyRealtime ably = new AblyRealtime(opts); - ConnectionWaiter connectionWaiter = new ConnectionWaiter(ably.connection); - - connectionWaiter.waitFor(ConnectionState.disconnected); - assertEquals("Verify disconnected state is reached", ConnectionState.disconnected, ably.connection.state); - ably.close(); - connectionWaiter.waitFor(ConnectionState.closed); - assertEquals("Verify closed state is reached", ConnectionState.closed, ably.connection.state); - } - - /** - * Verify that the connection enters the suspended state, after multiple attempts - * to connect to a non-existent ws host - */ - @Test - public void connect_fail_suspended() { - try { - ClientOptions opts = createOptions(testVars.keys[0].keyStr); - opts.realtimeHost = "non.existent.host"; - opts.environment = null; - AblyRealtime ably = new AblyRealtime(opts); - ConnectionWaiter connectionWaiter = new ConnectionWaiter(ably.connection); - - connectionWaiter.waitFor(ConnectionState.suspended); - /* Wait 1s to force bug where it changes to disconnected right after - * changing to suspended. Without this it fails only intermittently - * when that bug is present. */ - try { - Thread.sleep(1000); - } catch (InterruptedException e) {} - assertEquals("Verify suspended state is reached", ConnectionState.suspended, ably.connection.state); - assertTrue("Verify multiple connect attempts", connectionWaiter.getCount(ConnectionState.connecting) > 1); - assertTrue("Verify multiple connect attempts", connectionWaiter.getCount(ConnectionState.disconnected) > 1); - ably.close(); - connectionWaiter.waitFor(ConnectionState.closed); - assertEquals("Verify closed state is reached", ConnectionState.closed, ably.connection.state); - } catch (AblyException e) { - e.printStackTrace(); - fail("init0: Unexpected exception instantiating library"); - } - } - - /** - * Verify that the connection in the disconnected state (after attempts to - * connect to a non-existent ws host) allows an immediate explicit connect - * attempt, instead of ignoring the explicit connect and waiting till the - * next scheduled retry. - */ - @Test - public void connect_while_disconnected() { - try { - ClientOptions opts = createOptions(testVars.keys[0].keyStr); - opts.realtimeHost = "non.existent.host"; - opts.environment = null; - AblyRealtime ably = new AblyRealtime(opts); - ConnectionWaiter connectionWaiter = new ConnectionWaiter(ably.connection); - - connectionWaiter.waitFor(ConnectionState.disconnected); - assertEquals("Verify disconnected state is reached", ConnectionState.disconnected, ably.connection.state); - - long before = System.currentTimeMillis(); - ably.connection.connect(); - connectionWaiter.waitFor(ConnectionState.connecting); - assertTrue("Verify explicit connect is actioned immediately", System.currentTimeMillis() - before < 1000L); - - ably.close(); - connectionWaiter.waitFor(ConnectionState.closed); - assertEquals("Verify closed state is reached", ConnectionState.closed, ably.connection.state); - } catch (AblyException e) { - e.printStackTrace(); - fail("init0: Unexpected exception instantiating library"); - } - } - - /** - * Verify that the connection enters the disconnected state, after a token - * used for successful connection expires - */ - @Test - public void connect_token_expire_disconnected() { - try { - final ClientOptions optsForToken = createOptions(testVars.keys[0].keyStr); - final AblyRest ablyForToken = new AblyRest(optsForToken); - Auth.AuthOptions restAuthOptions = new Auth.AuthOptions() {{ - key = optsForToken.key; - queryTime = true; - }}; - TokenDetails tokenDetails = ablyForToken.auth.requestToken(new TokenParams() {{ ttl = 8000L; }}, restAuthOptions); - assertNotNull("Expected token value", tokenDetails.token); - - /* implement callback, using Ably instance with key */ - final class TokenGenerator implements TokenCallback { - @Override - public Object getTokenRequest(TokenParams params) throws AblyException { - ++cbCount; - return ablyForToken.auth.requestToken(params, null); - } - public int getCbCount() { return cbCount; } - private int cbCount = 0; - }; - - TokenGenerator authCallback = new TokenGenerator(); - - /* create Ably realtime instance without key */ - ClientOptions opts = createOptions(); - opts.tokenDetails = tokenDetails; - opts.authCallback = authCallback; - AblyRealtime ably = new AblyRealtime(opts); - - ably.connection.on(new ConnectionStateListener() { - @Override - public void onConnectionStateChanged(ConnectionStateChange state) { - System.out.println(state.current); - } - }); - - /* wait for connected state */ - ConnectionWaiter connectionWaiter = new ConnectionWaiter(ably.connection); - connectionWaiter.waitFor(ConnectionState.connected); - - /* wait for disconnected state (on token expiry), with timeout */ - connectionWaiter.waitFor(ConnectionState.disconnected, 1, 30000L); - - /* wait for connected state (on token renewal) */ - connectionWaiter.waitFor(ConnectionState.connected, 2, 30000L); - - /* verify that our token generator was called */ - assertEquals("Expected token generator to be called", 1, authCallback.getCbCount()); - - /* end */ - ably.close(); - connectionWaiter.waitFor(ConnectionState.closed); - assertEquals("Verify closed state is reached", ConnectionState.closed, ably.connection.state); - } catch (AblyException e) { - e.printStackTrace(); - fail("init0: Unexpected exception instantiating library"); - } - } - - /** - * Verify that the server issues reauth message 30 seconds before token expiration time, authCallback is - * called to obtain new token and in-place re-authorization takes place with connection staying in connected - * state. Also tests if UPDATE event is delivered on the connection - * - * Test for RTN4h, RTC8a1, RTN24 features - */ - @Test - public void connect_token_expire_inplace_reauth() { - try { - ClientOptions optsForToken = createOptions(testVars.keys[0].keyStr); - final AblyRest ablyForToken = new AblyRest(optsForToken); - /* Server will send reauth message 30 seconds before token expiration time i.e. in 4 seconds */ - TokenDetails tokenDetails = ablyForToken.auth.requestToken(new TokenParams() {{ ttl = 34000L; }}, null); - assertNotNull("Expected token value", tokenDetails.token); - - final boolean[] flags = new boolean[] { - false, /* authCallback is called */ - false, /* state other than connected is reached */ - false /* update event was delivered */ - }; - - /* create Ably realtime instance without key */ - ClientOptions opts = createOptions(); - opts.tokenDetails = tokenDetails; - opts.authCallback = new TokenCallback() { - /* implement callback, using Ably instance with key */ - @Override - public Object getTokenRequest(TokenParams params) throws AblyException { - synchronized (flags) { - flags[0] = true; - } - return ablyForToken.auth.requestToken(params, null); - } - }; - AblyRealtime ably = new AblyRealtime(opts); - - /* Test UPDATE event delivery */ - ably.connection.on(ConnectionEvent.update, new ConnectionStateListener() { - @Override - public void onConnectionStateChanged(ConnectionStateChange state) { - flags[2] = true; - } - }); - ably.connection.on(new ConnectionStateListener() { - @Override - public void onConnectionStateChanged(ConnectionStateChange state) { - if (state.previous == ConnectionState.connected && state.current != ConnectionState.connected) { - synchronized (flags) { - flags[1] = true; - flags.notify(); - } - } - } - }); - - synchronized (flags) { - try { - flags.wait(8000); - } catch (InterruptedException e) {} - } - - assertTrue("Verify token generation was called", flags[0]); - assertFalse("Verify connection didn't leave connected state", flags[1]); - assertTrue("Verify UPDATE event was delivered", flags[2]); - - } catch (AblyException e) { - e.printStackTrace(); - fail("init0: Unexpected exception instantiating library"); - } - } - - /** - * Verify that the connection fails when attempting to recover with a - * malformed connection id - */ - @Test - public void connect_invalid_recover_fail() { - AblyRealtime ably = null; - try { - ClientOptions opts = createOptions(testVars.keys[0].keyStr); - opts.recover = "not_a_valid_connection_id:99"; - ably = new AblyRealtime(opts); - ConnectionWaiter connectionWaiter = new ConnectionWaiter(ably.connection); - ErrorInfo fail = connectionWaiter.waitFor(ConnectionState.failed); - assertEquals("Verify failed state is reached", ConnectionState.failed, ably.connection.state); - assertEquals("Verify correct error code is given", 80018, fail.code); - } catch (AblyException e) { - e.printStackTrace(); - fail("init0: Unexpected exception instantiating library"); - } finally { - ably.close(); - } - } - - /** - * Verify that the connection creates a new connection but reports - * a recovery error, when attempting to recover with an unknown - * connection id - */ - @Test - public void connect_unknown_recover_fail() { - AblyRealtime ably = null; - try { - ClientOptions opts = createOptions(testVars.keys[0].keyStr); - String recoverConnectionId = "0123456789abcdef-99"; - opts.recover = recoverConnectionId + ":0"; - ably = new AblyRealtime(opts); - ConnectionWaiter connectionWaiter = new ConnectionWaiter(ably.connection); - ErrorInfo connectedError = connectionWaiter.waitFor(ConnectionState.connected); - assertEquals("Verify connected state is reached", ConnectionState.connected, ably.connection.state); - assertNotNull("Verify error is returned", connectedError); - assertEquals("Verify correct error code is given", 80008, connectedError.code); - assertFalse("Verify new connection id is assigned", recoverConnectionId.equals(ably.connection.key)); - } catch (AblyException e) { - e.printStackTrace(); - fail("init0: Unexpected exception instantiating library"); - } finally { - ably.close(); - } - } - - /** - * Test that connection manager correctly fails messages set stored in message queue - * Spec: RTN7c - */ - - @Test - public void connect_test_queued_messages_on_failure() { - AblyRealtime ably = null; - try { - ClientOptions opts = createOptions(testVars.keys[0].keyStr); - ably = new AblyRealtime(opts); - final int[] numberOfErrors = new int[]{0}; - - // assume we are in connecting state now - ably.connection.connectionManager.send(new ProtocolMessage(), true, new CompletionListener() { - @Override - public void onSuccess() { - fail("Unexpected success sending message"); - } - - @Override - public void onError(ErrorInfo reason) { - numberOfErrors[0]++; - } - }); - - // transition to suspended state failing messages - ably.connection.connectionManager.requestState(ConnectionState.suspended); - try { - Thread.sleep(500); - } catch (InterruptedException e) { - } - // transition once more to ensure onError() won't be called twice - ably.connection.connectionManager.requestState(ConnectionState.closed); - try { - Thread.sleep(500); - } catch (InterruptedException e) { - } - - // onError() should be called only once - assertEquals("Verifying number of onError() calls", numberOfErrors[0], 1); - } catch (AblyException e) { - e.printStackTrace(); - fail("Unexpected exception"); - } finally { - if (ably != null) - ably.close(); - } - } - - /** - * Allow token to expire and try to authorize with already expired token after that. Test that the connection state - * is changed in the correct way and without duplicates: - * - * connecting -> connected -> disconnected -> connecting -> disconnected -> connecting -> disconnected - */ - @Test - public void connect_reauth_failure_state_flow_test() { - - try { - AblyRest ablyRest = null; - ClientOptions opts = createOptions(testVars.keys[0].keyStr); - - ablyRest = new AblyRest(opts); - final TokenDetails tokenDetails = ablyRest.auth.requestToken(new TokenParams() {{ ttl = 8000L; }}, null); - assertNotNull("Expected token value", tokenDetails.token); - - final ArrayList stateHistory = new ArrayList<>(); - - ClientOptions optsForRealtime = createOptions(); - optsForRealtime.authCallback = new TokenCallback() { - @Override - public Object getTokenRequest(TokenParams params) throws AblyException { - // return already expired token - return tokenDetails; - } - }; - optsForRealtime.tokenDetails = tokenDetails; - final AblyRealtime ablyRealtime = new AblyRealtime(optsForRealtime); - - ablyRealtime.connection.on(ConnectionState.connected, new ConnectionStateListener() { - @Override - public void onConnectionStateChanged(ConnectionStateChange state) { - /* To go quicker into a disconnected state we use a - * smaller value for maxIdleInterval - */ - try { - Field field = ablyRealtime.connection.connectionManager.getClass().getDeclaredField("maxIdleInterval"); - field.setAccessible(true); - field.setLong(ablyRealtime.connection.connectionManager, 5000L); - } catch (NoSuchFieldException|IllegalAccessException e) { - fail("Unexpected exception in checking connectionStateTtl"); - } - } - }); - - (new ConnectionWaiter(ablyRealtime.connection)).waitFor(ConnectionState.connected); - // TODO: improve by collecting and testing also auth attempts - final List correctHistory = Arrays.asList( - ConnectionState.disconnected, - ConnectionState.connecting, - ConnectionState.disconnected, - ConnectionState.connecting, - ConnectionState.disconnected - ); - final int maxDisconnections = 3; - ablyRealtime.connection.on(new ConnectionStateListener() { - int disconnections = 0; - @Override - public void onConnectionStateChanged(ConnectionStateChange state) { - synchronized (stateHistory) { - stateHistory.add(state.current); - if (state.current == ConnectionState.disconnected) { - disconnections++; - if (disconnections == maxDisconnections) { - assertTrue("Verifying state change history", stateHistory.equals(correctHistory)); - ablyRealtime.close(); - } - } - } - } - }); - - ConnectionWaiter connectionWaiter = new ConnectionWaiter(ablyRealtime.connection); - connectionWaiter.waitFor(ConnectionState.closed); - } catch (AblyException e) { - e.printStackTrace(); - fail("init0: Unexpected exception instantiating library"); - } - } - - /** - * Throw exception in authCallback repeatedly and check if connection goes into suspended state - */ - @Test - public void connect_auth_failure_and_suspend_test() { - AblyRealtime ablyRealtime = null; - AblyRest ablyRest = null; - int oldDisconnectTimeout = Defaults.TIMEOUT_DISCONNECT; - - try { - /* Make test faster */ - Defaults.TIMEOUT_DISCONNECT = 1000; - - final int[] numberOfAuthCalls = new int[] {0}; - final boolean[] reachedFinalState = new boolean[] {false}; - - ClientOptions opts = createOptions(testVars.keys[0].keyStr); - - ablyRest = new AblyRest(opts); - final TokenDetails tokenDetails = ablyRest.auth.requestToken(new TokenParams() {{ ttl = 5000L; }}, null); - assertNotNull("Expected token value", tokenDetails.token); - - ClientOptions optsForRealtime = createOptions(); - optsForRealtime.authCallback = new TokenCallback() { - @Override - public Object getTokenRequest(TokenParams params) throws AblyException { - if (numberOfAuthCalls[0]++ == 0) - return tokenDetails; - else - throw AblyException.fromErrorInfo(new ErrorInfo("Auth failure", 90000)); - } - }; - ablyRealtime = new AblyRealtime(optsForRealtime); - - ablyRealtime.connection.on(new ConnectionStateListener() { - @Override - public void onConnectionStateChanged(ConnectionStateChange state) { - System.out.println(String.format("New state: %s", state.current)); - synchronized (reachedFinalState) { - reachedFinalState[0] = state.current == ConnectionState.closed || - state.current == ConnectionState.suspended || - state.current == ConnectionState.failed; - reachedFinalState.notify(); - } - } - }); - - synchronized (reachedFinalState) { - while (!reachedFinalState[0]) { - try { reachedFinalState.wait(); } catch (InterruptedException e) {} - } - } - - assertEquals("Verify suspended state", ablyRealtime.connection.state, ConnectionState.suspended); - } catch (AblyException e) { - e.printStackTrace(); - fail("init0: Unexpected exception instantiating library"); - } finally { - Defaults.TIMEOUT_DISCONNECT = oldDisconnectTimeout; - if (ablyRealtime != null) - ablyRealtime.close(); - } - } + @Rule + public Timeout testTimeout = Timeout.seconds(300); + + /** + * Verify that the connection enters the failed state, after attempting + * to connect with invalid app + * Spec: RTN4f + */ + @Test + public void connect_fail_notfound_error() throws AblyException { + AblyRealtime ably = null; + try { + ClientOptions opts = createOptions("not_an_app.invalid_key_id:invalid_key_value"); + ably = new AblyRealtime(opts); + ConnectionWaiter connectionWaiter = new ConnectionWaiter(ably.connection); + + ErrorInfo fail = connectionWaiter.waitFor(ConnectionState.failed); + assertEquals("Verify failed state is reached", ConnectionState.failed, ably.connection.state); + // assertEquals("Verify correct error code is given", 404, fail.statusCode); + } finally { + ably.close(); + } + } + + /** + * Verify that the connection enters the failed state, after attempting + * to connect with invalid key + */ + @Test + public void connect_fail_authorized_error() throws AblyException { + AblyRealtime ably = null; + try { + ClientOptions opts = createOptions(testVars.appId + ".invalid_key_id:invalid_key_value"); + ably = new AblyRealtime(opts); + ConnectionWaiter connectionWaiter = new ConnectionWaiter(ably.connection); + + ErrorInfo fail = connectionWaiter.waitFor(ConnectionState.failed); + assertEquals("Verify failed state is reached", ConnectionState.failed, ably.connection.state); + assertEquals("Verify correct error code is given", 401, fail.statusCode); + } finally { + ably.close(); + } + } + + /** + * Verify that the connection enters the disconnected state, after attempting + * to connect to a non-existent ws host + */ + @Test + public void connect_fail_disconnected() throws AblyException { + ClientOptions opts = createOptions(testVars.keys[0].keyStr); + opts.realtimeHost = "non.existent.host"; + opts.environment = null; + AblyRealtime ably = new AblyRealtime(opts); + ConnectionWaiter connectionWaiter = new ConnectionWaiter(ably.connection); + + connectionWaiter.waitFor(ConnectionState.disconnected); + assertEquals("Verify disconnected state is reached", ConnectionState.disconnected, ably.connection.state); + ably.close(); + connectionWaiter.waitFor(ConnectionState.closed); + assertEquals("Verify closed state is reached", ConnectionState.closed, ably.connection.state); + } + + /** + * Verify that the connection enters the suspended state, after multiple attempts + * to connect to a non-existent ws host + */ + @Test + public void connect_fail_suspended() { + try { + ClientOptions opts = createOptions(testVars.keys[0].keyStr); + opts.realtimeHost = "non.existent.host"; + opts.environment = null; + AblyRealtime ably = new AblyRealtime(opts); + ConnectionWaiter connectionWaiter = new ConnectionWaiter(ably.connection); + + connectionWaiter.waitFor(ConnectionState.suspended); + /* Wait 1s to force bug where it changes to disconnected right after + * changing to suspended. Without this it fails only intermittently + * when that bug is present. */ + try { + Thread.sleep(1000); + } catch (InterruptedException e) {} + assertEquals("Verify suspended state is reached", ConnectionState.suspended, ably.connection.state); + assertTrue("Verify multiple connect attempts", connectionWaiter.getCount(ConnectionState.connecting) > 1); + assertTrue("Verify multiple connect attempts", connectionWaiter.getCount(ConnectionState.disconnected) > 1); + ably.close(); + connectionWaiter.waitFor(ConnectionState.closed); + assertEquals("Verify closed state is reached", ConnectionState.closed, ably.connection.state); + } catch (AblyException e) { + e.printStackTrace(); + fail("init0: Unexpected exception instantiating library"); + } + } + + /** + * Verify that the connection in the disconnected state (after attempts to + * connect to a non-existent ws host) allows an immediate explicit connect + * attempt, instead of ignoring the explicit connect and waiting till the + * next scheduled retry. + */ + @Test + public void connect_while_disconnected() { + try { + ClientOptions opts = createOptions(testVars.keys[0].keyStr); + opts.realtimeHost = "non.existent.host"; + opts.environment = null; + AblyRealtime ably = new AblyRealtime(opts); + ConnectionWaiter connectionWaiter = new ConnectionWaiter(ably.connection); + + connectionWaiter.waitFor(ConnectionState.disconnected); + assertEquals("Verify disconnected state is reached", ConnectionState.disconnected, ably.connection.state); + + long before = System.currentTimeMillis(); + ably.connection.connect(); + connectionWaiter.waitFor(ConnectionState.connecting); + assertTrue("Verify explicit connect is actioned immediately", System.currentTimeMillis() - before < 1000L); + + ably.close(); + connectionWaiter.waitFor(ConnectionState.closed); + assertEquals("Verify closed state is reached", ConnectionState.closed, ably.connection.state); + } catch (AblyException e) { + e.printStackTrace(); + fail("init0: Unexpected exception instantiating library"); + } + } + + /** + * Verify that the connection enters the disconnected state, after a token + * used for successful connection expires + */ + @Test + public void connect_token_expire_disconnected() { + try { + final ClientOptions optsForToken = createOptions(testVars.keys[0].keyStr); + final AblyRest ablyForToken = new AblyRest(optsForToken); + Auth.AuthOptions restAuthOptions = new Auth.AuthOptions() {{ + key = optsForToken.key; + queryTime = true; + }}; + TokenDetails tokenDetails = ablyForToken.auth.requestToken(new TokenParams() {{ ttl = 8000L; }}, restAuthOptions); + assertNotNull("Expected token value", tokenDetails.token); + + /* implement callback, using Ably instance with key */ + final class TokenGenerator implements TokenCallback { + @Override + public Object getTokenRequest(TokenParams params) throws AblyException { + ++cbCount; + return ablyForToken.auth.requestToken(params, null); + } + public int getCbCount() { return cbCount; } + private int cbCount = 0; + }; + + TokenGenerator authCallback = new TokenGenerator(); + + /* create Ably realtime instance without key */ + ClientOptions opts = createOptions(); + opts.tokenDetails = tokenDetails; + opts.authCallback = authCallback; + AblyRealtime ably = new AblyRealtime(opts); + + ably.connection.on(new ConnectionStateListener() { + @Override + public void onConnectionStateChanged(ConnectionStateChange state) { + System.out.println(state.current); + } + }); + + /* wait for connected state */ + ConnectionWaiter connectionWaiter = new ConnectionWaiter(ably.connection); + connectionWaiter.waitFor(ConnectionState.connected); + + /* wait for disconnected state (on token expiry), with timeout */ + connectionWaiter.waitFor(ConnectionState.disconnected, 1, 30000L); + + /* wait for connected state (on token renewal) */ + connectionWaiter.waitFor(ConnectionState.connected, 2, 30000L); + + /* verify that our token generator was called */ + assertEquals("Expected token generator to be called", 1, authCallback.getCbCount()); + + /* end */ + ably.close(); + connectionWaiter.waitFor(ConnectionState.closed); + assertEquals("Verify closed state is reached", ConnectionState.closed, ably.connection.state); + } catch (AblyException e) { + e.printStackTrace(); + fail("init0: Unexpected exception instantiating library"); + } + } + + /** + * Verify that the server issues reauth message 30 seconds before token expiration time, authCallback is + * called to obtain new token and in-place re-authorization takes place with connection staying in connected + * state. Also tests if UPDATE event is delivered on the connection + * + * Test for RTN4h, RTC8a1, RTN24 features + */ + @Test + public void connect_token_expire_inplace_reauth() { + try { + ClientOptions optsForToken = createOptions(testVars.keys[0].keyStr); + final AblyRest ablyForToken = new AblyRest(optsForToken); + /* Server will send reauth message 30 seconds before token expiration time i.e. in 4 seconds */ + TokenDetails tokenDetails = ablyForToken.auth.requestToken(new TokenParams() {{ ttl = 34000L; }}, null); + assertNotNull("Expected token value", tokenDetails.token); + + final boolean[] flags = new boolean[] { + false, /* authCallback is called */ + false, /* state other than connected is reached */ + false /* update event was delivered */ + }; + + /* create Ably realtime instance without key */ + ClientOptions opts = createOptions(); + opts.tokenDetails = tokenDetails; + opts.authCallback = new TokenCallback() { + /* implement callback, using Ably instance with key */ + @Override + public Object getTokenRequest(TokenParams params) throws AblyException { + synchronized (flags) { + flags[0] = true; + } + return ablyForToken.auth.requestToken(params, null); + } + }; + AblyRealtime ably = new AblyRealtime(opts); + + /* Test UPDATE event delivery */ + ably.connection.on(ConnectionEvent.update, new ConnectionStateListener() { + @Override + public void onConnectionStateChanged(ConnectionStateChange state) { + flags[2] = true; + } + }); + ably.connection.on(new ConnectionStateListener() { + @Override + public void onConnectionStateChanged(ConnectionStateChange state) { + if (state.previous == ConnectionState.connected && state.current != ConnectionState.connected) { + synchronized (flags) { + flags[1] = true; + flags.notify(); + } + } + } + }); + + synchronized (flags) { + try { + flags.wait(8000); + } catch (InterruptedException e) {} + } + + assertTrue("Verify token generation was called", flags[0]); + assertFalse("Verify connection didn't leave connected state", flags[1]); + assertTrue("Verify UPDATE event was delivered", flags[2]); + + } catch (AblyException e) { + e.printStackTrace(); + fail("init0: Unexpected exception instantiating library"); + } + } + + /** + * Verify that the connection fails when attempting to recover with a + * malformed connection id + */ + @Test + public void connect_invalid_recover_fail() { + AblyRealtime ably = null; + try { + ClientOptions opts = createOptions(testVars.keys[0].keyStr); + opts.recover = "not_a_valid_connection_id:99"; + ably = new AblyRealtime(opts); + ConnectionWaiter connectionWaiter = new ConnectionWaiter(ably.connection); + ErrorInfo fail = connectionWaiter.waitFor(ConnectionState.failed); + assertEquals("Verify failed state is reached", ConnectionState.failed, ably.connection.state); + assertEquals("Verify correct error code is given", 80018, fail.code); + } catch (AblyException e) { + e.printStackTrace(); + fail("init0: Unexpected exception instantiating library"); + } finally { + ably.close(); + } + } + + /** + * Verify that the connection creates a new connection but reports + * a recovery error, when attempting to recover with an unknown + * connection id + */ + @Test + public void connect_unknown_recover_fail() { + AblyRealtime ably = null; + try { + ClientOptions opts = createOptions(testVars.keys[0].keyStr); + String recoverConnectionId = "0123456789abcdef-99"; + opts.recover = recoverConnectionId + ":0"; + ably = new AblyRealtime(opts); + ConnectionWaiter connectionWaiter = new ConnectionWaiter(ably.connection); + ErrorInfo connectedError = connectionWaiter.waitFor(ConnectionState.connected); + assertEquals("Verify connected state is reached", ConnectionState.connected, ably.connection.state); + assertNotNull("Verify error is returned", connectedError); + assertEquals("Verify correct error code is given", 80008, connectedError.code); + assertFalse("Verify new connection id is assigned", recoverConnectionId.equals(ably.connection.key)); + } catch (AblyException e) { + e.printStackTrace(); + fail("init0: Unexpected exception instantiating library"); + } finally { + ably.close(); + } + } + + /** + * Test that connection manager correctly fails messages set stored in message queue + * Spec: RTN7c + */ + + @Test + public void connect_test_queued_messages_on_failure() { + AblyRealtime ably = null; + try { + ClientOptions opts = createOptions(testVars.keys[0].keyStr); + ably = new AblyRealtime(opts); + final int[] numberOfErrors = new int[]{0}; + + // assume we are in connecting state now + ably.connection.connectionManager.send(new ProtocolMessage(), true, new CompletionListener() { + @Override + public void onSuccess() { + fail("Unexpected success sending message"); + } + + @Override + public void onError(ErrorInfo reason) { + numberOfErrors[0]++; + } + }); + + // transition to suspended state failing messages + ably.connection.connectionManager.requestState(ConnectionState.suspended); + try { + Thread.sleep(500); + } catch (InterruptedException e) { + } + // transition once more to ensure onError() won't be called twice + ably.connection.connectionManager.requestState(ConnectionState.closed); + try { + Thread.sleep(500); + } catch (InterruptedException e) { + } + + // onError() should be called only once + assertEquals("Verifying number of onError() calls", numberOfErrors[0], 1); + } catch (AblyException e) { + e.printStackTrace(); + fail("Unexpected exception"); + } finally { + if (ably != null) + ably.close(); + } + } + + /** + * Allow token to expire and try to authorize with already expired token after that. Test that the connection state + * is changed in the correct way and without duplicates: + * + * connecting -> connected -> disconnected -> connecting -> disconnected -> connecting -> disconnected + */ + @Test + public void connect_reauth_failure_state_flow_test() { + + try { + AblyRest ablyRest = null; + ClientOptions opts = createOptions(testVars.keys[0].keyStr); + + ablyRest = new AblyRest(opts); + final TokenDetails tokenDetails = ablyRest.auth.requestToken(new TokenParams() {{ ttl = 8000L; }}, null); + assertNotNull("Expected token value", tokenDetails.token); + + final ArrayList stateHistory = new ArrayList<>(); + + ClientOptions optsForRealtime = createOptions(); + optsForRealtime.authCallback = new TokenCallback() { + @Override + public Object getTokenRequest(TokenParams params) throws AblyException { + // return already expired token + return tokenDetails; + } + }; + optsForRealtime.tokenDetails = tokenDetails; + final AblyRealtime ablyRealtime = new AblyRealtime(optsForRealtime); + + ablyRealtime.connection.on(ConnectionState.connected, new ConnectionStateListener() { + @Override + public void onConnectionStateChanged(ConnectionStateChange state) { + /* To go quicker into a disconnected state we use a + * smaller value for maxIdleInterval + */ + try { + Field field = ablyRealtime.connection.connectionManager.getClass().getDeclaredField("maxIdleInterval"); + field.setAccessible(true); + field.setLong(ablyRealtime.connection.connectionManager, 5000L); + } catch (NoSuchFieldException|IllegalAccessException e) { + fail("Unexpected exception in checking connectionStateTtl"); + } + } + }); + + (new ConnectionWaiter(ablyRealtime.connection)).waitFor(ConnectionState.connected); + // TODO: improve by collecting and testing also auth attempts + final List correctHistory = Arrays.asList( + ConnectionState.disconnected, + ConnectionState.connecting, + ConnectionState.disconnected, + ConnectionState.connecting, + ConnectionState.disconnected + ); + final int maxDisconnections = 3; + ablyRealtime.connection.on(new ConnectionStateListener() { + int disconnections = 0; + @Override + public void onConnectionStateChanged(ConnectionStateChange state) { + synchronized (stateHistory) { + stateHistory.add(state.current); + if (state.current == ConnectionState.disconnected) { + disconnections++; + if (disconnections == maxDisconnections) { + assertTrue("Verifying state change history", stateHistory.equals(correctHistory)); + ablyRealtime.close(); + } + } + } + } + }); + + ConnectionWaiter connectionWaiter = new ConnectionWaiter(ablyRealtime.connection); + connectionWaiter.waitFor(ConnectionState.closed); + } catch (AblyException e) { + e.printStackTrace(); + fail("init0: Unexpected exception instantiating library"); + } + } + + /** + * Throw exception in authCallback repeatedly and check if connection goes into suspended state + */ + @Test + public void connect_auth_failure_and_suspend_test() { + AblyRealtime ablyRealtime = null; + AblyRest ablyRest = null; + int oldDisconnectTimeout = Defaults.TIMEOUT_DISCONNECT; + + try { + /* Make test faster */ + Defaults.TIMEOUT_DISCONNECT = 1000; + + final int[] numberOfAuthCalls = new int[] {0}; + final boolean[] reachedFinalState = new boolean[] {false}; + + ClientOptions opts = createOptions(testVars.keys[0].keyStr); + + ablyRest = new AblyRest(opts); + final TokenDetails tokenDetails = ablyRest.auth.requestToken(new TokenParams() {{ ttl = 5000L; }}, null); + assertNotNull("Expected token value", tokenDetails.token); + + ClientOptions optsForRealtime = createOptions(); + optsForRealtime.authCallback = new TokenCallback() { + @Override + public Object getTokenRequest(TokenParams params) throws AblyException { + if (numberOfAuthCalls[0]++ == 0) + return tokenDetails; + else + throw AblyException.fromErrorInfo(new ErrorInfo("Auth failure", 90000)); + } + }; + ablyRealtime = new AblyRealtime(optsForRealtime); + + ablyRealtime.connection.on(new ConnectionStateListener() { + @Override + public void onConnectionStateChanged(ConnectionStateChange state) { + System.out.println(String.format("New state: %s", state.current)); + synchronized (reachedFinalState) { + reachedFinalState[0] = state.current == ConnectionState.closed || + state.current == ConnectionState.suspended || + state.current == ConnectionState.failed; + reachedFinalState.notify(); + } + } + }); + + synchronized (reachedFinalState) { + while (!reachedFinalState[0]) { + try { reachedFinalState.wait(); } catch (InterruptedException e) {} + } + } + + assertEquals("Verify suspended state", ablyRealtime.connection.state, ConnectionState.suspended); + } catch (AblyException e) { + e.printStackTrace(); + fail("init0: Unexpected exception instantiating library"); + } finally { + Defaults.TIMEOUT_DISCONNECT = oldDisconnectTimeout; + if (ablyRealtime != null) + ablyRealtime.close(); + } + } } diff --git a/lib/src/test/java/io/ably/lib/test/realtime/RealtimeConnectTest.java b/lib/src/test/java/io/ably/lib/test/realtime/RealtimeConnectTest.java index ae2422ece..4c5ed455b 100644 --- a/lib/src/test/java/io/ably/lib/test/realtime/RealtimeConnectTest.java +++ b/lib/src/test/java/io/ably/lib/test/realtime/RealtimeConnectTest.java @@ -24,226 +24,226 @@ public class RealtimeConnectTest extends ParameterizedTest { - public Timeout testTimeout = Timeout.seconds(30); - - /** - * Perform a simple connect to the service and confirm that the connected state is reached. - * Also confirm that we did not get token authorization, as we did not - * set useTokenAuth=true. - */ - @Test - public void connect() { - try { - ClientOptions opts = createOptions(testVars.keys[0].keyStr); - AblyRealtime ably = new AblyRealtime(opts); - ConnectionWaiter connectionWaiter = new ConnectionWaiter(ably.connection); - - connectionWaiter.waitFor(ConnectionState.connected); - assertEquals("Verify connected state is reached", ConnectionState.connected, ably.connection.state); - assertTrue("Not expecting token auth", ably.auth.getAuthMethod() == AuthMethod.basic); - - ably.close(); - connectionWaiter.waitFor(ConnectionState.closed); - assertEquals("Verify closed state is reached", ConnectionState.closed, ably.connection.state); - } catch (AblyException e) { - e.printStackTrace(); - fail("init0: Unexpected exception instantiating library"); - } - } - - /** - * Perform a simple connect to the service - * and confirm that heartbeat messages are received. - */ - @Test - public void connect_heartbeat() { - try { - ClientOptions opts = createOptions(testVars.keys[0].keyStr); - CompletionWaiter heartbeatWaiter = new CompletionWaiter(); - AblyRealtime ably = new AblyRealtime(opts); - ConnectionWaiter connectionWaiter = new ConnectionWaiter(ably.connection); - connectionWaiter.waitFor(ConnectionState.connected); - assertEquals("Verify connected state is reached", ConnectionState.connected, ably.connection.state); - ably.connection.ping(heartbeatWaiter); - heartbeatWaiter.waitFor(); - assertTrue("Verify heartbeat occurred", heartbeatWaiter.success); - ably.close(); - connectionWaiter.waitFor(ConnectionState.closed); - assertEquals("Verify closed state is reached", ConnectionState.closed, ably.connection.state); - } catch (AblyException e) { - e.printStackTrace(); - fail("init0: Unexpected exception instantiating library"); - } - } - - /** - * Perform a simple connect, close the connection, and verify that - * the connection can be re-established by calling connect(). - */ - @Test - public void connect_after_close() { - try { - ClientOptions opts = createOptions(testVars.keys[0].keyStr); - AblyRealtime ably = new AblyRealtime(opts); - ConnectionWaiter connectionWaiter = new ConnectionWaiter(ably.connection); - connectionWaiter.waitFor(ConnectionState.connected); - assertEquals("Verify connected state is reached", ConnectionState.connected, ably.connection.state); - - /* send a few channels to increment PendingMessageQueue.startSerial */ - for (int i = 0; i < 3; i++) { - /* publish to the channel */ - CompletionWaiter msgComplete = new CompletionWaiter(); - ably.channels.get("test_channel").publish("test_event", "Test message", msgComplete); - /* wait for the publish callback to be called */ - msgComplete.waitFor(); - assertTrue("Verify success callback was called", msgComplete.success); - } - - ably.close(); - connectionWaiter.waitFor(ConnectionState.closed); - assertEquals("Verify closed state is reached", ConnectionState.closed, ably.connection.state); - try { Thread.sleep(1000L); } catch(InterruptedException e) {} - - ably.connection.connect(); - connectionWaiter.waitFor(ConnectionState.connected); - assertEquals("Verify connected state is reached", ConnectionState.connected, ably.connection.state); - - /* publish to the channel in the new connection to check that it works */ - CompletionWaiter msgComplete = new CompletionWaiter(); - ably.channels.get("test_channel").publish("test_event", "Test message", msgComplete); - /* wait for the publish callback to be called */ - msgComplete.waitFor(); - assertTrue("Verify success callback was called", msgComplete.success); - - ably.close(); - connectionWaiter.waitFor(ConnectionState.closed); - assertEquals("Verify closed state is reached", ConnectionState.closed, ably.connection.state); - } catch (AblyException e) { - e.printStackTrace(); - fail("init0: Unexpected exception instantiating library"); - } - } - - /** - * Connect with useTokenAuth=true and verify we got a token - */ - @Test - public void connect_useTokenAuth() { - AblyRealtime ably = null; - try { - ClientOptions opts = createOptions(testVars.keys[0].keyStr); - opts.useTokenAuth = true; - ably = new AblyRealtime(opts); - ConnectionWaiter connectionWaiter = new ConnectionWaiter(ably.connection); - connectionWaiter.waitFor(ConnectionState.connected); - assertEquals("Verify connected state is reached", ConnectionState.connected, ably.connection.state); - assertTrue("Expected to use token auth", ably.auth.getAuthMethod() == AuthMethod.token); - System.out.println("Token is " + ably.auth.getTokenDetails().token); - } catch (AblyException e) { - e.printStackTrace(); - fail("init1: Unexpected exception instantiating library"); - } finally { - if(ably != null) ably.close(); - } - } - - /** - * Verify that given transport params are included in the ws connection URL. - */ - @Test - public void connect_with_transport_params() { - try { - DebugOptions opts = new DebugOptions(testVars.keys[0].keyStr); - fillInOptions(opts); - final String[] urlWrapper = new String[1]; - opts.protocolListener = new RawProtocolListener() { - @Override - public void onRawConnect(String url) { - /* store url */ - urlWrapper[0] = url; - } - @Override - public void onRawConnectRequested(String url) {} - @Override - public void onRawMessageSend(ProtocolMessage message) {} - @Override - public void onRawMessageRecv(ProtocolMessage message) {} - }; - opts.transportParams = new Param[] {new Param("testStringParam", "testStringValue"), new Param("testIntParam", 100), new Param("testBooleanParam", false)}; - AblyRealtime ably = new AblyRealtime(opts); - ConnectionWaiter connectionWaiter = new ConnectionWaiter(ably.connection); - - connectionWaiter.waitFor(ConnectionState.connected); - assertEquals("Verify connected state is reached", ConnectionState.connected, ably.connection.state); - - String url = urlWrapper[0]; - assertNotNull("Verify connection url was obtained", url); - assertTrue("Verify expected string param present", url.contains("testStringParam=testStringValue")); - assertTrue("Verify expected int param present", url.contains("testIntParam=100")); - assertTrue("Verify expected boolean param present", url.contains("testBooleanParam=false")); - - ably.close(); - connectionWaiter.waitFor(ConnectionState.closed); - assertEquals("Verify closed state is reached", ConnectionState.closed, ably.connection.state); - } catch (AblyException e) { - e.printStackTrace(); - fail("connect_with_transport_params: Unexpected exception instantiating library"); - } - } - - /** - * Initiate a connect using AblyRealtime.connect() - */ - @Test - public void realtime_connect_proxy() { - try { - ClientOptions opts = createOptions(testVars.keys[0].keyStr); - opts.autoConnect = false; - AblyRealtime ably = new AblyRealtime(opts); - ConnectionWaiter connectionWaiter = new ConnectionWaiter(ably.connection); - - /* verify no connection happens */ - assertFalse("Verify no connection happens", connectionWaiter.waitFor(ConnectionState.connected, 1, 1000L)); - - /* trigger connection */ - ably.connect(); - connectionWaiter.waitFor(ConnectionState.connected); - assertEquals("Verify connected state is reached", ConnectionState.connected, ably.connection.state); - - ably.close(); - connectionWaiter.waitFor(ConnectionState.closed); - assertEquals("Verify closed state is reached", ConnectionState.closed, ably.connection.state); - } catch (AblyException e) { - e.printStackTrace(); - fail("realtime_connect_proxy: Unexpected exception instantiating library"); - } - } - - /** - * Close the connection whilst in the connecting state, verifying that the - * connection is closed down - */ - @Test - public void close_when_connecting() { - try { - ClientOptions opts = createOptions(testVars.keys[0].keyStr); - AblyRealtime ably = new AblyRealtime(opts); - ConnectionWaiter connectionWaiter = new ConnectionWaiter(ably.connection); - - connectionWaiter.waitFor(ConnectionState.connecting); - assertEquals("Verify connecting state is reached", ConnectionState.connecting, ably.connection.state); - - ably.close(); - connectionWaiter.waitFor(ConnectionState.closed); - assertEquals("Verify closed state is reached", ConnectionState.closed, ably.connection.state); - - /* wait to see if a further state change occurs */ - try { Thread.sleep(2000L); } catch(InterruptedException e) {} - assertEquals("Verify closed state is unchanged", ConnectionState.closed, ably.connection.state); - } catch (AblyException e) { - e.printStackTrace(); - fail("close_when_connecting: Unexpected exception instantiating library"); - } - } + public Timeout testTimeout = Timeout.seconds(30); + + /** + * Perform a simple connect to the service and confirm that the connected state is reached. + * Also confirm that we did not get token authorization, as we did not + * set useTokenAuth=true. + */ + @Test + public void connect() { + try { + ClientOptions opts = createOptions(testVars.keys[0].keyStr); + AblyRealtime ably = new AblyRealtime(opts); + ConnectionWaiter connectionWaiter = new ConnectionWaiter(ably.connection); + + connectionWaiter.waitFor(ConnectionState.connected); + assertEquals("Verify connected state is reached", ConnectionState.connected, ably.connection.state); + assertTrue("Not expecting token auth", ably.auth.getAuthMethod() == AuthMethod.basic); + + ably.close(); + connectionWaiter.waitFor(ConnectionState.closed); + assertEquals("Verify closed state is reached", ConnectionState.closed, ably.connection.state); + } catch (AblyException e) { + e.printStackTrace(); + fail("init0: Unexpected exception instantiating library"); + } + } + + /** + * Perform a simple connect to the service + * and confirm that heartbeat messages are received. + */ + @Test + public void connect_heartbeat() { + try { + ClientOptions opts = createOptions(testVars.keys[0].keyStr); + CompletionWaiter heartbeatWaiter = new CompletionWaiter(); + AblyRealtime ably = new AblyRealtime(opts); + ConnectionWaiter connectionWaiter = new ConnectionWaiter(ably.connection); + connectionWaiter.waitFor(ConnectionState.connected); + assertEquals("Verify connected state is reached", ConnectionState.connected, ably.connection.state); + ably.connection.ping(heartbeatWaiter); + heartbeatWaiter.waitFor(); + assertTrue("Verify heartbeat occurred", heartbeatWaiter.success); + ably.close(); + connectionWaiter.waitFor(ConnectionState.closed); + assertEquals("Verify closed state is reached", ConnectionState.closed, ably.connection.state); + } catch (AblyException e) { + e.printStackTrace(); + fail("init0: Unexpected exception instantiating library"); + } + } + + /** + * Perform a simple connect, close the connection, and verify that + * the connection can be re-established by calling connect(). + */ + @Test + public void connect_after_close() { + try { + ClientOptions opts = createOptions(testVars.keys[0].keyStr); + AblyRealtime ably = new AblyRealtime(opts); + ConnectionWaiter connectionWaiter = new ConnectionWaiter(ably.connection); + connectionWaiter.waitFor(ConnectionState.connected); + assertEquals("Verify connected state is reached", ConnectionState.connected, ably.connection.state); + + /* send a few channels to increment PendingMessageQueue.startSerial */ + for (int i = 0; i < 3; i++) { + /* publish to the channel */ + CompletionWaiter msgComplete = new CompletionWaiter(); + ably.channels.get("test_channel").publish("test_event", "Test message", msgComplete); + /* wait for the publish callback to be called */ + msgComplete.waitFor(); + assertTrue("Verify success callback was called", msgComplete.success); + } + + ably.close(); + connectionWaiter.waitFor(ConnectionState.closed); + assertEquals("Verify closed state is reached", ConnectionState.closed, ably.connection.state); + try { Thread.sleep(1000L); } catch(InterruptedException e) {} + + ably.connection.connect(); + connectionWaiter.waitFor(ConnectionState.connected); + assertEquals("Verify connected state is reached", ConnectionState.connected, ably.connection.state); + + /* publish to the channel in the new connection to check that it works */ + CompletionWaiter msgComplete = new CompletionWaiter(); + ably.channels.get("test_channel").publish("test_event", "Test message", msgComplete); + /* wait for the publish callback to be called */ + msgComplete.waitFor(); + assertTrue("Verify success callback was called", msgComplete.success); + + ably.close(); + connectionWaiter.waitFor(ConnectionState.closed); + assertEquals("Verify closed state is reached", ConnectionState.closed, ably.connection.state); + } catch (AblyException e) { + e.printStackTrace(); + fail("init0: Unexpected exception instantiating library"); + } + } + + /** + * Connect with useTokenAuth=true and verify we got a token + */ + @Test + public void connect_useTokenAuth() { + AblyRealtime ably = null; + try { + ClientOptions opts = createOptions(testVars.keys[0].keyStr); + opts.useTokenAuth = true; + ably = new AblyRealtime(opts); + ConnectionWaiter connectionWaiter = new ConnectionWaiter(ably.connection); + connectionWaiter.waitFor(ConnectionState.connected); + assertEquals("Verify connected state is reached", ConnectionState.connected, ably.connection.state); + assertTrue("Expected to use token auth", ably.auth.getAuthMethod() == AuthMethod.token); + System.out.println("Token is " + ably.auth.getTokenDetails().token); + } catch (AblyException e) { + e.printStackTrace(); + fail("init1: Unexpected exception instantiating library"); + } finally { + if(ably != null) ably.close(); + } + } + + /** + * Verify that given transport params are included in the ws connection URL. + */ + @Test + public void connect_with_transport_params() { + try { + DebugOptions opts = new DebugOptions(testVars.keys[0].keyStr); + fillInOptions(opts); + final String[] urlWrapper = new String[1]; + opts.protocolListener = new RawProtocolListener() { + @Override + public void onRawConnect(String url) { + /* store url */ + urlWrapper[0] = url; + } + @Override + public void onRawConnectRequested(String url) {} + @Override + public void onRawMessageSend(ProtocolMessage message) {} + @Override + public void onRawMessageRecv(ProtocolMessage message) {} + }; + opts.transportParams = new Param[] {new Param("testStringParam", "testStringValue"), new Param("testIntParam", 100), new Param("testBooleanParam", false)}; + AblyRealtime ably = new AblyRealtime(opts); + ConnectionWaiter connectionWaiter = new ConnectionWaiter(ably.connection); + + connectionWaiter.waitFor(ConnectionState.connected); + assertEquals("Verify connected state is reached", ConnectionState.connected, ably.connection.state); + + String url = urlWrapper[0]; + assertNotNull("Verify connection url was obtained", url); + assertTrue("Verify expected string param present", url.contains("testStringParam=testStringValue")); + assertTrue("Verify expected int param present", url.contains("testIntParam=100")); + assertTrue("Verify expected boolean param present", url.contains("testBooleanParam=false")); + + ably.close(); + connectionWaiter.waitFor(ConnectionState.closed); + assertEquals("Verify closed state is reached", ConnectionState.closed, ably.connection.state); + } catch (AblyException e) { + e.printStackTrace(); + fail("connect_with_transport_params: Unexpected exception instantiating library"); + } + } + + /** + * Initiate a connect using AblyRealtime.connect() + */ + @Test + public void realtime_connect_proxy() { + try { + ClientOptions opts = createOptions(testVars.keys[0].keyStr); + opts.autoConnect = false; + AblyRealtime ably = new AblyRealtime(opts); + ConnectionWaiter connectionWaiter = new ConnectionWaiter(ably.connection); + + /* verify no connection happens */ + assertFalse("Verify no connection happens", connectionWaiter.waitFor(ConnectionState.connected, 1, 1000L)); + + /* trigger connection */ + ably.connect(); + connectionWaiter.waitFor(ConnectionState.connected); + assertEquals("Verify connected state is reached", ConnectionState.connected, ably.connection.state); + + ably.close(); + connectionWaiter.waitFor(ConnectionState.closed); + assertEquals("Verify closed state is reached", ConnectionState.closed, ably.connection.state); + } catch (AblyException e) { + e.printStackTrace(); + fail("realtime_connect_proxy: Unexpected exception instantiating library"); + } + } + + /** + * Close the connection whilst in the connecting state, verifying that the + * connection is closed down + */ + @Test + public void close_when_connecting() { + try { + ClientOptions opts = createOptions(testVars.keys[0].keyStr); + AblyRealtime ably = new AblyRealtime(opts); + ConnectionWaiter connectionWaiter = new ConnectionWaiter(ably.connection); + + connectionWaiter.waitFor(ConnectionState.connecting); + assertEquals("Verify connecting state is reached", ConnectionState.connecting, ably.connection.state); + + ably.close(); + connectionWaiter.waitFor(ConnectionState.closed); + assertEquals("Verify closed state is reached", ConnectionState.closed, ably.connection.state); + + /* wait to see if a further state change occurs */ + try { Thread.sleep(2000L); } catch(InterruptedException e) {} + assertEquals("Verify closed state is unchanged", ConnectionState.closed, ably.connection.state); + } catch (AblyException e) { + e.printStackTrace(); + fail("close_when_connecting: Unexpected exception instantiating library"); + } + } } diff --git a/lib/src/test/java/io/ably/lib/test/realtime/RealtimeCryptoTest.java b/lib/src/test/java/io/ably/lib/test/realtime/RealtimeCryptoTest.java index ef8731bf0..3f8eb9a11 100644 --- a/lib/src/test/java/io/ably/lib/test/realtime/RealtimeCryptoTest.java +++ b/lib/src/test/java/io/ably/lib/test/realtime/RealtimeCryptoTest.java @@ -37,1085 +37,1085 @@ public class RealtimeCryptoTest extends ParameterizedTest { - @Rule - public Timeout testTimeout = Timeout.seconds(30); - - /** - * Connect to the service - * and publish an encrypted message on that channel using - * the default cipher params - */ - @Test - public void single_send() { - String channelName = "single_send_" + testParams.name; - AblyRealtime ably = null; - try { - ClientOptions opts = createOptions(testVars.keys[0].keyStr); - ably = new AblyRealtime(opts); - - /* create a channel */ - ChannelOptions channelOpts = new ChannelOptions() {{ encrypted = true; }}; - final Channel channel = ably.channels.get(channelName, channelOpts); - - /* attach */ - channel.attach(); - (new ChannelWaiter(channel)).waitFor(ChannelState.attached); - assertEquals("Channel is not attached", channel.state, ChannelState.attached); - - /* subscribe */ - MessageWaiter messageWaiter = new MessageWaiter(channel); - - /* publish to the channel */ - String messageText = "Test message (subscribe_send_binary)"; - CompletionWaiter msgComplete = new CompletionWaiter(); - channel.publish("test_event", messageText, msgComplete); - - /* wait for the publish callback to be called */ - msgComplete.waitFor(); - assertTrue("Success callback was not called", msgComplete.success); - - /* wait for the subscription callback to be called */ - messageWaiter.waitFor(1); - assertEquals( - "Unexpected number of received messages", - messageWaiter.receivedMessages.size(), 1 - ); - - /* check the correct plaintext recovered from the message */ - assertTrue( - "Unexpected message received", - messageText.equals(messageWaiter.receivedMessages.get(0).data) - ); - - } catch (AblyException e) { - e.printStackTrace(); - fail("init0: Unexpected exception instantiating library"); - } finally { - if(ably != null) { - ably.close(); - } - } - } - - /** - * Connect to the service - * and publish an encrypted message on that channel using - * a 256-bit key - */ - @Test - public void single_send_256() { - String channelName = "single_send_256_" + testParams.name; - AblyRealtime ably = null; - try { - ClientOptions opts = createOptions(testVars.keys[0].keyStr); - ably = new AblyRealtime(opts); - - /* create a key */ - KeyGenerator keygen = KeyGenerator.getInstance("AES"); - keygen.init(256); - byte[] key = keygen.generateKey().getEncoded(); - final CipherParams params = Crypto.getDefaultParams(key); - - /* create a channel */ - ChannelOptions channelOpts = new ChannelOptions() {{ encrypted = true; this.cipherParams = params; }}; - final Channel channel = ably.channels.get(channelName, channelOpts); - - /* attach */ - channel.attach(); - (new ChannelWaiter(channel)).waitFor(ChannelState.attached); - assertEquals( - "Channel is not attached", - channel.state, ChannelState.attached - ); - - /* subscribe */ - MessageWaiter messageWaiter = new MessageWaiter(channel); - - /* publish to the channel */ - String messageText = "Test message (subscribe_send_binary)"; - CompletionWaiter msgComplete = new CompletionWaiter(); - channel.publish("test_event", messageText, msgComplete); - - /* wait for the publish callback to be called */ - msgComplete.waitFor(); - assertTrue("Success callback was not called", msgComplete.success); - - /* wait for the subscription callback to be called */ - messageWaiter.waitFor(1); - assertEquals( - "Unexpected number of received messages", - messageWaiter.receivedMessages.size(), 1 - ); - - /* check the correct plaintext recovered from the message */ - assertTrue( - "Unexpected message received", - messageText.equals(messageWaiter.receivedMessages.get(0).data) - ); - - } catch (AblyException e) { - e.printStackTrace(); - fail("init0: Unexpected exception instantiating library"); - } catch (NoSuchAlgorithmException e) { - e.printStackTrace(); - fail("init0: Unexpected exception generating key"); - } finally { - if(ably != null) { - ably.close(); - } - } - } - - /** - * Connect to the service using the default (binary) protocol - * and attach, subscribe to an event, and publish multiple - * messages on that channel - */ - private void _multiple_send(String channelName, int messageCount, long delay) { - AblyRealtime ably = null; - try { - ClientOptions opts = createOptions(testVars.keys[0].keyStr); - ably = new AblyRealtime(opts); - - /* generate and remember message texts */ - String[] messageTexts = new String[messageCount]; - for(int i = 0; i < messageCount; i++) { - messageTexts[i] = "Test message (_multiple_send) " + i; - } - /* create a channel */ - ChannelOptions channelOpts = new ChannelOptions() {{ encrypted = true; }}; - final Channel channel = ably.channels.get(channelName, channelOpts); - - /* attach */ - channel.attach(); - (new ChannelWaiter(channel)).waitFor(ChannelState.attached); - assertEquals( - "Channel is not attached", - channel.state, ChannelState.attached - ); - - /* subscribe */ - MessageWaiter messageWaiter = new MessageWaiter(channel); - - /* publish to the channel */ - CompletionSet msgComplete = new CompletionSet(); - for(int i = 0; i < messageCount; i++) { - channel.publish("test_event", messageTexts[i], msgComplete.add()); - try { Thread.sleep(delay); } catch(InterruptedException e){} - } - - /* wait for the publish callback to be called */ - ErrorInfo[] errors = msgComplete.waitFor(); - assertTrue("Errors when sending messages", errors.length == 0); - - /* wait for the subscription callback to be called */ - messageWaiter.waitFor(messageCount); - assertEquals( - "Unexpected number of messages received", - messageWaiter.receivedMessages.size(), messageCount - ); - - /* check the correct plaintext recovered from the message */ - for(int i = 0; i < messageCount; i++) { - assertTrue( - "Unexpected message received", - messageTexts[i].equals( - messageWaiter.receivedMessages.get(i).data - ) - ); - } - } catch (AblyException e) { - e.printStackTrace(); - fail("init0: Unexpected exception instantiating library"); - } finally { - if(ably != null) { - ably.close(); - } - } - } - - @Test - public void multiple_send_2_200() { - int messageCount = 2; - long delay = 200L; - _multiple_send("multiple_send_binary_2_200_" + testParams.name, messageCount, delay); - } - - @Test - public void multiple_send_20_100() { - int messageCount = 20; - long delay = 100L; - _multiple_send("multiple_send_binary_20_100_" + testParams.name, messageCount, delay); - } - - /** - * Connect twice to the service, using the default (binary) protocol - * and the text protocol. Publish an encrypted message on that channel using - * the default cipher params and verify correct receipt. - */ - @Test - public void single_send_binary_text() { - String channelName = "single_send_binary_text_" + testParams.name; - AblyRealtime sender = null; - AblyRealtime receiver = null; - try { - ClientOptions senderOpts = createOptions(testVars.keys[0].keyStr); - sender = new AblyRealtime(senderOpts); - ClientOptions receiverOpts = createOptions(testVars.keys[0].keyStr); - receiverOpts.useBinaryProtocol = !testParams.useBinaryProtocol; - receiver = new AblyRealtime(receiverOpts); - - /* create a key */ - final CipherParams params = Crypto.getDefaultParams(); - - /* create a channel */ - final ChannelOptions senderChannelOpts = new ChannelOptions() {{ encrypted = true; cipherParams = params; }}; - final Channel senderChannel = sender.channels.get(channelName, senderChannelOpts); - final ChannelOptions receiverChannelOpts = new ChannelOptions() {{ encrypted = true; cipherParams = params; }}; - final Channel receiverChannel = receiver.channels.get(channelName, receiverChannelOpts); - - /* attach */ - senderChannel.attach(); - receiverChannel.attach(); - (new ChannelWaiter(senderChannel)).waitFor(ChannelState.attached); - (new ChannelWaiter(receiverChannel)).waitFor(ChannelState.attached); - assertEquals( - "Sender channel is not attached", - senderChannel.state, ChannelState.attached - ); - assertEquals( - "Receiver channel is not attached", - receiverChannel.state, ChannelState.attached - ); - - /* subscribe */ - MessageWaiter messageWaiter = new MessageWaiter(receiverChannel); - - /* publish to the channel */ - String messageText = "Test message (single_send_binary_text)"; - CompletionWaiter msgComplete = new CompletionWaiter(); - senderChannel.publish("test_event", messageText, msgComplete); - - /* wait for the publish callback to be called */ - msgComplete.waitFor(); - assertTrue("Success callback was not called", msgComplete.success); - - /* wait for the subscription callback to be called */ - messageWaiter.waitFor(1); - assertEquals( - "Unexpected number of received messages", - messageWaiter.receivedMessages.size(), 1 - ); - /* check the correct plaintext recovered from the message */ - assertTrue( - "Unexpected message received", - messageText.equals(messageWaiter.receivedMessages.get(0).data) - ); - } catch (AblyException e) { - e.printStackTrace(); - fail("init0: Unexpected exception instantiating library"); - } finally { - if(sender != null) { - sender.close(); - } - if(receiver != null) { - receiver.close(); - } - } - } - - /** - * Connect twice to the service, using different cipher keys. - * Publish an encrypted message on that channel using - * the default cipher params and verify that the decrypt failure - * is noticed as bad recovered plaintext. - */ - @Test - public void single_send_key_mismatch() { - AblyRealtime sender = null; - AblyRealtime receiver = null; - try { - ClientOptions senderOpts = createOptions(testVars.keys[0].keyStr); - sender = new AblyRealtime(senderOpts); - ClientOptions receiverOpts = createOptions(testVars.keys[0].keyStr); - receiver = new AblyRealtime(receiverOpts); - - /* create a channel */ - final ChannelOptions senderChannelOpts = new ChannelOptions() {{ encrypted = true; }}; - final Channel senderChannel = sender.channels.get("single_send_binary_text", senderChannelOpts); - final ChannelOptions receiverChannelOpts = new ChannelOptions() {{ encrypted = true; }}; - final Channel receiverChannel = receiver.channels.get("single_send_binary_text", receiverChannelOpts); - - /* attach */ - senderChannel.attach(); - receiverChannel.attach(); - (new ChannelWaiter(senderChannel)).waitFor(ChannelState.attached); - (new ChannelWaiter(receiverChannel)).waitFor(ChannelState.attached); - assertEquals( - "Sender channel is not attached", - senderChannel.state, ChannelState.attached - ); - assertEquals( - "Receiver channel is not attached", - receiverChannel.state, ChannelState.attached - ); - - /* subscribe */ - MessageWaiter messageWaiter = new MessageWaiter(receiverChannel); - - /* publish to the channel */ - String messageText = "Test message (single_send_key_mismatch)"; - CompletionWaiter msgComplete = new CompletionWaiter(); - senderChannel.publish("test_event", messageText, msgComplete); - - /* wait for the publish callback to be called */ - msgComplete.waitFor(); - assertTrue("Success callback was not called", msgComplete.success); - - /* wait for the subscription callback to be called */ - messageWaiter.waitFor(1); - assertEquals( - "Unexpected number of received messages", - messageWaiter.receivedMessages.size(), 1 - ); - - /* check the correct plaintext recovered from the message */ - assertFalse( - "Unexpected message received", - messageText.equals(messageWaiter.receivedMessages.get(0).data) - ); - } catch (AblyException e) { - e.printStackTrace(); - fail("init0: Unexpected exception instantiating library"); - } finally { - if(sender != null) { - sender.close(); - } - if(receiver != null) { - receiver.close(); - } - } - } - - - /** - * Connect twice to the service, one with and one without encryption. - * Publish an unencrypted message and verify that the receiving connection - * does not attempt to decrypt it. - */ - @Test - public void single_send_unencrypted() { - AblyRealtime sender = null; - AblyRealtime receiver = null; - try { - ClientOptions senderOpts = createOptions(testVars.keys[0].keyStr); - sender = new AblyRealtime(senderOpts); - ClientOptions receiverOpts = createOptions(testVars.keys[0].keyStr); - receiver = new AblyRealtime(receiverOpts); - - /* create a channel */ - final Channel senderChannel = sender.channels.get("single_send_unencrypted"); - ChannelOptions receiverChannelOpts = new ChannelOptions() {{ encrypted = true; }}; - final Channel receiverChannel = receiver.channels.get("single_send_unencrypted", receiverChannelOpts); - - /* attach */ - senderChannel.attach(); - receiverChannel.attach(); - (new ChannelWaiter(senderChannel)).waitFor(ChannelState.attached); - (new ChannelWaiter(receiverChannel)).waitFor(ChannelState.attached); - assertEquals( - "Sender channel is not attached", - senderChannel.state, ChannelState.attached - ); - assertEquals( - "Receiver channel is not attached", - receiverChannel.state, ChannelState.attached - ); - - /* subscribe */ - MessageWaiter messageWaiter = new MessageWaiter(receiverChannel); - - /* publish to the channel */ - String messageText = "Test message (single_send_unencrypted)"; - CompletionWaiter msgComplete = new CompletionWaiter(); - senderChannel.publish("test_event", messageText, msgComplete); - - /* wait for the publish callback to be called */ - msgComplete.waitFor(); - assertTrue("Success callback was not called", msgComplete.success); - - /* wait for the subscription callback to be called */ - messageWaiter.waitFor(1); - assertEquals( - "Unexpected number of received messages", - messageWaiter.receivedMessages.size(), 1 - ); - - /* check the correct text recovered from the message */ - assertTrue( - "Received message is not correct", - messageText.equals(messageWaiter.receivedMessages.get(0).data) - ); - } catch (AblyException e) { - e.printStackTrace(); - fail("init0: Unexpected exception instantiating library"); - } finally { - if(sender != null) { - sender.close(); - } - if(receiver != null) { - receiver.close(); - } - } - } - - /** - * Connect twice to the service, one with and one without encryption. - * Publish an unencrypted message and verify that the receiving connection - * does not attempt to decrypt it. - */ - @Test - public void single_send_encrypted_unhandled() { - AblyRealtime sender = null; - AblyRealtime receiver = null; - try { - ClientOptions senderOpts = createOptions(testVars.keys[0].keyStr); - sender = new AblyRealtime(senderOpts); - ClientOptions receiverOpts = createOptions(testVars.keys[0].keyStr); - receiver = new AblyRealtime(receiverOpts); - - /* create a channel */ - ChannelOptions senderChannelOpts = new ChannelOptions() {{ encrypted = true; }}; - final Channel senderChannel = sender.channels.get("single_send_encrypted_unhandled", senderChannelOpts); - final Channel receiverChannel = receiver.channels.get("single_send_encrypted_unhandled"); - - /* attach */ - senderChannel.attach(); - receiverChannel.attach(); - (new ChannelWaiter(senderChannel)).waitFor(ChannelState.attached); - (new ChannelWaiter(receiverChannel)).waitFor(ChannelState.attached); - assertEquals( - "Sender channel is not attached", - senderChannel.state, ChannelState.attached - ); - assertEquals( - "Receiver channel is not attached", - receiverChannel.state, ChannelState.attached - ); - - /* subscribe */ - MessageWaiter messageWaiter = new MessageWaiter(receiverChannel); - - /* publish to the channel */ - String messageText = "Test message (single_send_encrypted_unhandled)"; - CompletionWaiter msgComplete = new CompletionWaiter(); - senderChannel.publish("test_event", messageText, msgComplete); - - /* wait for the publish callback to be called */ - msgComplete.waitFor(); - assertTrue("Success callback was not called", msgComplete.success); - - /* wait for the subscription callback to be called */ - messageWaiter.waitFor(1); - assertEquals( - "Unexpected number of messages received", - messageWaiter.receivedMessages.size(), 1 - ); - - /* check the the message payload is indicated as encrypted */ + @Rule + public Timeout testTimeout = Timeout.seconds(30); + + /** + * Connect to the service + * and publish an encrypted message on that channel using + * the default cipher params + */ + @Test + public void single_send() { + String channelName = "single_send_" + testParams.name; + AblyRealtime ably = null; + try { + ClientOptions opts = createOptions(testVars.keys[0].keyStr); + ably = new AblyRealtime(opts); + + /* create a channel */ + ChannelOptions channelOpts = new ChannelOptions() {{ encrypted = true; }}; + final Channel channel = ably.channels.get(channelName, channelOpts); + + /* attach */ + channel.attach(); + (new ChannelWaiter(channel)).waitFor(ChannelState.attached); + assertEquals("Channel is not attached", channel.state, ChannelState.attached); + + /* subscribe */ + MessageWaiter messageWaiter = new MessageWaiter(channel); + + /* publish to the channel */ + String messageText = "Test message (subscribe_send_binary)"; + CompletionWaiter msgComplete = new CompletionWaiter(); + channel.publish("test_event", messageText, msgComplete); + + /* wait for the publish callback to be called */ + msgComplete.waitFor(); + assertTrue("Success callback was not called", msgComplete.success); + + /* wait for the subscription callback to be called */ + messageWaiter.waitFor(1); + assertEquals( + "Unexpected number of received messages", + messageWaiter.receivedMessages.size(), 1 + ); + + /* check the correct plaintext recovered from the message */ + assertTrue( + "Unexpected message received", + messageText.equals(messageWaiter.receivedMessages.get(0).data) + ); + + } catch (AblyException e) { + e.printStackTrace(); + fail("init0: Unexpected exception instantiating library"); + } finally { + if(ably != null) { + ably.close(); + } + } + } + + /** + * Connect to the service + * and publish an encrypted message on that channel using + * a 256-bit key + */ + @Test + public void single_send_256() { + String channelName = "single_send_256_" + testParams.name; + AblyRealtime ably = null; + try { + ClientOptions opts = createOptions(testVars.keys[0].keyStr); + ably = new AblyRealtime(opts); + + /* create a key */ + KeyGenerator keygen = KeyGenerator.getInstance("AES"); + keygen.init(256); + byte[] key = keygen.generateKey().getEncoded(); + final CipherParams params = Crypto.getDefaultParams(key); + + /* create a channel */ + ChannelOptions channelOpts = new ChannelOptions() {{ encrypted = true; this.cipherParams = params; }}; + final Channel channel = ably.channels.get(channelName, channelOpts); + + /* attach */ + channel.attach(); + (new ChannelWaiter(channel)).waitFor(ChannelState.attached); + assertEquals( + "Channel is not attached", + channel.state, ChannelState.attached + ); + + /* subscribe */ + MessageWaiter messageWaiter = new MessageWaiter(channel); + + /* publish to the channel */ + String messageText = "Test message (subscribe_send_binary)"; + CompletionWaiter msgComplete = new CompletionWaiter(); + channel.publish("test_event", messageText, msgComplete); + + /* wait for the publish callback to be called */ + msgComplete.waitFor(); + assertTrue("Success callback was not called", msgComplete.success); + + /* wait for the subscription callback to be called */ + messageWaiter.waitFor(1); + assertEquals( + "Unexpected number of received messages", + messageWaiter.receivedMessages.size(), 1 + ); + + /* check the correct plaintext recovered from the message */ + assertTrue( + "Unexpected message received", + messageText.equals(messageWaiter.receivedMessages.get(0).data) + ); + + } catch (AblyException e) { + e.printStackTrace(); + fail("init0: Unexpected exception instantiating library"); + } catch (NoSuchAlgorithmException e) { + e.printStackTrace(); + fail("init0: Unexpected exception generating key"); + } finally { + if(ably != null) { + ably.close(); + } + } + } + + /** + * Connect to the service using the default (binary) protocol + * and attach, subscribe to an event, and publish multiple + * messages on that channel + */ + private void _multiple_send(String channelName, int messageCount, long delay) { + AblyRealtime ably = null; + try { + ClientOptions opts = createOptions(testVars.keys[0].keyStr); + ably = new AblyRealtime(opts); + + /* generate and remember message texts */ + String[] messageTexts = new String[messageCount]; + for(int i = 0; i < messageCount; i++) { + messageTexts[i] = "Test message (_multiple_send) " + i; + } + /* create a channel */ + ChannelOptions channelOpts = new ChannelOptions() {{ encrypted = true; }}; + final Channel channel = ably.channels.get(channelName, channelOpts); + + /* attach */ + channel.attach(); + (new ChannelWaiter(channel)).waitFor(ChannelState.attached); + assertEquals( + "Channel is not attached", + channel.state, ChannelState.attached + ); + + /* subscribe */ + MessageWaiter messageWaiter = new MessageWaiter(channel); + + /* publish to the channel */ + CompletionSet msgComplete = new CompletionSet(); + for(int i = 0; i < messageCount; i++) { + channel.publish("test_event", messageTexts[i], msgComplete.add()); + try { Thread.sleep(delay); } catch(InterruptedException e){} + } + + /* wait for the publish callback to be called */ + ErrorInfo[] errors = msgComplete.waitFor(); + assertTrue("Errors when sending messages", errors.length == 0); + + /* wait for the subscription callback to be called */ + messageWaiter.waitFor(messageCount); + assertEquals( + "Unexpected number of messages received", + messageWaiter.receivedMessages.size(), messageCount + ); + + /* check the correct plaintext recovered from the message */ + for(int i = 0; i < messageCount; i++) { + assertTrue( + "Unexpected message received", + messageTexts[i].equals( + messageWaiter.receivedMessages.get(i).data + ) + ); + } + } catch (AblyException e) { + e.printStackTrace(); + fail("init0: Unexpected exception instantiating library"); + } finally { + if(ably != null) { + ably.close(); + } + } + } + + @Test + public void multiple_send_2_200() { + int messageCount = 2; + long delay = 200L; + _multiple_send("multiple_send_binary_2_200_" + testParams.name, messageCount, delay); + } + + @Test + public void multiple_send_20_100() { + int messageCount = 20; + long delay = 100L; + _multiple_send("multiple_send_binary_20_100_" + testParams.name, messageCount, delay); + } + + /** + * Connect twice to the service, using the default (binary) protocol + * and the text protocol. Publish an encrypted message on that channel using + * the default cipher params and verify correct receipt. + */ + @Test + public void single_send_binary_text() { + String channelName = "single_send_binary_text_" + testParams.name; + AblyRealtime sender = null; + AblyRealtime receiver = null; + try { + ClientOptions senderOpts = createOptions(testVars.keys[0].keyStr); + sender = new AblyRealtime(senderOpts); + ClientOptions receiverOpts = createOptions(testVars.keys[0].keyStr); + receiverOpts.useBinaryProtocol = !testParams.useBinaryProtocol; + receiver = new AblyRealtime(receiverOpts); + + /* create a key */ + final CipherParams params = Crypto.getDefaultParams(); + + /* create a channel */ + final ChannelOptions senderChannelOpts = new ChannelOptions() {{ encrypted = true; cipherParams = params; }}; + final Channel senderChannel = sender.channels.get(channelName, senderChannelOpts); + final ChannelOptions receiverChannelOpts = new ChannelOptions() {{ encrypted = true; cipherParams = params; }}; + final Channel receiverChannel = receiver.channels.get(channelName, receiverChannelOpts); + + /* attach */ + senderChannel.attach(); + receiverChannel.attach(); + (new ChannelWaiter(senderChannel)).waitFor(ChannelState.attached); + (new ChannelWaiter(receiverChannel)).waitFor(ChannelState.attached); + assertEquals( + "Sender channel is not attached", + senderChannel.state, ChannelState.attached + ); + assertEquals( + "Receiver channel is not attached", + receiverChannel.state, ChannelState.attached + ); + + /* subscribe */ + MessageWaiter messageWaiter = new MessageWaiter(receiverChannel); + + /* publish to the channel */ + String messageText = "Test message (single_send_binary_text)"; + CompletionWaiter msgComplete = new CompletionWaiter(); + senderChannel.publish("test_event", messageText, msgComplete); + + /* wait for the publish callback to be called */ + msgComplete.waitFor(); + assertTrue("Success callback was not called", msgComplete.success); + + /* wait for the subscription callback to be called */ + messageWaiter.waitFor(1); + assertEquals( + "Unexpected number of received messages", + messageWaiter.receivedMessages.size(), 1 + ); + /* check the correct plaintext recovered from the message */ + assertTrue( + "Unexpected message received", + messageText.equals(messageWaiter.receivedMessages.get(0).data) + ); + } catch (AblyException e) { + e.printStackTrace(); + fail("init0: Unexpected exception instantiating library"); + } finally { + if(sender != null) { + sender.close(); + } + if(receiver != null) { + receiver.close(); + } + } + } + + /** + * Connect twice to the service, using different cipher keys. + * Publish an encrypted message on that channel using + * the default cipher params and verify that the decrypt failure + * is noticed as bad recovered plaintext. + */ + @Test + public void single_send_key_mismatch() { + AblyRealtime sender = null; + AblyRealtime receiver = null; + try { + ClientOptions senderOpts = createOptions(testVars.keys[0].keyStr); + sender = new AblyRealtime(senderOpts); + ClientOptions receiverOpts = createOptions(testVars.keys[0].keyStr); + receiver = new AblyRealtime(receiverOpts); + + /* create a channel */ + final ChannelOptions senderChannelOpts = new ChannelOptions() {{ encrypted = true; }}; + final Channel senderChannel = sender.channels.get("single_send_binary_text", senderChannelOpts); + final ChannelOptions receiverChannelOpts = new ChannelOptions() {{ encrypted = true; }}; + final Channel receiverChannel = receiver.channels.get("single_send_binary_text", receiverChannelOpts); + + /* attach */ + senderChannel.attach(); + receiverChannel.attach(); + (new ChannelWaiter(senderChannel)).waitFor(ChannelState.attached); + (new ChannelWaiter(receiverChannel)).waitFor(ChannelState.attached); + assertEquals( + "Sender channel is not attached", + senderChannel.state, ChannelState.attached + ); + assertEquals( + "Receiver channel is not attached", + receiverChannel.state, ChannelState.attached + ); + + /* subscribe */ + MessageWaiter messageWaiter = new MessageWaiter(receiverChannel); + + /* publish to the channel */ + String messageText = "Test message (single_send_key_mismatch)"; + CompletionWaiter msgComplete = new CompletionWaiter(); + senderChannel.publish("test_event", messageText, msgComplete); + + /* wait for the publish callback to be called */ + msgComplete.waitFor(); + assertTrue("Success callback was not called", msgComplete.success); + + /* wait for the subscription callback to be called */ + messageWaiter.waitFor(1); + assertEquals( + "Unexpected number of received messages", + messageWaiter.receivedMessages.size(), 1 + ); + + /* check the correct plaintext recovered from the message */ + assertFalse( + "Unexpected message received", + messageText.equals(messageWaiter.receivedMessages.get(0).data) + ); + } catch (AblyException e) { + e.printStackTrace(); + fail("init0: Unexpected exception instantiating library"); + } finally { + if(sender != null) { + sender.close(); + } + if(receiver != null) { + receiver.close(); + } + } + } + + + /** + * Connect twice to the service, one with and one without encryption. + * Publish an unencrypted message and verify that the receiving connection + * does not attempt to decrypt it. + */ + @Test + public void single_send_unencrypted() { + AblyRealtime sender = null; + AblyRealtime receiver = null; + try { + ClientOptions senderOpts = createOptions(testVars.keys[0].keyStr); + sender = new AblyRealtime(senderOpts); + ClientOptions receiverOpts = createOptions(testVars.keys[0].keyStr); + receiver = new AblyRealtime(receiverOpts); + + /* create a channel */ + final Channel senderChannel = sender.channels.get("single_send_unencrypted"); + ChannelOptions receiverChannelOpts = new ChannelOptions() {{ encrypted = true; }}; + final Channel receiverChannel = receiver.channels.get("single_send_unencrypted", receiverChannelOpts); + + /* attach */ + senderChannel.attach(); + receiverChannel.attach(); + (new ChannelWaiter(senderChannel)).waitFor(ChannelState.attached); + (new ChannelWaiter(receiverChannel)).waitFor(ChannelState.attached); + assertEquals( + "Sender channel is not attached", + senderChannel.state, ChannelState.attached + ); + assertEquals( + "Receiver channel is not attached", + receiverChannel.state, ChannelState.attached + ); + + /* subscribe */ + MessageWaiter messageWaiter = new MessageWaiter(receiverChannel); + + /* publish to the channel */ + String messageText = "Test message (single_send_unencrypted)"; + CompletionWaiter msgComplete = new CompletionWaiter(); + senderChannel.publish("test_event", messageText, msgComplete); + + /* wait for the publish callback to be called */ + msgComplete.waitFor(); + assertTrue("Success callback was not called", msgComplete.success); + + /* wait for the subscription callback to be called */ + messageWaiter.waitFor(1); + assertEquals( + "Unexpected number of received messages", + messageWaiter.receivedMessages.size(), 1 + ); + + /* check the correct text recovered from the message */ + assertTrue( + "Received message is not correct", + messageText.equals(messageWaiter.receivedMessages.get(0).data) + ); + } catch (AblyException e) { + e.printStackTrace(); + fail("init0: Unexpected exception instantiating library"); + } finally { + if(sender != null) { + sender.close(); + } + if(receiver != null) { + receiver.close(); + } + } + } + + /** + * Connect twice to the service, one with and one without encryption. + * Publish an unencrypted message and verify that the receiving connection + * does not attempt to decrypt it. + */ + @Test + public void single_send_encrypted_unhandled() { + AblyRealtime sender = null; + AblyRealtime receiver = null; + try { + ClientOptions senderOpts = createOptions(testVars.keys[0].keyStr); + sender = new AblyRealtime(senderOpts); + ClientOptions receiverOpts = createOptions(testVars.keys[0].keyStr); + receiver = new AblyRealtime(receiverOpts); + + /* create a channel */ + ChannelOptions senderChannelOpts = new ChannelOptions() {{ encrypted = true; }}; + final Channel senderChannel = sender.channels.get("single_send_encrypted_unhandled", senderChannelOpts); + final Channel receiverChannel = receiver.channels.get("single_send_encrypted_unhandled"); + + /* attach */ + senderChannel.attach(); + receiverChannel.attach(); + (new ChannelWaiter(senderChannel)).waitFor(ChannelState.attached); + (new ChannelWaiter(receiverChannel)).waitFor(ChannelState.attached); + assertEquals( + "Sender channel is not attached", + senderChannel.state, ChannelState.attached + ); + assertEquals( + "Receiver channel is not attached", + receiverChannel.state, ChannelState.attached + ); + + /* subscribe */ + MessageWaiter messageWaiter = new MessageWaiter(receiverChannel); + + /* publish to the channel */ + String messageText = "Test message (single_send_encrypted_unhandled)"; + CompletionWaiter msgComplete = new CompletionWaiter(); + senderChannel.publish("test_event", messageText, msgComplete); + + /* wait for the publish callback to be called */ + msgComplete.waitFor(); + assertTrue("Success callback was not called", msgComplete.success); + + /* wait for the subscription callback to be called */ + messageWaiter.waitFor(1); + assertEquals( + "Unexpected number of messages received", + messageWaiter.receivedMessages.size(), 1 + ); + + /* check the the message payload is indicated as encrypted */ // assertTrue("Verify correct message text received", messageWaiter.receivedMessages.get(0).data instanceof CipherData); - } catch (AblyException e) { - e.printStackTrace(); - fail("init0: Unexpected exception instantiating library"); - } finally { - if(sender != null) { - sender.close(); - } - if(receiver != null) { - receiver.close(); - } - } - } - - /** - * Check Channel.setOptions updates CipherParams correctly: - * - publish a message using a key, verifying correct receipt; - * - publish with an updated key on the tx connection and verify that it is not decrypted by the rx connection; - * - publish with an updated key on the rx connection and verify connect receipt - */ - @Test - public void set_cipher_params() { - AblyRealtime sender = null; - AblyRealtime receiver = null; - try { - ClientOptions senderOpts = createOptions(testVars.keys[0].keyStr); - sender = new AblyRealtime(senderOpts); - ClientOptions receiverOpts = createOptions(testVars.keys[0].keyStr); - receiverOpts.useBinaryProtocol = !testParams.useBinaryProtocol; - receiver = new AblyRealtime(receiverOpts); - - /* create a key */ - final CipherParams params1 = Crypto.getDefaultParams(); - - /* create a channel */ - ChannelOptions senderChannelOpts = new ChannelOptions() {{ encrypted = true; cipherParams = params1; }}; - final Channel senderChannel = sender.channels.get("set_cipher_params", senderChannelOpts); - ChannelOptions receiverChannelOpts = new ChannelOptions() {{ encrypted = true; cipherParams = params1; }}; - final Channel receiverChannel = receiver.channels.get("set_cipher_params", receiverChannelOpts); - - /* attach */ - senderChannel.attach(); - receiverChannel.attach(); - (new ChannelWaiter(senderChannel)).waitFor(ChannelState.attached); - (new ChannelWaiter(receiverChannel)).waitFor(ChannelState.attached); - - assertEquals( - "Sender channel is not attached", - senderChannel.state, ChannelState.attached - ); - assertEquals( - "Receiver channel is not attached", - receiverChannel.state, ChannelState.attached - ); - - /* subscribe */ - MessageWaiter messageWaiter = new MessageWaiter(receiverChannel); - - /* publish to the channel */ - String messageText = "Test message (set_cipher_params)"; - CompletionWaiter msgComplete = new CompletionWaiter(); - senderChannel.publish("test_event", messageText, msgComplete); - - /* wait for the publish callback to be called */ - msgComplete.waitFor(); - assertTrue("Success callback was not called", msgComplete.success); - - /* wait for the subscription callback to be called */ - messageWaiter.waitFor(1); - assertEquals( - "Unexpected number of received messages", - messageWaiter.receivedMessages.size(), 1 - ); - - /* check the correct plaintext recovered from the message */ - assertTrue( - "Received message is not correct", - messageText.equals(messageWaiter.receivedMessages.get(0).data) - ); - - /* create a second key and set sender channel opts */ - final CipherParams params2 = Crypto.getDefaultParams(); - senderChannelOpts = new ChannelOptions() {{ encrypted = true; cipherParams = params2; }}; - senderChannel.setOptions(senderChannelOpts); - - /* publish to the channel, wait, check message bad */ - messageWaiter.reset(); - senderChannel.publish("test_event", messageText, msgComplete); - messageWaiter.waitFor(1); - assertFalse( - "Received message is not correct", - messageText.equals(messageWaiter.receivedMessages.get(0).data) - ); - - /* See issue https://github.com/ably/ably-java/issues/202 - * This final part of the test fails intermittently. For now just try - * it multiple times. */ - for (int count = 4;; --count) { - assertTrue("Verify correct plaintext received", count != 0); - - /* set rx channel opts */ - receiverChannel.setOptions(senderChannelOpts); - - /* publish to the channel, wait, check message bad */ - messageWaiter.reset(); - senderChannel.publish("test_event", messageText, msgComplete); - messageWaiter.waitFor(1); - if (messageText.equals(messageWaiter.receivedMessages.get(0).data)) { - break; - } - } - - } catch (AblyException e) { - e.printStackTrace(); - fail("init0: Unexpected exception instantiating library"); - } finally { - if(sender != null) { - sender.close(); - } - if(receiver != null) { - receiver.close(); - } - } - } - - /** - * Test channel options creation from the cipher key. - * - * This test should be removed when we get rid of the methods - * ChannelOptions.fromCipherKey(...) which are deprecated and have - * been replaced with ChannelOptions.withCipherKey(...). - * @see >> 4]; - hexChars[j * 2 + 1] = HEX_ARRAY[v & 0x0F]; - } - return new String(hexChars); - } - - @Test - public void decodeAppleLibrarySequences() throws NoSuchAlgorithmException, AblyException { - final Map apple = new LinkedHashMap<>(); - final String appleKey = "0102030405060708090A0B0C0D0E0F101112131415161718191A1B1C1D1E1F20"; - final String appleIv = "100F0E0D0C0B0A090807060504030201"; - apple.put( - "01", - "100F0E0D0C0B0A090807060504030201C18B3B262A725C728E2089A9BB04E0C9"); - apple.put( - "0102", - "100F0E0D0C0B0A09080706050403020107FEEFB5103001C131166F6D3DB66143"); - apple.put( - "010203", - "100F0E0D0C0B0A0908070605040302018C9E6A8CBACA88F4AFC78132D0F194E3"); - apple.put( - "01020304", - "100F0E0D0C0B0A090807060504030201AB8C3A090FCB8CED353A621F76ABDB8A"); - apple.put( - "0102030405", - "100F0E0D0C0B0A09080706050403020199BF04E9DDF21E591FA4BB45E734F6BD"); - apple.put( - "010203040506", - "100F0E0D0C0B0A09080706050403020149C87F17C0DDAD95ED6BB5E985E628AD"); - apple.put( - "01020304050607", - "100F0E0D0C0B0A090807060504030201C9CBB2F122CA14A95AE8AE01FC817E84"); - apple.put( - "0102030405060708", - "100F0E0D0C0B0A090807060504030201C85B0A14C1C21512D82DA3AECCB3201A"); - apple.put( - "010203040506070809", - "100F0E0D0C0B0A09080706050403020155311B93A81FD9642034DE137E2CE98D"); - apple.put( - "0102030405060708090A", - "100F0E0D0C0B0A0908070605040302012D9DCACDE38301B77E2C51B72FE9F31B"); - apple.put( - "0102030405060708090A0B", - "100F0E0D0C0B0A090807060504030201265782DBDF11A2AD0DEB9F71231CA9BA"); - apple.put( - "0102030405060708090A0B0C", - "100F0E0D0C0B0A0908070605040302019C196DB8E04A3067939931351D015CAE"); - apple.put( - "0102030405060708090A0B0C0D", - "100F0E0D0C0B0A0908070605040302011BB9EFC492B650703761DAEFF97A1FC1"); - apple.put( - "0102030405060708090A0B0C0D0E", - "100F0E0D0C0B0A09080706050403020157CC4C876775E5FC7B57C8876CDC0CEA"); - apple.put( - "0102030405060708090A0B0C0D0E0F", - "100F0E0D0C0B0A09080706050403020103F86465C2295B868CBB3F98A5DE0DF0"); - apple.put( - "0102030405060708090A0B0C0D0E0F10", - "100F0E0D0C0B0A090807060504030201DD85C6780D7CBDFDAF8F5CE92EC1C389197EF97609BBE4B7D292AFE6511E9F21"); - apple.put( - "0102030405060708090A0B0C0D0E0F1011", - "100F0E0D0C0B0A090807060504030201DD85C6780D7CBDFDAF8F5CE92EC1C389CE964C8964215B27A7AE48DC056732F0"); - apple.put( - "0102030405060708090A0B0C0D0E0F101112", - "100F0E0D0C0B0A090807060504030201DD85C6780D7CBDFDAF8F5CE92EC1C3896D936723C7A5816CC024E08603527959"); - apple.put( - "0102030405060708090A0B0C0D0E0F10111213", - "100F0E0D0C0B0A090807060504030201DD85C6780D7CBDFDAF8F5CE92EC1C3899CA4B3CC6E8D8C6D8FFD0AD70BA7BC65"); - apple.put( - "0102030405060708090A0B0C0D0E0F1011121314", - "100F0E0D0C0B0A090807060504030201DD85C6780D7CBDFDAF8F5CE92EC1C389BEA4DE4B76525611799D65582BE3CE5B"); - apple.put( - "0102030405060708090A0B0C0D0E0F101112131415", - "100F0E0D0C0B0A090807060504030201DD85C6780D7CBDFDAF8F5CE92EC1C389DBC26994C71DCE7B068AF1A5B7202550"); - apple.put( - "0102030405060708090A0B0C0D0E0F10111213141516", - "100F0E0D0C0B0A090807060504030201DD85C6780D7CBDFDAF8F5CE92EC1C3891F8B3B76E9DDA4982B36456BDD40EF0E"); - apple.put( - "0102030405060708090A0B0C0D0E0F1011121314151617", - "100F0E0D0C0B0A090807060504030201DD85C6780D7CBDFDAF8F5CE92EC1C3892BC4022B166896A7CA9763E61EF458F1"); - apple.put( - "0102030405060708090A0B0C0D0E0F101112131415161718", - "100F0E0D0C0B0A090807060504030201DD85C6780D7CBDFDAF8F5CE92EC1C389D0D4F09B27A411EF48EA46185C9D6074"); - apple.put( - "0102030405060708090A0B0C0D0E0F10111213141516171819", - "100F0E0D0C0B0A090807060504030201DD85C6780D7CBDFDAF8F5CE92EC1C3897D1174ADCAA121457AC96C6C829962C8"); - apple.put( - "0102030405060708090A0B0C0D0E0F101112131415161718191A", - "100F0E0D0C0B0A090807060504030201DD85C6780D7CBDFDAF8F5CE92EC1C389E83AF24AC021FC94B4DA9606DB19D0D9"); - apple.put( - "0102030405060708090A0B0C0D0E0F101112131415161718191A1B", - "100F0E0D0C0B0A090807060504030201DD85C6780D7CBDFDAF8F5CE92EC1C3898FEFA4682FFABAFB448EFE75DB321BB6"); - apple.put( - "0102030405060708090A0B0C0D0E0F101112131415161718191A1B1C", - "100F0E0D0C0B0A090807060504030201DD85C6780D7CBDFDAF8F5CE92EC1C3899E3FBE0B68D8E19D40FBD9F081066C5E"); - apple.put( - "0102030405060708090A0B0C0D0E0F101112131415161718191A1B1C1D", - "100F0E0D0C0B0A090807060504030201DD85C6780D7CBDFDAF8F5CE92EC1C389F2D251D999ED1CABD6C76D74F8DE20BF"); - apple.put( - "0102030405060708090A0B0C0D0E0F101112131415161718191A1B1C1D1E", - "100F0E0D0C0B0A090807060504030201DD85C6780D7CBDFDAF8F5CE92EC1C389745AE302AD445421E2020E4C64698E41"); - apple.put( - "0102030405060708090A0B0C0D0E0F101112131415161718191A1B1C1D1E1F", - "100F0E0D0C0B0A090807060504030201DD85C6780D7CBDFDAF8F5CE92EC1C3892189CE8E1F0F2B7D3343510EEBB885A0"); - apple.put( - "0102030405060708090A0B0C0D0E0F101112131415161718191A1B1C1D1E1F20", - "100F0E0D0C0B0A090807060504030201DD85C6780D7CBDFDAF8F5CE92EC1C389AD84FAD57E8FAA692A75313E76FDEC522FBEF5D7912F605D4FC2D1F6FD0D2637"); - apple.put( - "0102030405060708090A0B0C0D0E0F101112131415161718191A1B1C1D1E1F2021", - "100F0E0D0C0B0A090807060504030201DD85C6780D7CBDFDAF8F5CE92EC1C389AD84FAD57E8FAA692A75313E76FDEC5285714982C410B3BF37C3489B56FAE2FB"); - apple.put( - "0102030405060708090A0B0C0D0E0F101112131415161718191A1B1C1D1E1F202122", - "100F0E0D0C0B0A090807060504030201DD85C6780D7CBDFDAF8F5CE92EC1C389AD84FAD57E8FAA692A75313E76FDEC5226BDBA1215FF3C3B4B34CA9A85BDCFD0"); - apple.put( - "0102030405060708090A0B0C0D0E0F101112131415161718191A1B1C1D1E1F20212223", - "100F0E0D0C0B0A090807060504030201DD85C6780D7CBDFDAF8F5CE92EC1C389AD84FAD57E8FAA692A75313E76FDEC5208978E6B0FC4444DED918DB73764CFED"); - apple.put( - "0102030405060708090A0B0C0D0E0F101112131415161718191A1B1C1D1E1F2021222324", - "100F0E0D0C0B0A090807060504030201DD85C6780D7CBDFDAF8F5CE92EC1C389AD84FAD57E8FAA692A75313E76FDEC52FFAC17521AA380BB9E7BAB2462C36610"); - apple.put( - "0102030405060708090A0B0C0D0E0F101112131415161718191A1B1C1D1E1F202122232425", - "100F0E0D0C0B0A090807060504030201DD85C6780D7CBDFDAF8F5CE92EC1C389AD84FAD57E8FAA692A75313E76FDEC5218639B13B3DBAB9F5E92F2F6CC4F7481"); - apple.put( - "0102030405060708090A0B0C0D0E0F101112131415161718191A1B1C1D1E1F20212223242526", - "100F0E0D0C0B0A090807060504030201DD85C6780D7CBDFDAF8F5CE92EC1C389AD84FAD57E8FAA692A75313E76FDEC522C4052C742B73009972E6BE6AE6753BD"); - apple.put( - "0102030405060708090A0B0C0D0E0F101112131415161718191A1B1C1D1E1F2021222324252627", - "100F0E0D0C0B0A090807060504030201DD85C6780D7CBDFDAF8F5CE92EC1C389AD84FAD57E8FAA692A75313E76FDEC52F4932F9E79A2EFB14F94A3343EC91F12"); - apple.put( - "0102030405060708090A0B0C0D0E0F101112131415161718191A1B1C1D1E1F202122232425262728", - "100F0E0D0C0B0A090807060504030201DD85C6780D7CBDFDAF8F5CE92EC1C389AD84FAD57E8FAA692A75313E76FDEC52A96F28E8526F4C544431407083DA3E0C"); - apple.put( - "0102030405060708090A0B0C0D0E0F101112131415161718191A1B1C1D1E1F20212223242526272829", - "100F0E0D0C0B0A090807060504030201DD85C6780D7CBDFDAF8F5CE92EC1C389AD84FAD57E8FAA692A75313E76FDEC5232DF3FD5AD555A870610EFBE89793E93"); - apple.put( - "0102030405060708090A0B0C0D0E0F101112131415161718191A1B1C1D1E1F202122232425262728292A", - "100F0E0D0C0B0A090807060504030201DD85C6780D7CBDFDAF8F5CE92EC1C389AD84FAD57E8FAA692A75313E76FDEC520217EB61E2F102A27423DCF79B93B2B2"); - apple.put( - "0102030405060708090A0B0C0D0E0F101112131415161718191A1B1C1D1E1F202122232425262728292A2B", - "100F0E0D0C0B0A090807060504030201DD85C6780D7CBDFDAF8F5CE92EC1C389AD84FAD57E8FAA692A75313E76FDEC52A5DD3DA740A00F9083D1BEDADB572A2E"); - apple.put( - "0102030405060708090A0B0C0D0E0F101112131415161718191A1B1C1D1E1F202122232425262728292A2B2C", - "100F0E0D0C0B0A090807060504030201DD85C6780D7CBDFDAF8F5CE92EC1C389AD84FAD57E8FAA692A75313E76FDEC5226A9DD99883A6A14CB758CEBD8C84E17"); - apple.put( - "0102030405060708090A0B0C0D0E0F101112131415161718191A1B1C1D1E1F202122232425262728292A2B2C2D", - "100F0E0D0C0B0A090807060504030201DD85C6780D7CBDFDAF8F5CE92EC1C389AD84FAD57E8FAA692A75313E76FDEC522E33DADC6A8B7D1BA2E8BE4BB6D683CF"); - apple.put( - "0102030405060708090A0B0C0D0E0F101112131415161718191A1B1C1D1E1F202122232425262728292A2B2C2D2E", - "100F0E0D0C0B0A090807060504030201DD85C6780D7CBDFDAF8F5CE92EC1C389AD84FAD57E8FAA692A75313E76FDEC527B1B18076B27418D6481AF3D8C0184BB"); - apple.put( - "0102030405060708090A0B0C0D0E0F101112131415161718191A1B1C1D1E1F202122232425262728292A2B2C2D2E2F", - "100F0E0D0C0B0A090807060504030201DD85C6780D7CBDFDAF8F5CE92EC1C389AD84FAD57E8FAA692A75313E76FDEC52C8CD727FBC61D7118E6994CD38FA207C"); - apple.put( - "0102030405060708090A0B0C0D0E0F101112131415161718191A1B1C1D1E1F202122232425262728292A2B2C2D2E2F30", - "100F0E0D0C0B0A090807060504030201DD85C6780D7CBDFDAF8F5CE92EC1C389AD84FAD57E8FAA692A75313E76FDEC5298511F43949DD4C7C6D4E4C767BE26D4FEADD56E96F84810035B7C7E47DEAAD8"); - apple.put( - "0102030405060708090A0B0C0D0E0F101112131415161718191A1B1C1D1E1F202122232425262728292A2B2C2D2E2F3031", - "100F0E0D0C0B0A090807060504030201DD85C6780D7CBDFDAF8F5CE92EC1C389AD84FAD57E8FAA692A75313E76FDEC5298511F43949DD4C7C6D4E4C767BE26D468AFE6CBF19B74BDE904BEC71C389E48"); - apple.put( - "0102030405060708090A0B0C0D0E0F101112131415161718191A1B1C1D1E1F202122232425262728292A2B2C2D2E2F303132", - "100F0E0D0C0B0A090807060504030201DD85C6780D7CBDFDAF8F5CE92EC1C389AD84FAD57E8FAA692A75313E76FDEC5298511F43949DD4C7C6D4E4C767BE26D482C21C40198174A4F284924DFED97B17"); - apple.put( - "0102030405060708090A0B0C0D0E0F101112131415161718191A1B1C1D1E1F202122232425262728292A2B2C2D2E2F30313233", - "100F0E0D0C0B0A090807060504030201DD85C6780D7CBDFDAF8F5CE92EC1C389AD84FAD57E8FAA692A75313E76FDEC5298511F43949DD4C7C6D4E4C767BE26D4781E4D3416BB1E63A1133FFD286BB908"); - apple.put( - "0102030405060708090A0B0C0D0E0F101112131415161718191A1B1C1D1E1F202122232425262728292A2B2C2D2E2F3031323334", - "100F0E0D0C0B0A090807060504030201DD85C6780D7CBDFDAF8F5CE92EC1C389AD84FAD57E8FAA692A75313E76FDEC5298511F43949DD4C7C6D4E4C767BE26D4923EE3CE4AFCA7DDCAF8D2C450067BFE"); - apple.put( - "0102030405060708090A0B0C0D0E0F101112131415161718191A1B1C1D1E1F202122232425262728292A2B2C2D2E2F303132333435", - "100F0E0D0C0B0A090807060504030201DD85C6780D7CBDFDAF8F5CE92EC1C389AD84FAD57E8FAA692A75313E76FDEC5298511F43949DD4C7C6D4E4C767BE26D4631E7238AF8181C5CE14EA55BCA51C72"); - apple.put( - "0102030405060708090A0B0C0D0E0F101112131415161718191A1B1C1D1E1F202122232425262728292A2B2C2D2E2F30313233343536", - "100F0E0D0C0B0A090807060504030201DD85C6780D7CBDFDAF8F5CE92EC1C389AD84FAD57E8FAA692A75313E76FDEC5298511F43949DD4C7C6D4E4C767BE26D4D17E8CD90B71309F8479DDD6053214DC"); - apple.put( - "0102030405060708090A0B0C0D0E0F101112131415161718191A1B1C1D1E1F202122232425262728292A2B2C2D2E2F3031323334353637", - "100F0E0D0C0B0A090807060504030201DD85C6780D7CBDFDAF8F5CE92EC1C389AD84FAD57E8FAA692A75313E76FDEC5298511F43949DD4C7C6D4E4C767BE26D468916A7D602693C416C2AB97DB3F6EDB"); - apple.put( - "0102030405060708090A0B0C0D0E0F101112131415161718191A1B1C1D1E1F202122232425262728292A2B2C2D2E2F303132333435363738", - "100F0E0D0C0B0A090807060504030201DD85C6780D7CBDFDAF8F5CE92EC1C389AD84FAD57E8FAA692A75313E76FDEC5298511F43949DD4C7C6D4E4C767BE26D41EA7274945614C3D4EB7850B495D3C61"); - apple.put( - "0102030405060708090A0B0C0D0E0F101112131415161718191A1B1C1D1E1F202122232425262728292A2B2C2D2E2F30313233343536373839", - "100F0E0D0C0B0A090807060504030201DD85C6780D7CBDFDAF8F5CE92EC1C389AD84FAD57E8FAA692A75313E76FDEC5298511F43949DD4C7C6D4E4C767BE26D4ED2DA843786063244F338B5C247A5FE2"); - apple.put( - "0102030405060708090A0B0C0D0E0F101112131415161718191A1B1C1D1E1F202122232425262728292A2B2C2D2E2F303132333435363738393A", - "100F0E0D0C0B0A090807060504030201DD85C6780D7CBDFDAF8F5CE92EC1C389AD84FAD57E8FAA692A75313E76FDEC5298511F43949DD4C7C6D4E4C767BE26D45A36064BA7C95C958F8437FA411FD34B"); - apple.put( - "0102030405060708090A0B0C0D0E0F101112131415161718191A1B1C1D1E1F202122232425262728292A2B2C2D2E2F303132333435363738393A3B", - "100F0E0D0C0B0A090807060504030201DD85C6780D7CBDFDAF8F5CE92EC1C389AD84FAD57E8FAA692A75313E76FDEC5298511F43949DD4C7C6D4E4C767BE26D43CA3FA4F14CAE59248D7E95AF4C96AF3"); - apple.put( - "0102030405060708090A0B0C0D0E0F101112131415161718191A1B1C1D1E1F202122232425262728292A2B2C2D2E2F303132333435363738393A3B3C", - "100F0E0D0C0B0A090807060504030201DD85C6780D7CBDFDAF8F5CE92EC1C389AD84FAD57E8FAA692A75313E76FDEC5298511F43949DD4C7C6D4E4C767BE26D4194011ACA9B17F0FBA3BAEB426A96B2C"); - apple.put( - "0102030405060708090A0B0C0D0E0F101112131415161718191A1B1C1D1E1F202122232425262728292A2B2C2D2E2F303132333435363738393A3B3C3D", - "100F0E0D0C0B0A090807060504030201DD85C6780D7CBDFDAF8F5CE92EC1C389AD84FAD57E8FAA692A75313E76FDEC5298511F43949DD4C7C6D4E4C767BE26D4637E86EA90E9027CE52AB54F95CA4DB2"); - apple.put( - "0102030405060708090A0B0C0D0E0F101112131415161718191A1B1C1D1E1F202122232425262728292A2B2C2D2E2F303132333435363738393A3B3C3D3E", - "100F0E0D0C0B0A090807060504030201DD85C6780D7CBDFDAF8F5CE92EC1C389AD84FAD57E8FAA692A75313E76FDEC5298511F43949DD4C7C6D4E4C767BE26D45DA5A687079A7374893816F9C83174D9"); - apple.put( - "0102030405060708090A0B0C0D0E0F101112131415161718191A1B1C1D1E1F202122232425262728292A2B2C2D2E2F303132333435363738393A3B3C3D3E3F", - "100F0E0D0C0B0A090807060504030201DD85C6780D7CBDFDAF8F5CE92EC1C389AD84FAD57E8FAA692A75313E76FDEC5298511F43949DD4C7C6D4E4C767BE26D429B7AF5929FF893290B55FAAF74F8C7D"); - apple.put( - "0102030405060708090A0B0C0D0E0F101112131415161718191A1B1C1D1E1F202122232425262728292A2B2C2D2E2F303132333435363738393A3B3C3D3E3F40", - "100F0E0D0C0B0A090807060504030201DD85C6780D7CBDFDAF8F5CE92EC1C389AD84FAD57E8FAA692A75313E76FDEC5298511F43949DD4C7C6D4E4C767BE26D4FECF4BE2FF20FFA4685CF25AA46C073DE5D5838F20613E79680D6E4380FE0A29"); - apple.put( - "0102030405060708090A0B0C0D0E0F101112131415161718191A1B1C1D1E1F202122232425262728292A2B2C2D2E2F303132333435363738393A3B3C3D3E3F4041", - "100F0E0D0C0B0A090807060504030201DD85C6780D7CBDFDAF8F5CE92EC1C389AD84FAD57E8FAA692A75313E76FDEC5298511F43949DD4C7C6D4E4C767BE26D4FECF4BE2FF20FFA4685CF25AA46C073DBF82C25ABBEE49629EE392B19F2B7EAB"); - apple.put( - "0102030405060708090A0B0C0D0E0F101112131415161718191A1B1C1D1E1F202122232425262728292A2B2C2D2E2F303132333435363738393A3B3C3D3E3F404142", - "100F0E0D0C0B0A090807060504030201DD85C6780D7CBDFDAF8F5CE92EC1C389AD84FAD57E8FAA692A75313E76FDEC5298511F43949DD4C7C6D4E4C767BE26D4FECF4BE2FF20FFA4685CF25AA46C073D020F3877B543C7A88489B985B1BA025D"); - apple.put( - "0102030405060708090A0B0C0D0E0F101112131415161718191A1B1C1D1E1F202122232425262728292A2B2C2D2E2F303132333435363738393A3B3C3D3E3F40414243", - "100F0E0D0C0B0A090807060504030201DD85C6780D7CBDFDAF8F5CE92EC1C389AD84FAD57E8FAA692A75313E76FDEC5298511F43949DD4C7C6D4E4C767BE26D4FECF4BE2FF20FFA4685CF25AA46C073D4309EF51D3C757D9FFAD71B00CD4BA8B"); - apple.put( - "0102030405060708090A0B0C0D0E0F101112131415161718191A1B1C1D1E1F202122232425262728292A2B2C2D2E2F303132333435363738393A3B3C3D3E3F4041424344", - "100F0E0D0C0B0A090807060504030201DD85C6780D7CBDFDAF8F5CE92EC1C389AD84FAD57E8FAA692A75313E76FDEC5298511F43949DD4C7C6D4E4C767BE26D4FECF4BE2FF20FFA4685CF25AA46C073D0AAE386BD16405B7EC84DD0A484A140E"); - apple.put( - "0102030405060708090A0B0C0D0E0F101112131415161718191A1B1C1D1E1F202122232425262728292A2B2C2D2E2F303132333435363738393A3B3C3D3E3F404142434445", - "100F0E0D0C0B0A090807060504030201DD85C6780D7CBDFDAF8F5CE92EC1C389AD84FAD57E8FAA692A75313E76FDEC5298511F43949DD4C7C6D4E4C767BE26D4FECF4BE2FF20FFA4685CF25AA46C073D2A085661E6EECDAD932CA3C709C4DD58"); - apple.put( - "0102030405060708090A0B0C0D0E0F101112131415161718191A1B1C1D1E1F202122232425262728292A2B2C2D2E2F303132333435363738393A3B3C3D3E3F40414243444546", - "100F0E0D0C0B0A090807060504030201DD85C6780D7CBDFDAF8F5CE92EC1C389AD84FAD57E8FAA692A75313E76FDEC5298511F43949DD4C7C6D4E4C767BE26D4FECF4BE2FF20FFA4685CF25AA46C073D341FFEFA178945C75BA2144F828F482B"); - - final byte[] key = hexStringToByteArray(appleKey); - final byte[] iv = hexStringToByteArray(appleIv); - - final CipherParams params = Crypto.getParams("aes", key, iv); - boolean failed = false; - for (final Entry entry : apple.entrySet()) { - // We have to create a new ChannelCipher for each message we encode because - // cipher instances only use the IV we've supplied via CipherParams for the - // encryption of the very first message. - final ChannelCipher cipher = Crypto.getCipher(new ChannelOptions() {{ encrypted=true; cipherParams=params; }}); - - final byte[] appleMessage = hexStringToByteArray(entry.getKey()); - final byte[] appleEncrypted = hexStringToByteArray(entry.getValue()); - final byte[] encrypted = cipher.encrypt(appleMessage); - final byte[] decrypted = cipher.decrypt(appleEncrypted); - - try { - assertArrayEquals(appleMessage, decrypted); - System.out.println("Decryption Success for length " + appleMessage.length + "."); - } catch (final AssertionError e) { - failed = true; - System.out.println("Decryption BAD for length " + appleMessage.length + ":" - + "\n\texpected: " + byteArrayToHexString(appleMessage) - + "\n\tproduced: " + byteArrayToHexString(decrypted)); - } - - try { - assertArrayEquals(appleEncrypted, encrypted); - System.out.println("Encryption Success for length " + appleMessage.length + "."); - } catch (final AssertionError e) { - failed = true; - System.out.println("Encryption BAD for length " + appleMessage.length + ":" - + "\n\texpected: " + byteArrayToHexString(appleEncrypted) - + "\n\tproduced: " + byteArrayToHexString(encrypted)); - } - - System.out.println(); - } - - assertFalse("At least one decryption or encryption operation failed. See output for details.", failed); - } - - private static final Random RANDOM = new Random(); - - private static byte[] generateNonce(final int size) { - final byte[] nonce = new byte[size]; - RANDOM.nextBytes(nonce); - return nonce; - } - - /** - * Test Crypto.generateRandomKey. - * @see RSE2 - */ - @Test - public void generate_random_key() { - final int numberOfRandomKeys = 50; - final int randomKeyBits = 256; - byte[][] randomKeys = new byte[numberOfRandomKeys][]; - - for (int i=0; i>> 4]; + hexChars[j * 2 + 1] = HEX_ARRAY[v & 0x0F]; + } + return new String(hexChars); + } + + @Test + public void decodeAppleLibrarySequences() throws NoSuchAlgorithmException, AblyException { + final Map apple = new LinkedHashMap<>(); + final String appleKey = "0102030405060708090A0B0C0D0E0F101112131415161718191A1B1C1D1E1F20"; + final String appleIv = "100F0E0D0C0B0A090807060504030201"; + apple.put( + "01", + "100F0E0D0C0B0A090807060504030201C18B3B262A725C728E2089A9BB04E0C9"); + apple.put( + "0102", + "100F0E0D0C0B0A09080706050403020107FEEFB5103001C131166F6D3DB66143"); + apple.put( + "010203", + "100F0E0D0C0B0A0908070605040302018C9E6A8CBACA88F4AFC78132D0F194E3"); + apple.put( + "01020304", + "100F0E0D0C0B0A090807060504030201AB8C3A090FCB8CED353A621F76ABDB8A"); + apple.put( + "0102030405", + "100F0E0D0C0B0A09080706050403020199BF04E9DDF21E591FA4BB45E734F6BD"); + apple.put( + "010203040506", + "100F0E0D0C0B0A09080706050403020149C87F17C0DDAD95ED6BB5E985E628AD"); + apple.put( + "01020304050607", + "100F0E0D0C0B0A090807060504030201C9CBB2F122CA14A95AE8AE01FC817E84"); + apple.put( + "0102030405060708", + "100F0E0D0C0B0A090807060504030201C85B0A14C1C21512D82DA3AECCB3201A"); + apple.put( + "010203040506070809", + "100F0E0D0C0B0A09080706050403020155311B93A81FD9642034DE137E2CE98D"); + apple.put( + "0102030405060708090A", + "100F0E0D0C0B0A0908070605040302012D9DCACDE38301B77E2C51B72FE9F31B"); + apple.put( + "0102030405060708090A0B", + "100F0E0D0C0B0A090807060504030201265782DBDF11A2AD0DEB9F71231CA9BA"); + apple.put( + "0102030405060708090A0B0C", + "100F0E0D0C0B0A0908070605040302019C196DB8E04A3067939931351D015CAE"); + apple.put( + "0102030405060708090A0B0C0D", + "100F0E0D0C0B0A0908070605040302011BB9EFC492B650703761DAEFF97A1FC1"); + apple.put( + "0102030405060708090A0B0C0D0E", + "100F0E0D0C0B0A09080706050403020157CC4C876775E5FC7B57C8876CDC0CEA"); + apple.put( + "0102030405060708090A0B0C0D0E0F", + "100F0E0D0C0B0A09080706050403020103F86465C2295B868CBB3F98A5DE0DF0"); + apple.put( + "0102030405060708090A0B0C0D0E0F10", + "100F0E0D0C0B0A090807060504030201DD85C6780D7CBDFDAF8F5CE92EC1C389197EF97609BBE4B7D292AFE6511E9F21"); + apple.put( + "0102030405060708090A0B0C0D0E0F1011", + "100F0E0D0C0B0A090807060504030201DD85C6780D7CBDFDAF8F5CE92EC1C389CE964C8964215B27A7AE48DC056732F0"); + apple.put( + "0102030405060708090A0B0C0D0E0F101112", + "100F0E0D0C0B0A090807060504030201DD85C6780D7CBDFDAF8F5CE92EC1C3896D936723C7A5816CC024E08603527959"); + apple.put( + "0102030405060708090A0B0C0D0E0F10111213", + "100F0E0D0C0B0A090807060504030201DD85C6780D7CBDFDAF8F5CE92EC1C3899CA4B3CC6E8D8C6D8FFD0AD70BA7BC65"); + apple.put( + "0102030405060708090A0B0C0D0E0F1011121314", + "100F0E0D0C0B0A090807060504030201DD85C6780D7CBDFDAF8F5CE92EC1C389BEA4DE4B76525611799D65582BE3CE5B"); + apple.put( + "0102030405060708090A0B0C0D0E0F101112131415", + "100F0E0D0C0B0A090807060504030201DD85C6780D7CBDFDAF8F5CE92EC1C389DBC26994C71DCE7B068AF1A5B7202550"); + apple.put( + "0102030405060708090A0B0C0D0E0F10111213141516", + "100F0E0D0C0B0A090807060504030201DD85C6780D7CBDFDAF8F5CE92EC1C3891F8B3B76E9DDA4982B36456BDD40EF0E"); + apple.put( + "0102030405060708090A0B0C0D0E0F1011121314151617", + "100F0E0D0C0B0A090807060504030201DD85C6780D7CBDFDAF8F5CE92EC1C3892BC4022B166896A7CA9763E61EF458F1"); + apple.put( + "0102030405060708090A0B0C0D0E0F101112131415161718", + "100F0E0D0C0B0A090807060504030201DD85C6780D7CBDFDAF8F5CE92EC1C389D0D4F09B27A411EF48EA46185C9D6074"); + apple.put( + "0102030405060708090A0B0C0D0E0F10111213141516171819", + "100F0E0D0C0B0A090807060504030201DD85C6780D7CBDFDAF8F5CE92EC1C3897D1174ADCAA121457AC96C6C829962C8"); + apple.put( + "0102030405060708090A0B0C0D0E0F101112131415161718191A", + "100F0E0D0C0B0A090807060504030201DD85C6780D7CBDFDAF8F5CE92EC1C389E83AF24AC021FC94B4DA9606DB19D0D9"); + apple.put( + "0102030405060708090A0B0C0D0E0F101112131415161718191A1B", + "100F0E0D0C0B0A090807060504030201DD85C6780D7CBDFDAF8F5CE92EC1C3898FEFA4682FFABAFB448EFE75DB321BB6"); + apple.put( + "0102030405060708090A0B0C0D0E0F101112131415161718191A1B1C", + "100F0E0D0C0B0A090807060504030201DD85C6780D7CBDFDAF8F5CE92EC1C3899E3FBE0B68D8E19D40FBD9F081066C5E"); + apple.put( + "0102030405060708090A0B0C0D0E0F101112131415161718191A1B1C1D", + "100F0E0D0C0B0A090807060504030201DD85C6780D7CBDFDAF8F5CE92EC1C389F2D251D999ED1CABD6C76D74F8DE20BF"); + apple.put( + "0102030405060708090A0B0C0D0E0F101112131415161718191A1B1C1D1E", + "100F0E0D0C0B0A090807060504030201DD85C6780D7CBDFDAF8F5CE92EC1C389745AE302AD445421E2020E4C64698E41"); + apple.put( + "0102030405060708090A0B0C0D0E0F101112131415161718191A1B1C1D1E1F", + "100F0E0D0C0B0A090807060504030201DD85C6780D7CBDFDAF8F5CE92EC1C3892189CE8E1F0F2B7D3343510EEBB885A0"); + apple.put( + "0102030405060708090A0B0C0D0E0F101112131415161718191A1B1C1D1E1F20", + "100F0E0D0C0B0A090807060504030201DD85C6780D7CBDFDAF8F5CE92EC1C389AD84FAD57E8FAA692A75313E76FDEC522FBEF5D7912F605D4FC2D1F6FD0D2637"); + apple.put( + "0102030405060708090A0B0C0D0E0F101112131415161718191A1B1C1D1E1F2021", + "100F0E0D0C0B0A090807060504030201DD85C6780D7CBDFDAF8F5CE92EC1C389AD84FAD57E8FAA692A75313E76FDEC5285714982C410B3BF37C3489B56FAE2FB"); + apple.put( + "0102030405060708090A0B0C0D0E0F101112131415161718191A1B1C1D1E1F202122", + "100F0E0D0C0B0A090807060504030201DD85C6780D7CBDFDAF8F5CE92EC1C389AD84FAD57E8FAA692A75313E76FDEC5226BDBA1215FF3C3B4B34CA9A85BDCFD0"); + apple.put( + "0102030405060708090A0B0C0D0E0F101112131415161718191A1B1C1D1E1F20212223", + "100F0E0D0C0B0A090807060504030201DD85C6780D7CBDFDAF8F5CE92EC1C389AD84FAD57E8FAA692A75313E76FDEC5208978E6B0FC4444DED918DB73764CFED"); + apple.put( + "0102030405060708090A0B0C0D0E0F101112131415161718191A1B1C1D1E1F2021222324", + "100F0E0D0C0B0A090807060504030201DD85C6780D7CBDFDAF8F5CE92EC1C389AD84FAD57E8FAA692A75313E76FDEC52FFAC17521AA380BB9E7BAB2462C36610"); + apple.put( + "0102030405060708090A0B0C0D0E0F101112131415161718191A1B1C1D1E1F202122232425", + "100F0E0D0C0B0A090807060504030201DD85C6780D7CBDFDAF8F5CE92EC1C389AD84FAD57E8FAA692A75313E76FDEC5218639B13B3DBAB9F5E92F2F6CC4F7481"); + apple.put( + "0102030405060708090A0B0C0D0E0F101112131415161718191A1B1C1D1E1F20212223242526", + "100F0E0D0C0B0A090807060504030201DD85C6780D7CBDFDAF8F5CE92EC1C389AD84FAD57E8FAA692A75313E76FDEC522C4052C742B73009972E6BE6AE6753BD"); + apple.put( + "0102030405060708090A0B0C0D0E0F101112131415161718191A1B1C1D1E1F2021222324252627", + "100F0E0D0C0B0A090807060504030201DD85C6780D7CBDFDAF8F5CE92EC1C389AD84FAD57E8FAA692A75313E76FDEC52F4932F9E79A2EFB14F94A3343EC91F12"); + apple.put( + "0102030405060708090A0B0C0D0E0F101112131415161718191A1B1C1D1E1F202122232425262728", + "100F0E0D0C0B0A090807060504030201DD85C6780D7CBDFDAF8F5CE92EC1C389AD84FAD57E8FAA692A75313E76FDEC52A96F28E8526F4C544431407083DA3E0C"); + apple.put( + "0102030405060708090A0B0C0D0E0F101112131415161718191A1B1C1D1E1F20212223242526272829", + "100F0E0D0C0B0A090807060504030201DD85C6780D7CBDFDAF8F5CE92EC1C389AD84FAD57E8FAA692A75313E76FDEC5232DF3FD5AD555A870610EFBE89793E93"); + apple.put( + "0102030405060708090A0B0C0D0E0F101112131415161718191A1B1C1D1E1F202122232425262728292A", + "100F0E0D0C0B0A090807060504030201DD85C6780D7CBDFDAF8F5CE92EC1C389AD84FAD57E8FAA692A75313E76FDEC520217EB61E2F102A27423DCF79B93B2B2"); + apple.put( + "0102030405060708090A0B0C0D0E0F101112131415161718191A1B1C1D1E1F202122232425262728292A2B", + "100F0E0D0C0B0A090807060504030201DD85C6780D7CBDFDAF8F5CE92EC1C389AD84FAD57E8FAA692A75313E76FDEC52A5DD3DA740A00F9083D1BEDADB572A2E"); + apple.put( + "0102030405060708090A0B0C0D0E0F101112131415161718191A1B1C1D1E1F202122232425262728292A2B2C", + "100F0E0D0C0B0A090807060504030201DD85C6780D7CBDFDAF8F5CE92EC1C389AD84FAD57E8FAA692A75313E76FDEC5226A9DD99883A6A14CB758CEBD8C84E17"); + apple.put( + "0102030405060708090A0B0C0D0E0F101112131415161718191A1B1C1D1E1F202122232425262728292A2B2C2D", + "100F0E0D0C0B0A090807060504030201DD85C6780D7CBDFDAF8F5CE92EC1C389AD84FAD57E8FAA692A75313E76FDEC522E33DADC6A8B7D1BA2E8BE4BB6D683CF"); + apple.put( + "0102030405060708090A0B0C0D0E0F101112131415161718191A1B1C1D1E1F202122232425262728292A2B2C2D2E", + "100F0E0D0C0B0A090807060504030201DD85C6780D7CBDFDAF8F5CE92EC1C389AD84FAD57E8FAA692A75313E76FDEC527B1B18076B27418D6481AF3D8C0184BB"); + apple.put( + "0102030405060708090A0B0C0D0E0F101112131415161718191A1B1C1D1E1F202122232425262728292A2B2C2D2E2F", + "100F0E0D0C0B0A090807060504030201DD85C6780D7CBDFDAF8F5CE92EC1C389AD84FAD57E8FAA692A75313E76FDEC52C8CD727FBC61D7118E6994CD38FA207C"); + apple.put( + "0102030405060708090A0B0C0D0E0F101112131415161718191A1B1C1D1E1F202122232425262728292A2B2C2D2E2F30", + "100F0E0D0C0B0A090807060504030201DD85C6780D7CBDFDAF8F5CE92EC1C389AD84FAD57E8FAA692A75313E76FDEC5298511F43949DD4C7C6D4E4C767BE26D4FEADD56E96F84810035B7C7E47DEAAD8"); + apple.put( + "0102030405060708090A0B0C0D0E0F101112131415161718191A1B1C1D1E1F202122232425262728292A2B2C2D2E2F3031", + "100F0E0D0C0B0A090807060504030201DD85C6780D7CBDFDAF8F5CE92EC1C389AD84FAD57E8FAA692A75313E76FDEC5298511F43949DD4C7C6D4E4C767BE26D468AFE6CBF19B74BDE904BEC71C389E48"); + apple.put( + "0102030405060708090A0B0C0D0E0F101112131415161718191A1B1C1D1E1F202122232425262728292A2B2C2D2E2F303132", + "100F0E0D0C0B0A090807060504030201DD85C6780D7CBDFDAF8F5CE92EC1C389AD84FAD57E8FAA692A75313E76FDEC5298511F43949DD4C7C6D4E4C767BE26D482C21C40198174A4F284924DFED97B17"); + apple.put( + "0102030405060708090A0B0C0D0E0F101112131415161718191A1B1C1D1E1F202122232425262728292A2B2C2D2E2F30313233", + "100F0E0D0C0B0A090807060504030201DD85C6780D7CBDFDAF8F5CE92EC1C389AD84FAD57E8FAA692A75313E76FDEC5298511F43949DD4C7C6D4E4C767BE26D4781E4D3416BB1E63A1133FFD286BB908"); + apple.put( + "0102030405060708090A0B0C0D0E0F101112131415161718191A1B1C1D1E1F202122232425262728292A2B2C2D2E2F3031323334", + "100F0E0D0C0B0A090807060504030201DD85C6780D7CBDFDAF8F5CE92EC1C389AD84FAD57E8FAA692A75313E76FDEC5298511F43949DD4C7C6D4E4C767BE26D4923EE3CE4AFCA7DDCAF8D2C450067BFE"); + apple.put( + "0102030405060708090A0B0C0D0E0F101112131415161718191A1B1C1D1E1F202122232425262728292A2B2C2D2E2F303132333435", + "100F0E0D0C0B0A090807060504030201DD85C6780D7CBDFDAF8F5CE92EC1C389AD84FAD57E8FAA692A75313E76FDEC5298511F43949DD4C7C6D4E4C767BE26D4631E7238AF8181C5CE14EA55BCA51C72"); + apple.put( + "0102030405060708090A0B0C0D0E0F101112131415161718191A1B1C1D1E1F202122232425262728292A2B2C2D2E2F30313233343536", + "100F0E0D0C0B0A090807060504030201DD85C6780D7CBDFDAF8F5CE92EC1C389AD84FAD57E8FAA692A75313E76FDEC5298511F43949DD4C7C6D4E4C767BE26D4D17E8CD90B71309F8479DDD6053214DC"); + apple.put( + "0102030405060708090A0B0C0D0E0F101112131415161718191A1B1C1D1E1F202122232425262728292A2B2C2D2E2F3031323334353637", + "100F0E0D0C0B0A090807060504030201DD85C6780D7CBDFDAF8F5CE92EC1C389AD84FAD57E8FAA692A75313E76FDEC5298511F43949DD4C7C6D4E4C767BE26D468916A7D602693C416C2AB97DB3F6EDB"); + apple.put( + "0102030405060708090A0B0C0D0E0F101112131415161718191A1B1C1D1E1F202122232425262728292A2B2C2D2E2F303132333435363738", + "100F0E0D0C0B0A090807060504030201DD85C6780D7CBDFDAF8F5CE92EC1C389AD84FAD57E8FAA692A75313E76FDEC5298511F43949DD4C7C6D4E4C767BE26D41EA7274945614C3D4EB7850B495D3C61"); + apple.put( + "0102030405060708090A0B0C0D0E0F101112131415161718191A1B1C1D1E1F202122232425262728292A2B2C2D2E2F30313233343536373839", + "100F0E0D0C0B0A090807060504030201DD85C6780D7CBDFDAF8F5CE92EC1C389AD84FAD57E8FAA692A75313E76FDEC5298511F43949DD4C7C6D4E4C767BE26D4ED2DA843786063244F338B5C247A5FE2"); + apple.put( + "0102030405060708090A0B0C0D0E0F101112131415161718191A1B1C1D1E1F202122232425262728292A2B2C2D2E2F303132333435363738393A", + "100F0E0D0C0B0A090807060504030201DD85C6780D7CBDFDAF8F5CE92EC1C389AD84FAD57E8FAA692A75313E76FDEC5298511F43949DD4C7C6D4E4C767BE26D45A36064BA7C95C958F8437FA411FD34B"); + apple.put( + "0102030405060708090A0B0C0D0E0F101112131415161718191A1B1C1D1E1F202122232425262728292A2B2C2D2E2F303132333435363738393A3B", + "100F0E0D0C0B0A090807060504030201DD85C6780D7CBDFDAF8F5CE92EC1C389AD84FAD57E8FAA692A75313E76FDEC5298511F43949DD4C7C6D4E4C767BE26D43CA3FA4F14CAE59248D7E95AF4C96AF3"); + apple.put( + "0102030405060708090A0B0C0D0E0F101112131415161718191A1B1C1D1E1F202122232425262728292A2B2C2D2E2F303132333435363738393A3B3C", + "100F0E0D0C0B0A090807060504030201DD85C6780D7CBDFDAF8F5CE92EC1C389AD84FAD57E8FAA692A75313E76FDEC5298511F43949DD4C7C6D4E4C767BE26D4194011ACA9B17F0FBA3BAEB426A96B2C"); + apple.put( + "0102030405060708090A0B0C0D0E0F101112131415161718191A1B1C1D1E1F202122232425262728292A2B2C2D2E2F303132333435363738393A3B3C3D", + "100F0E0D0C0B0A090807060504030201DD85C6780D7CBDFDAF8F5CE92EC1C389AD84FAD57E8FAA692A75313E76FDEC5298511F43949DD4C7C6D4E4C767BE26D4637E86EA90E9027CE52AB54F95CA4DB2"); + apple.put( + "0102030405060708090A0B0C0D0E0F101112131415161718191A1B1C1D1E1F202122232425262728292A2B2C2D2E2F303132333435363738393A3B3C3D3E", + "100F0E0D0C0B0A090807060504030201DD85C6780D7CBDFDAF8F5CE92EC1C389AD84FAD57E8FAA692A75313E76FDEC5298511F43949DD4C7C6D4E4C767BE26D45DA5A687079A7374893816F9C83174D9"); + apple.put( + "0102030405060708090A0B0C0D0E0F101112131415161718191A1B1C1D1E1F202122232425262728292A2B2C2D2E2F303132333435363738393A3B3C3D3E3F", + "100F0E0D0C0B0A090807060504030201DD85C6780D7CBDFDAF8F5CE92EC1C389AD84FAD57E8FAA692A75313E76FDEC5298511F43949DD4C7C6D4E4C767BE26D429B7AF5929FF893290B55FAAF74F8C7D"); + apple.put( + "0102030405060708090A0B0C0D0E0F101112131415161718191A1B1C1D1E1F202122232425262728292A2B2C2D2E2F303132333435363738393A3B3C3D3E3F40", + "100F0E0D0C0B0A090807060504030201DD85C6780D7CBDFDAF8F5CE92EC1C389AD84FAD57E8FAA692A75313E76FDEC5298511F43949DD4C7C6D4E4C767BE26D4FECF4BE2FF20FFA4685CF25AA46C073DE5D5838F20613E79680D6E4380FE0A29"); + apple.put( + "0102030405060708090A0B0C0D0E0F101112131415161718191A1B1C1D1E1F202122232425262728292A2B2C2D2E2F303132333435363738393A3B3C3D3E3F4041", + "100F0E0D0C0B0A090807060504030201DD85C6780D7CBDFDAF8F5CE92EC1C389AD84FAD57E8FAA692A75313E76FDEC5298511F43949DD4C7C6D4E4C767BE26D4FECF4BE2FF20FFA4685CF25AA46C073DBF82C25ABBEE49629EE392B19F2B7EAB"); + apple.put( + "0102030405060708090A0B0C0D0E0F101112131415161718191A1B1C1D1E1F202122232425262728292A2B2C2D2E2F303132333435363738393A3B3C3D3E3F404142", + "100F0E0D0C0B0A090807060504030201DD85C6780D7CBDFDAF8F5CE92EC1C389AD84FAD57E8FAA692A75313E76FDEC5298511F43949DD4C7C6D4E4C767BE26D4FECF4BE2FF20FFA4685CF25AA46C073D020F3877B543C7A88489B985B1BA025D"); + apple.put( + "0102030405060708090A0B0C0D0E0F101112131415161718191A1B1C1D1E1F202122232425262728292A2B2C2D2E2F303132333435363738393A3B3C3D3E3F40414243", + "100F0E0D0C0B0A090807060504030201DD85C6780D7CBDFDAF8F5CE92EC1C389AD84FAD57E8FAA692A75313E76FDEC5298511F43949DD4C7C6D4E4C767BE26D4FECF4BE2FF20FFA4685CF25AA46C073D4309EF51D3C757D9FFAD71B00CD4BA8B"); + apple.put( + "0102030405060708090A0B0C0D0E0F101112131415161718191A1B1C1D1E1F202122232425262728292A2B2C2D2E2F303132333435363738393A3B3C3D3E3F4041424344", + "100F0E0D0C0B0A090807060504030201DD85C6780D7CBDFDAF8F5CE92EC1C389AD84FAD57E8FAA692A75313E76FDEC5298511F43949DD4C7C6D4E4C767BE26D4FECF4BE2FF20FFA4685CF25AA46C073D0AAE386BD16405B7EC84DD0A484A140E"); + apple.put( + "0102030405060708090A0B0C0D0E0F101112131415161718191A1B1C1D1E1F202122232425262728292A2B2C2D2E2F303132333435363738393A3B3C3D3E3F404142434445", + "100F0E0D0C0B0A090807060504030201DD85C6780D7CBDFDAF8F5CE92EC1C389AD84FAD57E8FAA692A75313E76FDEC5298511F43949DD4C7C6D4E4C767BE26D4FECF4BE2FF20FFA4685CF25AA46C073D2A085661E6EECDAD932CA3C709C4DD58"); + apple.put( + "0102030405060708090A0B0C0D0E0F101112131415161718191A1B1C1D1E1F202122232425262728292A2B2C2D2E2F303132333435363738393A3B3C3D3E3F40414243444546", + "100F0E0D0C0B0A090807060504030201DD85C6780D7CBDFDAF8F5CE92EC1C389AD84FAD57E8FAA692A75313E76FDEC5298511F43949DD4C7C6D4E4C767BE26D4FECF4BE2FF20FFA4685CF25AA46C073D341FFEFA178945C75BA2144F828F482B"); + + final byte[] key = hexStringToByteArray(appleKey); + final byte[] iv = hexStringToByteArray(appleIv); + + final CipherParams params = Crypto.getParams("aes", key, iv); + boolean failed = false; + for (final Entry entry : apple.entrySet()) { + // We have to create a new ChannelCipher for each message we encode because + // cipher instances only use the IV we've supplied via CipherParams for the + // encryption of the very first message. + final ChannelCipher cipher = Crypto.getCipher(new ChannelOptions() {{ encrypted=true; cipherParams=params; }}); + + final byte[] appleMessage = hexStringToByteArray(entry.getKey()); + final byte[] appleEncrypted = hexStringToByteArray(entry.getValue()); + final byte[] encrypted = cipher.encrypt(appleMessage); + final byte[] decrypted = cipher.decrypt(appleEncrypted); + + try { + assertArrayEquals(appleMessage, decrypted); + System.out.println("Decryption Success for length " + appleMessage.length + "."); + } catch (final AssertionError e) { + failed = true; + System.out.println("Decryption BAD for length " + appleMessage.length + ":" + + "\n\texpected: " + byteArrayToHexString(appleMessage) + + "\n\tproduced: " + byteArrayToHexString(decrypted)); + } + + try { + assertArrayEquals(appleEncrypted, encrypted); + System.out.println("Encryption Success for length " + appleMessage.length + "."); + } catch (final AssertionError e) { + failed = true; + System.out.println("Encryption BAD for length " + appleMessage.length + ":" + + "\n\texpected: " + byteArrayToHexString(appleEncrypted) + + "\n\tproduced: " + byteArrayToHexString(encrypted)); + } + + System.out.println(); + } + + assertFalse("At least one decryption or encryption operation failed. See output for details.", failed); + } + + private static final Random RANDOM = new Random(); + + private static byte[] generateNonce(final int size) { + final byte[] nonce = new byte[size]; + RANDOM.nextBytes(nonce); + return nonce; + } + + /** + * Test Crypto.generateRandomKey. + * @see RSE2 + */ + @Test + public void generate_random_key() { + final int numberOfRandomKeys = 50; + final int randomKeyBits = 256; + byte[][] randomKeys = new byte[numberOfRandomKeys][]; + + for (int i=0; i> requestParameters = null; - for (int i = 0; requestParameters == null && i<10; i++) { - try { Thread.sleep(100); } catch (InterruptedException e) {} - requestParameters = server.getRequestParameters(); - } - realtime.close(); - - assertNotNull("Verify connection attempt", requestParameters); - - /* Spec RTN2e */ - assertEquals("Verify correct key param", requestParameters.get("key"), - Collections.singletonList(key)); - - /* Spec RTN2f - * This test should not directly validate version against Defaults.ABLY_VERSION, nor - * Defaults.ABLY_VERSION_PARAM, as ultimately the request param has been derived from those values. - */ - assertEquals("Verify correct version", requestParameters.get("v"), - Collections.singletonList("1.2")); - - /* Spec RTN2g - * This test should not directly validate version against Defaults.ABLY_LIB_VERSION, nor - * Defaults.ABLY_LIB_PARAM, as ultimately the request param has been derived from those values. - */ - assertEquals("Verify correct lib version", requestParameters.get("lib"), - Collections.singletonList("java-1.2.2")); - - /* Spec RTN2a */ - assertEquals("Verify correct format", requestParameters.get("format"), - Collections.singletonList(testParams.useBinaryProtocol ? "msgpack" : "json")); - - /* test echo option */ - opts = new ClientOptions(key); - opts.port = port; - opts.realtimeHost = "localhost"; - opts.tls = false; - opts.useBinaryProtocol = testParams.useBinaryProtocol; - opts.echoMessages = false; - server.resetRequestParameters(); - realtime = new AblyRealtime(opts); - requestParameters = null; - for (int i = 0; requestParameters == null && i<10; i++) { - try { Thread.sleep(100); } catch (InterruptedException e) {} - requestParameters = server.getRequestParameters(); - } - realtime.close(); - - assertNotNull("Verify connection attempt", requestParameters); - - /* Spec: RTN2b */ - assertEquals("Verify correct echo param", requestParameters.get("echo"), - Collections.singletonList("false")); - - /* test token auth option */ - String clientId = "test client id"; - opts = new ClientOptions(); - opts.port = port; - opts.realtimeHost = "localhost"; - opts.tls = false; - opts.useBinaryProtocol = testParams.useBinaryProtocol; - opts.useTokenAuth = true; - opts.token = key; /* not really a token, but ok for this test */ - opts.clientId = clientId; - - server.resetRequestParameters(); - realtime = new AblyRealtime(opts); - requestParameters = null; - for (int i = 0; requestParameters == null && i<10; i++) { - try { Thread.sleep(100); } catch (InterruptedException e) {} - requestParameters = server.getRequestParameters(); - } - realtime.close(); - - assertNotNull("Verify connection attempt", requestParameters); - - /* Spec: RTN2d */ - assertEquals("Verify correct clientId param", requestParameters.get("clientId"), - Collections.singletonList(clientId)); - - /* Spec: RTN2e */ - assertEquals("Verify correct accessToken param", requestParameters.get("accessToken"), - Collections.singletonList(key)); - - } catch (AblyException e) { - e.printStackTrace(); - Assert.fail("websocket_http_header_test: Unexpected exception"); - } finally { - if (realtime != null) - realtime.close(); - } - } - - private static class SessionHandlerNanoHTTPD extends NanoHTTPD { - Map> requestParameters; - - SessionHandlerNanoHTTPD(int port) { - super(port); - } - - @Override - public Response serve(IHTTPSession session) { - if (requestParameters == null) - requestParameters = decodeParameters(session.getQueryParameterString()); - - return newFixedLengthResponse("Ignored response"); - } - - void resetRequestParameters() { - requestParameters = null; - } - - Map> getRequestParameters() { - return requestParameters; - } - } + private SessionHandlerNanoHTTPD server; + private int port; + + @Before + public void setUp() throws IOException { + /* Create custom RouterNanoHTTPD class for getting session object */ + port = testParams.useBinaryProtocol ? 27333 : 27332; + server = new SessionHandlerNanoHTTPD(port); + server.start(NanoHTTPD.SOCKET_READ_TIMEOUT, true); + + while (!server.wasStarted()) { + try { + Thread.sleep(100); + } catch (InterruptedException e) { + e.printStackTrace(); + } + } + } + + @After + public void tearDown() { + server.stop(); + } + + /** + * Verify that correct version is used for realtime HTTP request + */ + @Test + public void realtime_websocket_param_test() { + AblyRealtime realtime = null; + try { + /* Init values for local server */ + String key = testVars.keys[0].keyStr; + ClientOptions opts = new ClientOptions(key); + opts.port = port; + opts.realtimeHost = "localhost"; + opts.tls = false; + opts.useBinaryProtocol = testParams.useBinaryProtocol; + + server.resetRequestParameters(); + realtime = new AblyRealtime(opts); + Map> requestParameters = null; + for (int i = 0; requestParameters == null && i<10; i++) { + try { Thread.sleep(100); } catch (InterruptedException e) {} + requestParameters = server.getRequestParameters(); + } + realtime.close(); + + assertNotNull("Verify connection attempt", requestParameters); + + /* Spec RTN2e */ + assertEquals("Verify correct key param", requestParameters.get("key"), + Collections.singletonList(key)); + + /* Spec RTN2f + * This test should not directly validate version against Defaults.ABLY_VERSION, nor + * Defaults.ABLY_VERSION_PARAM, as ultimately the request param has been derived from those values. + */ + assertEquals("Verify correct version", requestParameters.get("v"), + Collections.singletonList("1.2")); + + /* Spec RTN2g + * This test should not directly validate version against Defaults.ABLY_LIB_VERSION, nor + * Defaults.ABLY_LIB_PARAM, as ultimately the request param has been derived from those values. + */ + assertEquals("Verify correct lib version", requestParameters.get("lib"), + Collections.singletonList("java-1.2.2")); + + /* Spec RTN2a */ + assertEquals("Verify correct format", requestParameters.get("format"), + Collections.singletonList(testParams.useBinaryProtocol ? "msgpack" : "json")); + + /* test echo option */ + opts = new ClientOptions(key); + opts.port = port; + opts.realtimeHost = "localhost"; + opts.tls = false; + opts.useBinaryProtocol = testParams.useBinaryProtocol; + opts.echoMessages = false; + server.resetRequestParameters(); + realtime = new AblyRealtime(opts); + requestParameters = null; + for (int i = 0; requestParameters == null && i<10; i++) { + try { Thread.sleep(100); } catch (InterruptedException e) {} + requestParameters = server.getRequestParameters(); + } + realtime.close(); + + assertNotNull("Verify connection attempt", requestParameters); + + /* Spec: RTN2b */ + assertEquals("Verify correct echo param", requestParameters.get("echo"), + Collections.singletonList("false")); + + /* test token auth option */ + String clientId = "test client id"; + opts = new ClientOptions(); + opts.port = port; + opts.realtimeHost = "localhost"; + opts.tls = false; + opts.useBinaryProtocol = testParams.useBinaryProtocol; + opts.useTokenAuth = true; + opts.token = key; /* not really a token, but ok for this test */ + opts.clientId = clientId; + + server.resetRequestParameters(); + realtime = new AblyRealtime(opts); + requestParameters = null; + for (int i = 0; requestParameters == null && i<10; i++) { + try { Thread.sleep(100); } catch (InterruptedException e) {} + requestParameters = server.getRequestParameters(); + } + realtime.close(); + + assertNotNull("Verify connection attempt", requestParameters); + + /* Spec: RTN2d */ + assertEquals("Verify correct clientId param", requestParameters.get("clientId"), + Collections.singletonList(clientId)); + + /* Spec: RTN2e */ + assertEquals("Verify correct accessToken param", requestParameters.get("accessToken"), + Collections.singletonList(key)); + + } catch (AblyException e) { + e.printStackTrace(); + Assert.fail("websocket_http_header_test: Unexpected exception"); + } finally { + if (realtime != null) + realtime.close(); + } + } + + private static class SessionHandlerNanoHTTPD extends NanoHTTPD { + Map> requestParameters; + + SessionHandlerNanoHTTPD(int port) { + super(port); + } + + @Override + public Response serve(IHTTPSession session) { + if (requestParameters == null) + requestParameters = decodeParameters(session.getQueryParameterString()); + + return newFixedLengthResponse("Ignored response"); + } + + void resetRequestParameters() { + requestParameters = null; + } + + Map> getRequestParameters() { + return requestParameters; + } + } } diff --git a/lib/src/test/java/io/ably/lib/test/realtime/RealtimeInitTest.java b/lib/src/test/java/io/ably/lib/test/realtime/RealtimeInitTest.java index d583af80b..541537bfa 100644 --- a/lib/src/test/java/io/ably/lib/test/realtime/RealtimeInitTest.java +++ b/lib/src/test/java/io/ably/lib/test/realtime/RealtimeInitTest.java @@ -17,183 +17,183 @@ public class RealtimeInitTest extends ParameterizedTest { - /** - * Init library with a key only - */ - @Test - public void init_key_string() { - AblyRealtime ably = null; - try { - ably = new AblyRealtime(testVars.keys[0].keyStr); - } catch (AblyException e) { - e.printStackTrace(); - fail("init0: Unexpected exception instantiating library"); - } finally { - if(ably != null) ably.close(); - } - } + /** + * Init library with a key only + */ + @Test + public void init_key_string() { + AblyRealtime ably = null; + try { + ably = new AblyRealtime(testVars.keys[0].keyStr); + } catch (AblyException e) { + e.printStackTrace(); + fail("init0: Unexpected exception instantiating library"); + } finally { + if(ably != null) ably.close(); + } + } - /** - * Init library with a key in options - */ - @Test - public void init_key_opts() { - AblyRealtime ably = null; - try { - ably = new AblyRealtime(new ClientOptions(testVars.keys[0].keyStr)); - } catch (AblyException e) { - e.printStackTrace(); - fail("init1: Unexpected exception instantiating library"); - } finally { - if(ably != null) ably.close(); - } - } + /** + * Init library with a key in options + */ + @Test + public void init_key_opts() { + AblyRealtime ably = null; + try { + ably = new AblyRealtime(new ClientOptions(testVars.keys[0].keyStr)); + } catch (AblyException e) { + e.printStackTrace(); + fail("init1: Unexpected exception instantiating library"); + } finally { + if(ably != null) ably.close(); + } + } - /** - * Init library with key string - */ - @Test - public void init_key() { - AblyRealtime ably = null; - try { - ClientOptions opts = new ClientOptions(testVars.keys[0].keyStr); - ably = new AblyRealtime(opts); - } catch (AblyException e) { - e.printStackTrace(); - fail("init2: Unexpected exception instantiating library"); - } finally { - if(ably != null) ably.close(); - } - } + /** + * Init library with key string + */ + @Test + public void init_key() { + AblyRealtime ably = null; + try { + ClientOptions opts = new ClientOptions(testVars.keys[0].keyStr); + ably = new AblyRealtime(opts); + } catch (AblyException e) { + e.printStackTrace(); + fail("init2: Unexpected exception instantiating library"); + } finally { + if(ably != null) ably.close(); + } + } - /** - * Init library with specified host - */ - @Test - public void init_host() { - AblyRealtime ably = null; - try { - ClientOptions opts = new ClientOptions(testVars.keys[0].keyStr); - String hostExpected = "some.other.host"; - opts.restHost = hostExpected; - ably = new AblyRealtime(opts); - assertEquals("Unexpected host mismatch", hostExpected, ably.httpCore.getPrimaryHost()); - } catch (AblyException e) { - e.printStackTrace(); - fail("init4: Unexpected exception instantiating library"); - } finally { - if(ably != null) ably.close(); - } - } + /** + * Init library with specified host + */ + @Test + public void init_host() { + AblyRealtime ably = null; + try { + ClientOptions opts = new ClientOptions(testVars.keys[0].keyStr); + String hostExpected = "some.other.host"; + opts.restHost = hostExpected; + ably = new AblyRealtime(opts); + assertEquals("Unexpected host mismatch", hostExpected, ably.httpCore.getPrimaryHost()); + } catch (AblyException e) { + e.printStackTrace(); + fail("init4: Unexpected exception instantiating library"); + } finally { + if(ably != null) ably.close(); + } + } - /** - * Init library with specified port - */ - @Test - public void init_port() { - AblyRealtime ably = null; - try { - ClientOptions opts = new ClientOptions(testVars.keys[0].keyStr); - opts.port = 9998; - opts.tlsPort = 9999; - ably = new AblyRealtime(opts); - assertEquals("Unexpected port mismatch", Defaults.getPort(opts), opts.tlsPort); - } catch (AblyException e) { - e.printStackTrace(); - fail("init5: Unexpected exception instantiating library"); - } finally { - if(ably != null) ably.close(); - } - } + /** + * Init library with specified port + */ + @Test + public void init_port() { + AblyRealtime ably = null; + try { + ClientOptions opts = new ClientOptions(testVars.keys[0].keyStr); + opts.port = 9998; + opts.tlsPort = 9999; + ably = new AblyRealtime(opts); + assertEquals("Unexpected port mismatch", Defaults.getPort(opts), opts.tlsPort); + } catch (AblyException e) { + e.printStackTrace(); + fail("init5: Unexpected exception instantiating library"); + } finally { + if(ably != null) ably.close(); + } + } - /** - * Verify encrypted defaults to true - */ - @Test - public void init_default_secure() { - AblyRealtime ably = null; - try { - ClientOptions opts = new ClientOptions(testVars.keys[0].keyStr); - ably = new AblyRealtime(opts); - assertEquals("Unexpected port mismatch", Defaults.getPort(opts), Defaults.TLS_PORT); - } catch (AblyException e) { - e.printStackTrace(); - fail("init6: Unexpected exception instantiating library"); - } finally { - if(ably != null) ably.close(); - } - } + /** + * Verify encrypted defaults to true + */ + @Test + public void init_default_secure() { + AblyRealtime ably = null; + try { + ClientOptions opts = new ClientOptions(testVars.keys[0].keyStr); + ably = new AblyRealtime(opts); + assertEquals("Unexpected port mismatch", Defaults.getPort(opts), Defaults.TLS_PORT); + } catch (AblyException e) { + e.printStackTrace(); + fail("init6: Unexpected exception instantiating library"); + } finally { + if(ably != null) ably.close(); + } + } - /** - * Verify encrypted can be set to false - */ - @Test - public void init_insecure() { - AblyRealtime ably = null; - try { - ClientOptions opts = new ClientOptions(testVars.keys[0].keyStr); - opts.tls = false; - ably = new AblyRealtime(opts); - assertEquals("Unexpected scheme mismatch", Defaults.getPort(opts), Defaults.PORT); - } catch (AblyException e) { - e.printStackTrace(); - fail("init7: Unexpected exception instantiating library"); - } finally { - if(ably != null) ably.close(); - } - } + /** + * Verify encrypted can be set to false + */ + @Test + public void init_insecure() { + AblyRealtime ably = null; + try { + ClientOptions opts = new ClientOptions(testVars.keys[0].keyStr); + opts.tls = false; + ably = new AblyRealtime(opts); + assertEquals("Unexpected scheme mismatch", Defaults.getPort(opts), Defaults.PORT); + } catch (AblyException e) { + e.printStackTrace(); + fail("init7: Unexpected exception instantiating library"); + } finally { + if(ably != null) ably.close(); + } + } - /** - * Init with log handler; check called - */ - private boolean init8_logCalled; - @Test - public void init_log_handler() { - AblyRealtime ably = null; - try { - ClientOptions opts = new ClientOptions(testVars.keys[0].keyStr); - opts.logHandler = new LogHandler() { - @Override - public void println(int severity, String tag, String msg, Throwable tr) { - init8_logCalled = true; - System.out.println(msg); - } - }; - opts.logLevel = Log.VERBOSE; - ably = new AblyRealtime(opts); - assertTrue("Log handler not called", init8_logCalled); - } catch (AblyException e) { - e.printStackTrace(); - fail("init8: Unexpected exception instantiating library"); - } finally { - if(ably != null) ably.close(); - } - } + /** + * Init with log handler; check called + */ + private boolean init8_logCalled; + @Test + public void init_log_handler() { + AblyRealtime ably = null; + try { + ClientOptions opts = new ClientOptions(testVars.keys[0].keyStr); + opts.logHandler = new LogHandler() { + @Override + public void println(int severity, String tag, String msg, Throwable tr) { + init8_logCalled = true; + System.out.println(msg); + } + }; + opts.logLevel = Log.VERBOSE; + ably = new AblyRealtime(opts); + assertTrue("Log handler not called", init8_logCalled); + } catch (AblyException e) { + e.printStackTrace(); + fail("init8: Unexpected exception instantiating library"); + } finally { + if(ably != null) ably.close(); + } + } - /** - * Init with log handler; check not called if logLevel == NONE - */ - private boolean init9_logCalled; - @Test - public void init_log_level() { - AblyRealtime ably = null; - try { - ClientOptions opts = new ClientOptions(testVars.keys[0].keyStr); - opts.logHandler = new LogHandler() { - @Override - public void println(int severity, String tag, String msg, Throwable tr) { - init9_logCalled = true; - System.out.println(msg); - } - }; - opts.logLevel = Log.NONE; - ably = new AblyRealtime(opts); - assertFalse("Log handler incorrectly called", init9_logCalled); - } catch (AblyException e) { - e.printStackTrace(); - fail("init9: Unexpected exception instantiating library"); - } finally { - if(ably != null) ably.close(); - } - } + /** + * Init with log handler; check not called if logLevel == NONE + */ + private boolean init9_logCalled; + @Test + public void init_log_level() { + AblyRealtime ably = null; + try { + ClientOptions opts = new ClientOptions(testVars.keys[0].keyStr); + opts.logHandler = new LogHandler() { + @Override + public void println(int severity, String tag, String msg, Throwable tr) { + init9_logCalled = true; + System.out.println(msg); + } + }; + opts.logLevel = Log.NONE; + ably = new AblyRealtime(opts); + assertFalse("Log handler incorrectly called", init9_logCalled); + } catch (AblyException e) { + e.printStackTrace(); + fail("init9: Unexpected exception instantiating library"); + } finally { + if(ably != null) ably.close(); + } + } } diff --git a/lib/src/test/java/io/ably/lib/test/realtime/RealtimeJWTTest.java b/lib/src/test/java/io/ably/lib/test/realtime/RealtimeJWTTest.java index 8f1157067..a82b0f078 100644 --- a/lib/src/test/java/io/ably/lib/test/realtime/RealtimeJWTTest.java +++ b/lib/src/test/java/io/ably/lib/test/realtime/RealtimeJWTTest.java @@ -25,378 +25,378 @@ public class RealtimeJWTTest extends ParameterizedTest { - private AblyRest restJWTRequester; - private ClientOptions jwtRequesterOptions; - private Key key = testVars.keys[0]; - private final String clientId = "testJWTClientID"; - private final String channelName = "testJWTChannel" + UUID.randomUUID().toString(); - private final String messageName = "testJWTMessage" + UUID.randomUUID().toString(); - Param[] keys = new Param[]{ new Param("keyName", key.keyName), new Param("keySecret", key.keySecret) }; - Param[] clientIdParam = new Param[] { new Param("clientId", clientId) }; - Param[] shortTokenTtl = new Param[] { new Param("expiresIn", 5) }; - Param[] mediumTokenTtl = new Param[] { new Param("expiresIn", 35) }; - private final String susbcribeOnlyCapability = "{\"" + channelName + "\": [\"subscribe\"]}"; - private final String publishCapability = "{\"" + channelName + "\": [\"publish\"]}"; - private static final String echoServer = "https://echo.ably.io/createJWT"; - - /** - * Request a JWT that specifies a clientId - * Verifies that the clientId matches the one requested - */ - @Test - public void auth_clientid_match_the_one_requested_in_jwt() { - try { - /* create ably realtime with JWT token */ - ClientOptions realtimeOptions = buildClientOptions(mergeParams(keys, clientIdParam), null); - assertNotNull("Expected token value", realtimeOptions.token); - AblyRealtime ablyRealtime = new AblyRealtime(realtimeOptions); - - /* wait for connected state */ - ConnectionWaiter connectionWaiter = new ConnectionWaiter(ablyRealtime.connection); - connectionWaiter.waitFor(ConnectionState.connected); - assertEquals("Connected state was NOT reached", ConnectionState.connected, ablyRealtime.connection.state); - - /* check expected clientId */ - assertEquals("clientId does NOT match the one requested", clientId, ablyRealtime.auth.clientId); - - ablyRealtime.close(); - } catch (AblyException e) { - e.printStackTrace(); - fail(); - } - } - - /** - * Request a JWT with subscribe-only capabilities - * Verifies that publishing on a channel fails - */ - @Test - public void auth_jwt_with_subscribe_only_capability() { - try { - /* create ably realtime with JWT token that has subscribe-only capabilities */ - ClientOptions realtimeOptions = buildClientOptions(keys, susbcribeOnlyCapability); - assertNotNull("Expected token value", realtimeOptions.token); - final AblyRealtime ablyRealtime = new AblyRealtime(realtimeOptions); - - /* wait for connected state */ - ConnectionWaiter connectionWaiter = new ConnectionWaiter(ablyRealtime.connection); - connectionWaiter.waitFor(ConnectionState.connected); - assertEquals("Connected state was NOT reached", ConnectionState.connected, ablyRealtime.connection.state); - - /* attach to channel and verify attached state */ - Channel channel = ablyRealtime.channels.get(channelName); - channel.attach(); - new ChannelWaiter(channel).waitFor(ChannelState.attached); - - /* publish and verify that it fails */ - channel.publish(messageName, null, new CompletionListener() { - @Override - public void onSuccess() { - ablyRealtime.close(); - fail("It should not succeed"); - } - - @Override - public void onError(ErrorInfo error) { - assertEquals("Unexpected status code", 401, error.statusCode); - assertEquals("Unexpected error code", 40160, error.code); - assertEquals("Unexpected error message", "Unable to perform channel operation (permission denied)", error.message); - ablyRealtime.close(); - } - }); - connectionWaiter.waitFor(ConnectionState.closed); - } catch (AblyException e) { - e.printStackTrace(); - fail(); - } - } - - /** - * Request a JWT with publish capabilities - * Verifies that publishing on a channel succeeds - */ - @Test - public void auth_jwt_with_publish_capability() { - try { - /* create ably realtime with JWT token that has publish capabilities */ - ClientOptions realtimeOptions = buildClientOptions(keys, publishCapability); - assertNotNull("Expected token value", realtimeOptions.token); - final AblyRealtime ablyRealtime = new AblyRealtime(realtimeOptions); - - /* wait for connected state */ - ConnectionWaiter connectionWaiter = new ConnectionWaiter(ablyRealtime.connection); - connectionWaiter.waitFor(ConnectionState.connected); - assertEquals("Connected state was NOT reached", ConnectionState.connected, ablyRealtime.connection.state); - - /* attach to channel and verify attached state */ - Channel channel = ablyRealtime.channels.get(channelName); - channel.attach(); - new ChannelWaiter(channel).waitFor(ChannelState.attached); - - /* publish, verify that it succeeds then close */ - final Message message = new Message(messageName, null); - channel.publish(message, new CompletionListener() { - @Override - public void onSuccess() { - System.out.println("Message " + messageName + " published successfully"); - ablyRealtime.close(); - } - - @Override - public void onError(ErrorInfo reason) { - ablyRealtime.close(); - fail("Publish should not fail"); - } - }); - connectionWaiter.waitFor(ConnectionState.closed); - } catch (AblyException e) { - e.printStackTrace(); - fail(); - } - } - - /** - * Request a JWT with a ttl of 5 seconds and - * verify the correct error and message in the disconnected state change. - * Spec: RTN15h1 - */ - @Test - public void auth_jwt_with_token_that_expires() { - try { - /* create ably realtime with JWT token that expires in 5 seconds */ - ClientOptions realtimeOptions = buildClientOptions(mergeParams(keys, shortTokenTtl), null); - assertNotNull("Expected token value", realtimeOptions.token); - final AblyRealtime ablyRealtime = new AblyRealtime(realtimeOptions); - - /* wait for connected state */ - ConnectionWaiter connectionWaiter = new ConnectionWaiter(ablyRealtime.connection); - connectionWaiter.waitFor(ConnectionState.connected); - assertEquals("Connected state was NOT reached", ConnectionState.connected, ablyRealtime.connection.state); - - /* Verify the expected error reason when disconnected */ - ablyRealtime.connection.once(ConnectionEvent.disconnected, new ConnectionStateListener() { - - @Override - public void onConnectionStateChanged(ConnectionStateChange stateChange) { - assertEquals("Unexpected connection stage change", 40142, stateChange.reason.code); - assertTrue("Unexpected error message", stateChange.reason.message.contains("Key/token status changed (expire)")); - } - }); - connectionWaiter.waitFor(ConnectionState.failed); - } catch (AblyException e) { - e.printStackTrace(); - fail(); - } - } - - /** - * Request a JWT with a ttl of 35 seconds and - * verify that the client reauths without going through a disconnected state. (RTC8a4) - */ - @Test - public void auth_jwt_with_client_than_reauths_without_disconnecting() { - try { - final String[] tokens = new String[1]; - final boolean[] authMessages = new boolean[] { false }; - final boolean[] updateEvents = new boolean[] { false }; - - /* create ably realtime with authUrl and params that include a ttl of 35 seconds */ - DebugOptions options = new DebugOptions(testVars.keys[0].keyStr); - options.environment = createOptions().environment; - options.authUrl = echoServer; - options.authParams = mergeParams(keys, mediumTokenTtl); - options.protocolListener = new RawProtocolListener() { - @Override - public void onRawConnectRequested(String url) {} - @Override - public void onRawConnect(String url) { } - @Override - public void onRawMessageSend(ProtocolMessage message) { } - @Override - public void onRawMessageRecv(ProtocolMessage message) { - if (message.action == ProtocolMessage.Action.auth) { - authMessages[0] = true; - } - } - }; - final AblyRealtime ablyRealtime = new AblyRealtime(options); - - /* Once connected for the first time capture the assigned token */ - ablyRealtime.connection.once(ConnectionEvent.connected, new ConnectionStateListener() { - @Override - public void onConnectionStateChanged(ConnectionStateChange stateChange) { - assertEquals("State is not connected", ConnectionState.connected, stateChange.current); - synchronized (tokens) { - tokens[0] = ablyRealtime.auth.getTokenDetails().token; - } - } - }); - - /* Fail if the disconnected state is ever reached */ - ablyRealtime.connection.once(ConnectionEvent.disconnected, new ConnectionStateListener() { - @Override - public void onConnectionStateChanged(ConnectionStateChange stateChange) { - fail("Should NOT enter the disconnected state"); - } - }); - - /* Once receiving the update event check that the token is a new one - * and verify the auth protocol message has been received. */ - ablyRealtime.connection.on(ConnectionEvent.update, new ConnectionStateListener() { - @Override - public void onConnectionStateChanged(ConnectionStateChange state) { - assertNotEquals("Token should not be the same", tokens[0], ablyRealtime.auth.getTokenDetails().token); - assertTrue("Auth protocol message has not been received", authMessages[0]); - updateEvents[0] = true; - ablyRealtime.close(); - } - }); - - /* wait for connected state */ - ConnectionWaiter connectionWaiter = new ConnectionWaiter(ablyRealtime.connection); - connectionWaiter.waitFor(ConnectionState.connected); - assertEquals("Connected state was NOT reached", ConnectionState.connected, ablyRealtime.connection.state); - - /* wait for closed state */ - connectionWaiter.waitFor(ConnectionState.closed); - assertTrue("Update event not received", updateEvents[0]); - } catch (AblyException e) { - e.printStackTrace(); - fail(); - } - } - - /** - * Request a JWT with a ttl of 35 seconds and - * verify that the client reauths without going through a disconnected state using an authCallback. (RTC8a4) - */ - @Test - public void auth_jwt_with_client_than_reauths_without_disconnecting_via_authCallback() { - try { - final String[] tokens = new String[1]; - final boolean[] authMessages = new boolean[] { false }; - final boolean[] updateEvents = new boolean[] { false }; - final List callbackCalled = new ArrayList(); - - /* authCallback that with params that include a ttl of 35 seconds */ - TokenCallback authCallback = new TokenCallback() { - @Override - public Object getTokenRequest(TokenParams params) throws AblyException { - final String[] resultToken = new String[1]; - AblyRest rest = new AblyRest(createOptions(testVars.keys[0].keyStr)); - HttpHelpers.getUri(rest.httpCore, echoServer, new Param[]{}, mergeParams(keys, mediumTokenTtl), new ResponseHandler() { - @Override - public Object handleResponse(HttpCore.Response response, ErrorInfo error) throws AblyException { - try { - callbackCalled.add(true); - resultToken[0] = new String(response.body, "UTF-8"); - } catch (UnsupportedEncodingException e) { - e.printStackTrace(); - fail("Error in fetching a JWT token using authCallback"); - } - return null; - } - }); - return resultToken[0]; - } - }; - - /* create ably realtime with authCallback defined above */ - DebugOptions options = new DebugOptions(testVars.keys[0].keyStr); - options.environment = createOptions().environment; - options.authCallback = authCallback; - options.protocolListener = new RawProtocolListener() { - @Override - public void onRawConnectRequested(String url) {} - @Override - public void onRawConnect(String url) { } - @Override - public void onRawMessageSend(ProtocolMessage message) { } - @Override - public void onRawMessageRecv(ProtocolMessage message) { - if (message.action == ProtocolMessage.Action.auth) { - authMessages[0] = true; - } - } - }; - final AblyRealtime ablyRealtime = new AblyRealtime(options); - - /* Once connected for the first time capture the assigned token and - * verify the callback has been called once */ - ablyRealtime.connection.once(ConnectionEvent.connected, new ConnectionStateListener() { - @Override - public void onConnectionStateChanged(ConnectionStateChange stateChange) { - assertTrue("Callback not called the first time", callbackCalled.get(0)); - assertEquals("State is not connected", ConnectionState.connected, stateChange.current); - synchronized (tokens) { - tokens[0] = ablyRealtime.auth.getTokenDetails().token; - } - } - }); - - /* Fail if the disconnected state is ever reached */ - ablyRealtime.connection.once(ConnectionEvent.disconnected, new ConnectionStateListener() { - @Override - public void onConnectionStateChanged(ConnectionStateChange stateChange) { - ablyRealtime.close(); - fail("Should NOT enter the disconnected state"); - } - }); - - /* Once receiving the update event check that the token is a new one, - * verify the auth protocol message has been received and verify the callback has been called twice. */ - ablyRealtime.connection.on(ConnectionEvent.update, new ConnectionStateListener() { - @Override - public void onConnectionStateChanged(ConnectionStateChange state) { - assertTrue("Callback not called the second time", callbackCalled.get(1)); - assertEquals("Callback not called 2 times", callbackCalled.size(), 2); - assertNotEquals("Token should not be the same", tokens[0], ablyRealtime.auth.getTokenDetails().token); - assertTrue("Auth protocol message has not been received", authMessages[0]); - updateEvents[0] = true; - ablyRealtime.close(); - } - }); - - /* wait for connected state */ - ConnectionWaiter connectionWaiter = new ConnectionWaiter(ablyRealtime.connection); - connectionWaiter.waitFor(ConnectionState.connected); - assertEquals("Connected state was NOT reached", ConnectionState.connected, ablyRealtime.connection.state); - - /* wait for closed state */ - connectionWaiter.waitFor(ConnectionState.closed); - assertTrue("Update event not received", updateEvents[0]); - } catch (AblyException e) { - e.printStackTrace(); - fail(); - } - } - - /** - * Helper to create ClientOptions with a JWT token fetched via authUrl according to the parameters - */ - private ClientOptions buildClientOptions(Param[] params, String capability) { - try { - final String[] resultToken = new String[1]; - AblyRest rest = new AblyRest(createOptions(testVars.keys[0].keyStr)); - HttpHelpers.getUri(rest.httpCore, echoServer, null, params, new ResponseHandler() { - @Override - public Object handleResponse(HttpCore.Response response, ErrorInfo error) throws AblyException { - try { - resultToken[0] = new String(response.body, "UTF-8"); - } catch (UnsupportedEncodingException e) { - e.printStackTrace(); - fail("Error in fetching a JWT token " + e); - } - return null; - } - }); - ClientOptions realtimeOptions = createOptions(); - realtimeOptions.token = resultToken[0]; - return realtimeOptions; - } catch (AblyException e) { - fail("Failure in fetching a JWT token to create ClientOptions " + e); - return null; - } - } + private AblyRest restJWTRequester; + private ClientOptions jwtRequesterOptions; + private Key key = testVars.keys[0]; + private final String clientId = "testJWTClientID"; + private final String channelName = "testJWTChannel" + UUID.randomUUID().toString(); + private final String messageName = "testJWTMessage" + UUID.randomUUID().toString(); + Param[] keys = new Param[]{ new Param("keyName", key.keyName), new Param("keySecret", key.keySecret) }; + Param[] clientIdParam = new Param[] { new Param("clientId", clientId) }; + Param[] shortTokenTtl = new Param[] { new Param("expiresIn", 5) }; + Param[] mediumTokenTtl = new Param[] { new Param("expiresIn", 35) }; + private final String susbcribeOnlyCapability = "{\"" + channelName + "\": [\"subscribe\"]}"; + private final String publishCapability = "{\"" + channelName + "\": [\"publish\"]}"; + private static final String echoServer = "https://echo.ably.io/createJWT"; + + /** + * Request a JWT that specifies a clientId + * Verifies that the clientId matches the one requested + */ + @Test + public void auth_clientid_match_the_one_requested_in_jwt() { + try { + /* create ably realtime with JWT token */ + ClientOptions realtimeOptions = buildClientOptions(mergeParams(keys, clientIdParam), null); + assertNotNull("Expected token value", realtimeOptions.token); + AblyRealtime ablyRealtime = new AblyRealtime(realtimeOptions); + + /* wait for connected state */ + ConnectionWaiter connectionWaiter = new ConnectionWaiter(ablyRealtime.connection); + connectionWaiter.waitFor(ConnectionState.connected); + assertEquals("Connected state was NOT reached", ConnectionState.connected, ablyRealtime.connection.state); + + /* check expected clientId */ + assertEquals("clientId does NOT match the one requested", clientId, ablyRealtime.auth.clientId); + + ablyRealtime.close(); + } catch (AblyException e) { + e.printStackTrace(); + fail(); + } + } + + /** + * Request a JWT with subscribe-only capabilities + * Verifies that publishing on a channel fails + */ + @Test + public void auth_jwt_with_subscribe_only_capability() { + try { + /* create ably realtime with JWT token that has subscribe-only capabilities */ + ClientOptions realtimeOptions = buildClientOptions(keys, susbcribeOnlyCapability); + assertNotNull("Expected token value", realtimeOptions.token); + final AblyRealtime ablyRealtime = new AblyRealtime(realtimeOptions); + + /* wait for connected state */ + ConnectionWaiter connectionWaiter = new ConnectionWaiter(ablyRealtime.connection); + connectionWaiter.waitFor(ConnectionState.connected); + assertEquals("Connected state was NOT reached", ConnectionState.connected, ablyRealtime.connection.state); + + /* attach to channel and verify attached state */ + Channel channel = ablyRealtime.channels.get(channelName); + channel.attach(); + new ChannelWaiter(channel).waitFor(ChannelState.attached); + + /* publish and verify that it fails */ + channel.publish(messageName, null, new CompletionListener() { + @Override + public void onSuccess() { + ablyRealtime.close(); + fail("It should not succeed"); + } + + @Override + public void onError(ErrorInfo error) { + assertEquals("Unexpected status code", 401, error.statusCode); + assertEquals("Unexpected error code", 40160, error.code); + assertEquals("Unexpected error message", "Unable to perform channel operation (permission denied)", error.message); + ablyRealtime.close(); + } + }); + connectionWaiter.waitFor(ConnectionState.closed); + } catch (AblyException e) { + e.printStackTrace(); + fail(); + } + } + + /** + * Request a JWT with publish capabilities + * Verifies that publishing on a channel succeeds + */ + @Test + public void auth_jwt_with_publish_capability() { + try { + /* create ably realtime with JWT token that has publish capabilities */ + ClientOptions realtimeOptions = buildClientOptions(keys, publishCapability); + assertNotNull("Expected token value", realtimeOptions.token); + final AblyRealtime ablyRealtime = new AblyRealtime(realtimeOptions); + + /* wait for connected state */ + ConnectionWaiter connectionWaiter = new ConnectionWaiter(ablyRealtime.connection); + connectionWaiter.waitFor(ConnectionState.connected); + assertEquals("Connected state was NOT reached", ConnectionState.connected, ablyRealtime.connection.state); + + /* attach to channel and verify attached state */ + Channel channel = ablyRealtime.channels.get(channelName); + channel.attach(); + new ChannelWaiter(channel).waitFor(ChannelState.attached); + + /* publish, verify that it succeeds then close */ + final Message message = new Message(messageName, null); + channel.publish(message, new CompletionListener() { + @Override + public void onSuccess() { + System.out.println("Message " + messageName + " published successfully"); + ablyRealtime.close(); + } + + @Override + public void onError(ErrorInfo reason) { + ablyRealtime.close(); + fail("Publish should not fail"); + } + }); + connectionWaiter.waitFor(ConnectionState.closed); + } catch (AblyException e) { + e.printStackTrace(); + fail(); + } + } + + /** + * Request a JWT with a ttl of 5 seconds and + * verify the correct error and message in the disconnected state change. + * Spec: RTN15h1 + */ + @Test + public void auth_jwt_with_token_that_expires() { + try { + /* create ably realtime with JWT token that expires in 5 seconds */ + ClientOptions realtimeOptions = buildClientOptions(mergeParams(keys, shortTokenTtl), null); + assertNotNull("Expected token value", realtimeOptions.token); + final AblyRealtime ablyRealtime = new AblyRealtime(realtimeOptions); + + /* wait for connected state */ + ConnectionWaiter connectionWaiter = new ConnectionWaiter(ablyRealtime.connection); + connectionWaiter.waitFor(ConnectionState.connected); + assertEquals("Connected state was NOT reached", ConnectionState.connected, ablyRealtime.connection.state); + + /* Verify the expected error reason when disconnected */ + ablyRealtime.connection.once(ConnectionEvent.disconnected, new ConnectionStateListener() { + + @Override + public void onConnectionStateChanged(ConnectionStateChange stateChange) { + assertEquals("Unexpected connection stage change", 40142, stateChange.reason.code); + assertTrue("Unexpected error message", stateChange.reason.message.contains("Key/token status changed (expire)")); + } + }); + connectionWaiter.waitFor(ConnectionState.failed); + } catch (AblyException e) { + e.printStackTrace(); + fail(); + } + } + + /** + * Request a JWT with a ttl of 35 seconds and + * verify that the client reauths without going through a disconnected state. (RTC8a4) + */ + @Test + public void auth_jwt_with_client_than_reauths_without_disconnecting() { + try { + final String[] tokens = new String[1]; + final boolean[] authMessages = new boolean[] { false }; + final boolean[] updateEvents = new boolean[] { false }; + + /* create ably realtime with authUrl and params that include a ttl of 35 seconds */ + DebugOptions options = new DebugOptions(testVars.keys[0].keyStr); + options.environment = createOptions().environment; + options.authUrl = echoServer; + options.authParams = mergeParams(keys, mediumTokenTtl); + options.protocolListener = new RawProtocolListener() { + @Override + public void onRawConnectRequested(String url) {} + @Override + public void onRawConnect(String url) { } + @Override + public void onRawMessageSend(ProtocolMessage message) { } + @Override + public void onRawMessageRecv(ProtocolMessage message) { + if (message.action == ProtocolMessage.Action.auth) { + authMessages[0] = true; + } + } + }; + final AblyRealtime ablyRealtime = new AblyRealtime(options); + + /* Once connected for the first time capture the assigned token */ + ablyRealtime.connection.once(ConnectionEvent.connected, new ConnectionStateListener() { + @Override + public void onConnectionStateChanged(ConnectionStateChange stateChange) { + assertEquals("State is not connected", ConnectionState.connected, stateChange.current); + synchronized (tokens) { + tokens[0] = ablyRealtime.auth.getTokenDetails().token; + } + } + }); + + /* Fail if the disconnected state is ever reached */ + ablyRealtime.connection.once(ConnectionEvent.disconnected, new ConnectionStateListener() { + @Override + public void onConnectionStateChanged(ConnectionStateChange stateChange) { + fail("Should NOT enter the disconnected state"); + } + }); + + /* Once receiving the update event check that the token is a new one + * and verify the auth protocol message has been received. */ + ablyRealtime.connection.on(ConnectionEvent.update, new ConnectionStateListener() { + @Override + public void onConnectionStateChanged(ConnectionStateChange state) { + assertNotEquals("Token should not be the same", tokens[0], ablyRealtime.auth.getTokenDetails().token); + assertTrue("Auth protocol message has not been received", authMessages[0]); + updateEvents[0] = true; + ablyRealtime.close(); + } + }); + + /* wait for connected state */ + ConnectionWaiter connectionWaiter = new ConnectionWaiter(ablyRealtime.connection); + connectionWaiter.waitFor(ConnectionState.connected); + assertEquals("Connected state was NOT reached", ConnectionState.connected, ablyRealtime.connection.state); + + /* wait for closed state */ + connectionWaiter.waitFor(ConnectionState.closed); + assertTrue("Update event not received", updateEvents[0]); + } catch (AblyException e) { + e.printStackTrace(); + fail(); + } + } + + /** + * Request a JWT with a ttl of 35 seconds and + * verify that the client reauths without going through a disconnected state using an authCallback. (RTC8a4) + */ + @Test + public void auth_jwt_with_client_than_reauths_without_disconnecting_via_authCallback() { + try { + final String[] tokens = new String[1]; + final boolean[] authMessages = new boolean[] { false }; + final boolean[] updateEvents = new boolean[] { false }; + final List callbackCalled = new ArrayList(); + + /* authCallback that with params that include a ttl of 35 seconds */ + TokenCallback authCallback = new TokenCallback() { + @Override + public Object getTokenRequest(TokenParams params) throws AblyException { + final String[] resultToken = new String[1]; + AblyRest rest = new AblyRest(createOptions(testVars.keys[0].keyStr)); + HttpHelpers.getUri(rest.httpCore, echoServer, new Param[]{}, mergeParams(keys, mediumTokenTtl), new ResponseHandler() { + @Override + public Object handleResponse(HttpCore.Response response, ErrorInfo error) throws AblyException { + try { + callbackCalled.add(true); + resultToken[0] = new String(response.body, "UTF-8"); + } catch (UnsupportedEncodingException e) { + e.printStackTrace(); + fail("Error in fetching a JWT token using authCallback"); + } + return null; + } + }); + return resultToken[0]; + } + }; + + /* create ably realtime with authCallback defined above */ + DebugOptions options = new DebugOptions(testVars.keys[0].keyStr); + options.environment = createOptions().environment; + options.authCallback = authCallback; + options.protocolListener = new RawProtocolListener() { + @Override + public void onRawConnectRequested(String url) {} + @Override + public void onRawConnect(String url) { } + @Override + public void onRawMessageSend(ProtocolMessage message) { } + @Override + public void onRawMessageRecv(ProtocolMessage message) { + if (message.action == ProtocolMessage.Action.auth) { + authMessages[0] = true; + } + } + }; + final AblyRealtime ablyRealtime = new AblyRealtime(options); + + /* Once connected for the first time capture the assigned token and + * verify the callback has been called once */ + ablyRealtime.connection.once(ConnectionEvent.connected, new ConnectionStateListener() { + @Override + public void onConnectionStateChanged(ConnectionStateChange stateChange) { + assertTrue("Callback not called the first time", callbackCalled.get(0)); + assertEquals("State is not connected", ConnectionState.connected, stateChange.current); + synchronized (tokens) { + tokens[0] = ablyRealtime.auth.getTokenDetails().token; + } + } + }); + + /* Fail if the disconnected state is ever reached */ + ablyRealtime.connection.once(ConnectionEvent.disconnected, new ConnectionStateListener() { + @Override + public void onConnectionStateChanged(ConnectionStateChange stateChange) { + ablyRealtime.close(); + fail("Should NOT enter the disconnected state"); + } + }); + + /* Once receiving the update event check that the token is a new one, + * verify the auth protocol message has been received and verify the callback has been called twice. */ + ablyRealtime.connection.on(ConnectionEvent.update, new ConnectionStateListener() { + @Override + public void onConnectionStateChanged(ConnectionStateChange state) { + assertTrue("Callback not called the second time", callbackCalled.get(1)); + assertEquals("Callback not called 2 times", callbackCalled.size(), 2); + assertNotEquals("Token should not be the same", tokens[0], ablyRealtime.auth.getTokenDetails().token); + assertTrue("Auth protocol message has not been received", authMessages[0]); + updateEvents[0] = true; + ablyRealtime.close(); + } + }); + + /* wait for connected state */ + ConnectionWaiter connectionWaiter = new ConnectionWaiter(ablyRealtime.connection); + connectionWaiter.waitFor(ConnectionState.connected); + assertEquals("Connected state was NOT reached", ConnectionState.connected, ablyRealtime.connection.state); + + /* wait for closed state */ + connectionWaiter.waitFor(ConnectionState.closed); + assertTrue("Update event not received", updateEvents[0]); + } catch (AblyException e) { + e.printStackTrace(); + fail(); + } + } + + /** + * Helper to create ClientOptions with a JWT token fetched via authUrl according to the parameters + */ + private ClientOptions buildClientOptions(Param[] params, String capability) { + try { + final String[] resultToken = new String[1]; + AblyRest rest = new AblyRest(createOptions(testVars.keys[0].keyStr)); + HttpHelpers.getUri(rest.httpCore, echoServer, null, params, new ResponseHandler() { + @Override + public Object handleResponse(HttpCore.Response response, ErrorInfo error) throws AblyException { + try { + resultToken[0] = new String(response.body, "UTF-8"); + } catch (UnsupportedEncodingException e) { + e.printStackTrace(); + fail("Error in fetching a JWT token " + e); + } + return null; + } + }); + ClientOptions realtimeOptions = createOptions(); + realtimeOptions.token = resultToken[0]; + return realtimeOptions; + } catch (AblyException e) { + fail("Failure in fetching a JWT token to create ClientOptions " + e); + return null; + } + } } diff --git a/lib/src/test/java/io/ably/lib/test/realtime/RealtimeMessageTest.java b/lib/src/test/java/io/ably/lib/test/realtime/RealtimeMessageTest.java index 94b51020e..babd880c4 100644 --- a/lib/src/test/java/io/ably/lib/test/realtime/RealtimeMessageTest.java +++ b/lib/src/test/java/io/ably/lib/test/realtime/RealtimeMessageTest.java @@ -48,922 +48,922 @@ public class RealtimeMessageTest extends ParameterizedTest { - private static final String testMessagesEncodingFile = "ably-common/test-resources/messages-encoding.json"; - private static Gson gson = new Gson(); - - @Rule - public Timeout testTimeout = Timeout.seconds(300); - - /** - * Connect to the service and attach, subscribe to an event, and publish on that channel - */ - @Test - public void single_send() { - AblyRealtime ably = null; - try { - ClientOptions opts = createOptions(testVars.keys[0].keyStr); - ably = new AblyRealtime(opts); - - /* create a channel */ - final Channel channel = ably.channels.get("subscribe_send_binary"); - - /* attach */ - channel.attach(); - (new ChannelWaiter(channel)).waitFor(ChannelState.attached); - assertEquals("Verify attached state reached", channel.state, ChannelState.attached); - - /* subscribe */ - MessageWaiter messageWaiter = new MessageWaiter(channel); - - /* publish to the channel */ - CompletionWaiter msgComplete = new CompletionWaiter(); - channel.publish("test_event", "Test message (subscribe_send_binary)", msgComplete); - - /* wait for the publish callback to be called */ - msgComplete.waitFor(); - assertTrue("Verify success callback was called", msgComplete.success); - - /* wait for the subscription callback to be called */ - messageWaiter.waitFor(1); - assertEquals("Verify message subscription was called", messageWaiter.receivedMessages.size(), 1); - - } catch(AblyException e) { - e.printStackTrace(); - fail("init0: Unexpected exception instantiating library"); - } finally { - if(ably != null) - ably.close(); - } - } - - /** - * Connect to the service on two connections; - * attach, subscribe to an event, publish on one - * connection and confirm receipt on the other. - */ - @Test - public void single_send_noecho() { - AblyRealtime txAbly = null; - AblyRealtime rxAbly = null; - try { - ClientOptions opts = createOptions(testVars.keys[0].keyStr); - opts.echoMessages = false; - txAbly = new AblyRealtime(opts); - rxAbly = new AblyRealtime(opts); - String channelName = "subscribe_send_binary_noecho"; - - /* create a channel */ - final Channel txChannel = txAbly.channels.get(channelName); - final Channel rxChannel = rxAbly.channels.get(channelName); - - /* attach both connections */ - txChannel.attach(); - (new ChannelWaiter(txChannel)).waitFor(ChannelState.attached); - assertEquals("Verify attached state reached", txChannel.state, ChannelState.attached); - rxChannel.attach(); - (new ChannelWaiter(rxChannel)).waitFor(ChannelState.attached); - assertEquals("Verify attached state reached", rxChannel.state, ChannelState.attached); - - /* subscribe on both connections */ - MessageWaiter txMessageWaiter = new MessageWaiter(txChannel); - MessageWaiter rxMessageWaiter = new MessageWaiter(rxChannel); - - /* publish to the channel */ - CompletionWaiter msgComplete = new CompletionWaiter(); - txChannel.publish("test_event", "Test message (subscribe_send_binary_noecho)", msgComplete); - - /* wait for the publish callback to be called */ - msgComplete.waitFor(); - assertTrue("Verify success callback was called", msgComplete.success); - - /* wait for the subscription callback to be called */ - rxMessageWaiter.waitFor(1); - assertEquals("Verify rx message subscription was called", rxMessageWaiter.receivedMessages.size(), 1); - - /* wait to verify that the subscription callback is not called on txConnection */ - txMessageWaiter.waitFor(1, 1000L); - assertEquals("Verify tx message subscription was not called", txMessageWaiter.receivedMessages.size(), 0); - - } catch(AblyException e) { - e.printStackTrace(); - fail("single_send_binary_noecho: Unexpected exception instantiating library"); - } finally { - if(txAbly != null) - txAbly.close(); - if(rxAbly != null) - rxAbly.close(); - } - } - - /** - * Get a channel and subscribe without explicitly attaching. - * Verify that the channel reaches the attached state. - */ - @Test - public void subscribe_implicit_attach() { - AblyRealtime ably = null; - String channelName = "subscribe_implicit_attach_" + testParams.name; - try { - ClientOptions opts = createOptions(testVars.keys[0].keyStr); - ably = new AblyRealtime(opts); - - /* create a channel */ - final Channel channel = ably.channels.get(channelName); - - /* subscribe */ - MessageWaiter messageWaiter = new MessageWaiter(channel); - - /* verify attached state is reached */ - (new ChannelWaiter(channel)).waitFor(ChannelState.attached); - assertEquals("Verify attached state reached", channel.state, ChannelState.attached); - - /* publish to the channel */ - CompletionWaiter msgComplete = new CompletionWaiter(); - channel.publish("test_event", "Test message (" + channelName + ")", msgComplete); - - /* wait for the publish callback to be called */ - msgComplete.waitFor(); - assertTrue("Verify success callback was called", msgComplete.success); - - /* wait for the subscription callback to be called */ - messageWaiter.waitFor(1); - assertEquals("Verify message subscription was called", messageWaiter.receivedMessages.size(), 1); - - } catch(AblyException e) { - e.printStackTrace(); - fail("init0: Unexpected exception instantiating library"); - } finally { - if(ably != null) - ably.close(); - } - } - - /** - * Connect to the service using the default (binary) protocol - * and attach, subscribe to an event, and publish multiple - * messages on that channel - */ - private void _multiple_send(String channelName, int messageCount, int msgSize, boolean binary, long delay) { - AblyRealtime ably = null; - try { - ClientOptions opts = createOptions(testVars.keys[0].keyStr); - ably = new AblyRealtime(opts); - - /* create a channel */ - final Channel channel = ably.channels.get(channelName); - - /* attach */ - channel.attach(); - (new ChannelWaiter(channel)).waitFor(ChannelState.attached); - assertEquals("Verify attached state reached", channel.state, ChannelState.attached); - - /* subscribe */ - MessageWaiter messageWaiter = new MessageWaiter(channel); - - /* publish to the channel */ - CompletionSet msgComplete = new CompletionSet(); - if(binary) { - byte[][] messagesSent = new byte[messageCount][]; - for(int i = 0; i < messageCount; i++) { - byte[] messageData = messagesSent[i] = Helpers.RandomGenerator.generateRandomBuffer(msgSize); - channel.publish("test_event", messageData, msgComplete.add()); - try { - Thread.sleep(delay); - } catch(InterruptedException e) { - } - } - - /* wait for the publish callback to be called */ - ErrorInfo[] errors = msgComplete.waitFor(); - assertTrue("Verify success from all message callbacks", errors.length == 0); - - /* wait for the subscription callback to be called */ - messageWaiter.waitFor(messageCount); - - /* verify received message content */ - List receivedMessages = messageWaiter.receivedMessages; - assertEquals("Verify message subscriptions all called", receivedMessages.size(), messageCount); - for(int i = 0; i < messageCount; i++) { - assertArrayEquals("Verify expected message contents", messagesSent[i], (byte[]) receivedMessages.get(i).data); - } - } else { - String[] messagesSent = new String[messageCount]; - for(int i = 0; i < messageCount; i++) { - String messageData = messagesSent[i] = Helpers.RandomGenerator.generateRandomString(msgSize); - channel.publish("test_event", messageData, msgComplete.add()); - try { - Thread.sleep(delay); - } catch(InterruptedException e) { - } - } - - /* wait for the publish callback to be called */ - ErrorInfo[] errors = msgComplete.waitFor(); - assertTrue("Verify success from all message callbacks", errors.length == 0); - - /* wait for the subscription callback to be called */ - messageWaiter.waitFor(messageCount); - - /* verify received message content */ - List receivedMessages = messageWaiter.receivedMessages; - assertEquals("Verify message subscriptions all called", receivedMessages.size(), messageCount); - for(int i = 0; i < messageCount; i++) { - assertEquals("Verify expected message contents", messagesSent[i], (String) receivedMessages.get(i).data); - } - } - - } catch(AblyException e) { - e.printStackTrace(); - fail("channelName: Unexpected exception instantiating library"); - } finally { - if(ably != null) - ably.close(); - } - } - - /* - * Test right and wrong channel states to publish messages - * Tests RTL6c - */ - @Test - public void publish_channel_state() { - AblyRealtime ably = null; - try { - ClientOptions opts = createOptions(testVars.keys[0].keyStr); - ably = new AblyRealtime(opts); - - Channel pubChannel = ably.channels.get("publish_channel_state"); - ChannelWaiter channelWaiter = new ChannelWaiter(pubChannel); - pubChannel.attach(); - - /* Publish in attaching state */ - pubChannel.publish(new Message("name1", "data1")); - - channelWaiter.waitFor(ChannelState.attached); - - /* Go to suspended state */ - ably.connection.connectionManager.requestState(ConnectionState.suspended); - channelWaiter.waitFor(ChannelState.suspended); - - boolean error = false; - try { - pubChannel.publish(new Message("name2", "data2")); - } catch(AblyException e) { - error = true; - } - assertTrue("Verify exception was thrown on publishing in suspended state", error); - - /* reconnect and try again */ - ably.connection.connectionManager.requestState(ConnectionState.connecting); - channelWaiter.waitFor(ChannelState.attached); - - pubChannel.publish(new Message("name3", "data3")); - - /* fail connection */ - ably.connection.connectionManager.requestState(ConnectionState.failed); - channelWaiter.waitFor(ChannelState.failed); - error = false; - try { - pubChannel.publish(new Message("name4", "data4")); - } catch(AblyException e) { - error = true; - } - assertTrue("Verify exception was thrown on publishing in failed state", error); - - } catch(AblyException e) { - e.printStackTrace(); - fail("Unexpected exception"); - } finally { - if(ably != null) - ably.close(); - } - - } - - /** - * Connect to the service using the default (binary) protocol - * and attach, subscribe to an event, and publish multiple - * messages on that channel - */ - private void _multiple_send_batch(String channelName, int messageCount, int batchCount, long batchDelay) { - AblyRealtime ably = null; - try { - ClientOptions opts = createOptions(testVars.keys[0].keyStr); - ably = new AblyRealtime(opts); - - /* create a channel */ - final Channel channel = ably.channels.get(channelName); - - /* attach */ - channel.attach(); - (new ChannelWaiter(channel)).waitFor(ChannelState.attached); - assertEquals("Verify attached state reached", channel.state, ChannelState.attached); - - /* subscribe */ - MessageWaiter messageWaiter = new MessageWaiter(channel); - - /* publish to the channel */ - CompletionSet msgComplete = new CompletionSet(); - for(int i = 0; i < (int) (messageCount / batchCount); i++) { - for(int j = 0; j < batchCount; j++) { - channel.publish("test_event", "Test message (_multiple_send_batch) " + i * batchCount + j, msgComplete.add()); - } - try { - Thread.sleep(batchDelay); - } catch(InterruptedException e) {/*ignore*/} - } - - /* wait for the publish callback to be called */ - ErrorInfo[] errors = msgComplete.waitFor(); - assertTrue("Verify success from all message callbacks", errors.length == 0); - - /* wait for the subscription callback to be called */ - messageWaiter.waitFor(messageCount); - assertEquals("Verify message subscriptions all called", messageWaiter.receivedMessages.size(), messageCount); - - } catch(AblyException e) { - e.printStackTrace(); - fail("channelName: Unexpected exception instantiating library"); - } finally { - if(ably != null) - ably.close(); - } - } - - @Test - public void multiple_send_10_1000_16_string() { - int messageCount = 10; - long delay = 1000L; - _multiple_send("multiple_send_10_1000_16_string_" + testParams.name, messageCount, 16, false, delay); - } - - @Test - public void multiple_send_10_1000_16_binary() { - int messageCount = 10; - long delay = 1000L; - _multiple_send("multiple_send_10_1000_16_binary_" + testParams.name, messageCount, 16, true, delay); - } - - @Test - public void multiple_send_10_1000_512_string() { - int messageCount = 10; - long delay = 1000L; - _multiple_send("multiple_send_10_1000_512_string_" + testParams.name, messageCount, 512, false, delay); - } - - @Test - public void multiple_send_10_1000_512_binary() { - int messageCount = 10; - long delay = 1000L; - _multiple_send("multiple_send_10_1000_512_binary_" + testParams.name, messageCount, 512, true, delay); - } - - @Test - public void multiple_send_20_200() { - int messageCount = 20; - long delay = 200L; - _multiple_send("multiple_send_20_200_" + testParams.name, messageCount, 256, true, delay); - } - - @Test - public void multiple_send_200_50() { - int messageCount = 200; - long delay = 50L; - _multiple_send("multiple_send_binary_200_50_" + testParams.name, messageCount, 256, true, delay); - } - - @Test - public void multiple_send_1000_10() { - int messageCount = 1000; - long delay = 10L; - _multiple_send("multiple_send_binary_1000_10_" + testParams.name, messageCount, 256, true, delay); - } - - /** - * Connect to the service - * using credentials that are unable to publish,and attach. - * Attempt to publish and verify that an error is received. - */ - @Test - public void single_error() { - AblyRealtime ably = null; - try { - ClientOptions opts = createOptions(testVars.keys[4].keyStr); - ably = new AblyRealtime(opts); - - /* create a channel; channel3 can subscribe but not publish - * with this key */ - final Channel channel = ably.channels.get("channel3"); - - /* attach */ - channel.attach(); - (new ChannelWaiter(channel)).waitFor(ChannelState.attached); - assertEquals("Verify attached state reached", channel.state, ChannelState.attached); - - /* publish to the channel */ - CompletionWaiter msgComplete = new CompletionWaiter(); - channel.publish("test_event", "Test message (single_error_binary)", msgComplete); - - /* wait for the publish callback to be called */ - ErrorInfo fail = msgComplete.waitFor(); - assertEquals("Verify error callback was called", fail.statusCode, 401); - - } catch(AblyException e) { - e.printStackTrace(); - fail("single_error_binary: Unexpected exception instantiating library"); - } finally { - if(ably != null) - ably.close(); - } - } - - @Test - public void ensure_disconnect_with_error_does_not_move_to_failed() { - AblyRealtime ably = null; - try { - ClientOptions opts = createOptions(testVars.keys[0].keyStr); - ably = new AblyRealtime(opts); - - /* wait until connected */ - (new ConnectionWaiter(ably.connection)).waitFor(ConnectionState.connected); - assertEquals("Verify connected state reached", ably.connection.state, ConnectionState.connected); - - ProtocolMessage protoMessage = new ProtocolMessage(ProtocolMessage.Action.disconnect); - protoMessage.error = new ErrorInfo("test error", 123); - - ConnectionManager connectionManager = ably.connection.connectionManager; - connectionManager.onMessage(null, protoMessage); - - // On disconnected we retry right away since we're connected, so we can only - // check that the state is not failed. - assertNotEquals("connection state should not be failed", ably.connection.state, ConnectionState.failed); - } catch(AblyException e) { - e.printStackTrace(); - fail("init0: Unexpected exception instantiating library"); - } finally { - if(ably != null) - ably.close(); - } - } - - @Test - public void messages_encoding_fixtures() { - MessagesEncodingData fixtures; - try { - fixtures = (MessagesEncodingData) Setup.loadJson(testMessagesEncodingFile, MessagesEncodingData.class); - } catch(IOException e) { - fail(); - return; - } - - AblyRealtime ably = null; - try { - ClientOptions opts = createOptions(testVars.keys[0].keyStr); - ably = new AblyRealtime(opts); - final Channel channel = ably.channels.get("test"); - - channel.attach(); - (new ChannelWaiter(channel)).waitFor(ChannelState.attached); - assertEquals("Verify attached state reached", channel.state, ChannelState.attached); - - for(MessagesEncodingDataItem fixtureMessage : fixtures.messages) { - /* subscribe */ - MessageWaiter messageWaiter = new MessageWaiter(channel); - - HttpHelpers.postSync(ably.http, "/channels/" + channel.name + "/messages", null, null, new HttpUtils.JsonRequestBody(fixtureMessage), null, true); - - messageWaiter.waitFor(1); - channel.unsubscribe(messageWaiter); - - Message receivedMessage = messageWaiter.receivedMessages.get(0); - - expectDataToMatch(fixtureMessage, receivedMessage); - - CompletionWaiter msgComplete = new CompletionWaiter(); - channel.publish(receivedMessage, msgComplete); - msgComplete.waitFor(); - - MessagesEncodingDataItem persistedMessage = ably.http.request(new Http.Execute() { - @Override - public void execute(HttpScheduler http, Callback callback) throws AblyException { - http.get("/channels/" + channel.name + "/messages?limit=1", null, null, new HttpCore.ResponseHandler() { - @Override - public MessagesEncodingDataItem[] handleResponse(HttpCore.Response response, ErrorInfo error) throws AblyException { - if(error != null) { - throw AblyException.fromErrorInfo(error); - } - return gson.fromJson(new String(response.body), MessagesEncodingDataItem[].class); - } - }, true, callback); - } - }).sync()[0]; - - assertEquals("Verify persisted message encoding", fixtureMessage.encoding, persistedMessage.encoding); - assertEquals("Verify persisted message data", fixtureMessage.data, persistedMessage.data); - } - } catch(AblyException e) { - e.printStackTrace(); - fail("init0: Unexpected exception instantiating library"); - } finally { - if(ably != null) - ably.close(); - } - } - - @Test - public void messages_msgpack_and_json_encoding_is_compatible() { - MessagesEncodingData fixtures; - try { - fixtures = (MessagesEncodingData) Setup.loadJson(testMessagesEncodingFile, MessagesEncodingData.class); - } catch(IOException e) { - fail(); - return; - } - - // Publish each data type through raw JSON POST and retrieve through MsgPack and JSON. - - AblyRealtime realtimeSubscribeClientMsgPack = null; - AblyRealtime realtimeSubscribeClientJson = null; - try { - ClientOptions jsonOpts = createOptions(testVars.keys[0].keyStr); - - ClientOptions msgpackOpts = createOptions(testVars.keys[0].keyStr); - msgpackOpts.useBinaryProtocol = !testParams.useBinaryProtocol; - - AblyRest restPublishClient = new AblyRest(jsonOpts); - realtimeSubscribeClientMsgPack = new AblyRealtime(msgpackOpts); - realtimeSubscribeClientJson = new AblyRealtime(jsonOpts); - - final Channel realtimeSubscribeChannelMsgPack = realtimeSubscribeClientMsgPack.channels.get("test-subscribe"); - final Channel realtimeSubscribeChannelJson = realtimeSubscribeClientJson.channels.get("test-subscribe"); - - for(Channel realtimeSubscribeChannel : new Channel[]{realtimeSubscribeChannelMsgPack, realtimeSubscribeChannelJson}) { - realtimeSubscribeChannel.attach(); - (new ChannelWaiter(realtimeSubscribeChannel)).waitFor(ChannelState.attached); - assertEquals("Verify attached state reached", realtimeSubscribeChannel.state, ChannelState.attached); - - for(MessagesEncodingDataItem fixtureMessage : fixtures.messages) { - MessageWaiter messageWaiter = new MessageWaiter(realtimeSubscribeChannel); - - HttpHelpers.postSync(restPublishClient.http, "/channels/" + realtimeSubscribeChannel.name + "/messages", null, null, new HttpUtils.JsonRequestBody(fixtureMessage), null, true); - - messageWaiter.waitFor(1); - realtimeSubscribeChannel.unsubscribe(messageWaiter); - - Message receivedMessage = messageWaiter.receivedMessages.get(0); - - expectDataToMatch(fixtureMessage, receivedMessage); - } - } - - for(AblyRealtime realtimeSubscribeClient : new AblyRealtime[]{realtimeSubscribeClientMsgPack, realtimeSubscribeClientJson}) { - realtimeSubscribeClient.close(); - realtimeSubscribeClient = null; - } - - // Publish each data type through MsgPack and JSON and retrieve through raw JSON GET. - - AblyRest restPublishClientMsgPack = new AblyRest(msgpackOpts); - AblyRest restPublishClientJson = new AblyRest(jsonOpts); - AblyRest restRetrieveClient = new AblyRest(jsonOpts); - - final io.ably.lib.rest.Channel restPublishChannelMsgPack = restPublishClientMsgPack.channels.get("test-publish"); - final io.ably.lib.rest.Channel restPublishChannelJson = restPublishClientJson.channels.get("test-publish"); - - for(MessagesEncodingDataItem fixtureMessage : fixtures.messages) { - Object data = fixtureMessage.expectedValue; - if(fixtureMessage.expectedHexValue != null) { - data = hexStringToByteArray(fixtureMessage.expectedHexValue); - } else if(data instanceof JsonPrimitive) { - data = ((JsonPrimitive) data).getAsString(); - } - - for(final io.ably.lib.rest.Channel restPublishChannel : new io.ably.lib.rest.Channel[]{restPublishChannelMsgPack, restPublishChannelJson}) { - restPublishChannel.publish("event", data); - - MessagesEncodingDataItem persistedMessage = restRetrieveClient.http.request(new Http.Execute() { - @Override - public void execute(HttpScheduler http, Callback callback) throws AblyException { - http.get("/channels/" + restPublishChannel.name + "/messages?limit=1", null, null, new HttpCore.ResponseHandler() { - @Override - public MessagesEncodingDataItem[] handleResponse(HttpCore.Response response, ErrorInfo error) throws AblyException { - if(error != null) { - throw AblyException.fromErrorInfo(error); - } - return gson.fromJson(new String(response.body), MessagesEncodingDataItem[].class); - } - }, true, callback); - } - }).sync()[0]; - - assertEquals("Verify persisted message encoding", fixtureMessage.encoding, persistedMessage.encoding); - assertEquals("Verify persisted message data", fixtureMessage.data, persistedMessage.data); - } - } - } catch(AblyException e) { - e.printStackTrace(); - fail("init0: Unexpected exception instantiating library"); - } finally { - if(realtimeSubscribeClientMsgPack != null) - realtimeSubscribeClientMsgPack.close(); - if(realtimeSubscribeClientJson != null) - realtimeSubscribeClientJson.close(); - } - } - - /** - * Test behaviour when message is encoded as encrypted but encryption is not set up - */ - @Test - public void message_inconsistent_encoding() { - AblyRealtime realtimeSubscribeClient = null; - final ArrayList log = new ArrayList<>(); - - try { - ClientOptions apiOptions = createOptions(testVars.keys[0].keyStr); - apiOptions.logHandler = new Log.LogHandler() { - @Override - public void println(int severity, String tag, String msg, Throwable tr) { - synchronized(log) { - log.add(String.format(Locale.US, "%s: %s", tag, msg)); - } - } - }; - apiOptions.logLevel = Log.INFO; - - AblyRest restPublishClient = new AblyRest(apiOptions); - realtimeSubscribeClient = new AblyRealtime(apiOptions); - - final Channel realtimeSubscribeChannelJson = realtimeSubscribeClient.channels.get("test-encoding"); - - realtimeSubscribeChannelJson.attach(); - (new ChannelWaiter(realtimeSubscribeChannelJson)).waitFor(ChannelState.attached); - assertEquals("Verify attached state reached", realtimeSubscribeChannelJson.state, ChannelState.attached); - - MessageWaiter messageWaiter = new MessageWaiter(realtimeSubscribeChannelJson); - - MessagesEncodingDataItem testData = new MessagesEncodingDataItem(); - testData.data = "MDEyMzQ1Njc4OQ=="; /* Base64("0123456789") */ - testData.encoding = "utf-8/cipher+aes-128-cbc/base64"; - testData.expectedType = "binary"; - testData.expectedHexValue = "30313233343536373839"; /* hex for "0123456789" */ - - HttpHelpers.postSync(restPublishClient.http, "/channels/" + realtimeSubscribeChannelJson.name + "/messages", null, null, new HttpUtils.JsonRequestBody(testData), null, true); - - messageWaiter.waitFor(1); - realtimeSubscribeChannelJson.unsubscribe(messageWaiter); - - Message receivedMessage = messageWaiter.receivedMessages.get(0); - - expectDataToMatch(testData, receivedMessage); - assertEquals("Verify resulting encoding", receivedMessage.encoding, "utf-8/cipher+aes-128-cbc"); - - synchronized(log) { - boolean foundErrorMessage = false; - for(String logMessage : log) { - if(logMessage.contains("encryption is not set up")) - foundErrorMessage = true; - } - assertTrue("Verify logged error messages", foundErrorMessage); - } - } catch(AblyException e) { - e.printStackTrace(); - fail("init0: Unexpected exception instantiating library"); - } finally { - if(realtimeSubscribeClient != null) - realtimeSubscribeClient.close(); - } - } - - private void expectDataToMatch(MessagesEncodingDataItem fixtureMessage, Message receivedMessage) { - if(fixtureMessage.expectedType.equals("string")) { - assertEquals("Verify decoded message data", fixtureMessage.expectedValue.getAsString(), receivedMessage.data); - } else if(fixtureMessage.expectedType.equals("jsonObject")) { - assertEquals("Verify decoded message data", fixtureMessage.expectedValue.getAsJsonObject(), receivedMessage.data); - } else if(fixtureMessage.expectedType.equals("jsonArray")) { - assertEquals("Verify decoded message data", fixtureMessage.expectedValue.getAsJsonArray(), receivedMessage.data); - } else if(fixtureMessage.expectedType.equals("binary")) { - byte[] receivedData = (byte[]) receivedMessage.data; - StringBuilder sb = new StringBuilder(receivedData.length * 2); - for(byte b : receivedData) { - sb.append(String.format("%02x", b & 0xff)); - } - String receivedDataHex = sb.toString(); - assertEquals("Verify decoded message data", fixtureMessage.expectedHexValue, receivedDataHex); - } else { - throw new RuntimeException(String.format("unhandled: %s", fixtureMessage.expectedType)); - } - } - - public static byte[] hexStringToByteArray(String s) { - int len = s.length(); - byte[] data = new byte[len / 2]; - for(int i = 0; i < len; i += 2) { - data[i / 2] = (byte) ((Character.digit(s.charAt(i), 16) << 4) - + Character.digit(s.charAt(i + 1), 16)); - } - return data; - } - - static class MessagesEncodingData { - public MessagesEncodingDataItem[] messages; - } - - static class MessagesEncodingDataItem { - public String data; - public String encoding; - public String expectedType; - public JsonElement expectedValue; - public String expectedHexValue; - } - - @Test - public void reject_invalid_message_data() throws AblyException { - HashMap data = new HashMap(); - Message message = new Message("event", data); - Log.LogHandler originalLogHandler = Log.handler; - int originalLogLevel = Log.level; - Log.setLevel(Log.DEBUG); - final ArrayList capturedLog = new ArrayList<>(); - Log.setHandler(new Log.LogHandler() { - @Override - public void println(int severity, String tag, String msg, Throwable tr) { - capturedLog.add(new LogLine(severity, tag, msg, tr)); - } - }); - - try { - message.encode(null); - } catch(AblyException e) { - assertEquals(null, message.encoding); - assertEquals(data, message.data); - assertEquals(1, capturedLog.size()); - LogLine capturedLine = capturedLog.get(0); - assertTrue(capturedLine.tag.contains("ably")); - assertTrue(capturedLine.msg.contains("Message data must be either `byte[]`, `String` or `JSONElement`; implicit coercion of other types to String is deprecated")); - } catch(Throwable t) { - fail("reject_invalid_message_data: Unexpected exception"); - } finally { - Log.setHandler(originalLogHandler); - Log.setLevel(originalLogLevel); - } - } - - public static class LogLine { - public int severity; - public String tag; - public String msg; - public Throwable tr; - - public LogLine(int severity, String tag, String msg, Throwable tr) { - this.severity = severity; - this.tag = tag; - this.msg = msg; - this.tr = tr; - } - } - - /** - * To Test Message.fromEncoded(JsonObject, ChannelOptions) and Message.fromEncoded(String, ChannelOptions) - * Refer Spec TM3 - * @throws AblyException - */ - @Test - public void message_from_encoded_json_object() throws AblyException { - /*Test Base64 data decoding in Message.fromEncoded(JsonObject)*/ - Message sendMsg = new Message("test_from_encoded_method", "0123456789".getBytes()); - sendMsg.clientId = "client-id"; - sendMsg.connectionId = "connection-id"; - sendMsg.timestamp = System.currentTimeMillis(); - sendMsg.encode(null); - - Message receivedMsg = Message.fromEncoded(Serialisation.gson.toJsonTree(sendMsg).getAsJsonObject(), null); - assertEquals(receivedMsg.name, sendMsg.name); - assertArrayEquals((byte[]) receivedMsg.data, "0123456789".getBytes()); - - /*Test JSON Data decoding in Message.fromEncoded(JsonObject)*/ - JsonObject person = new JsonObject(); - person.addProperty("name", "Amit"); - person.addProperty("country", "Interlaken Ost"); - - Message userDetails = new Message("user_details", person); - userDetails.encode(null); - Message decodedMessage1 = Message.fromEncoded(Serialisation.gson.toJsonTree(userDetails).getAsJsonObject(), null); - - assertEquals(userDetails.name, decodedMessage1.name); - assertEquals(person, decodedMessage1.data); - - /*Test Message.fromEncoded(String)*/ - Message decodedMessage2 = Message.fromEncoded(Serialisation.gson.toJson(userDetails), null); - assertEquals(userDetails.name, decodedMessage2.name); - assertEquals(person, decodedMessage2.data); - - /*Test invalid case.*/ - try { - //We pass invalid Message object - Message.fromEncoded("[]", null); - fail(); - } catch(Exception e) {/*ignore as we are expecting it to fail.*/} - } - - /** - * To test Message.fromEncodedArray(JsonArray, ChannelOptions) and Message.fromEncodedArray(String, ChannelOptions) - * Refer Spec. TM3 - * @throws AblyException - */ - @Test - public void messages_from_encoded_json_array() throws AblyException { - JsonArray fixtures = null; - MessagesData testMessages = null; - try { - testMessages = (MessagesData) Setup.loadJson(testMessagesEncodingFile, MessagesData.class); - JsonObject jsonObject = (JsonObject) Setup.loadJson(testMessagesEncodingFile, JsonObject.class); - //We use this as-is for decoding purposes. - fixtures = jsonObject.getAsJsonArray("messages"); - } catch(IOException e) { - fail(); - return; - } - - Message[] decodedMessages = Message.fromEncodedArray(fixtures, null); - for(int index = 0; index < decodedMessages.length; index++) { - Message testInputMsg = testMessages.messages[index]; - testInputMsg.decode(null); - if(testInputMsg.data instanceof byte[]) { - assertArrayEquals((byte[]) testInputMsg.data, (byte[]) decodedMessages[index].data); - } else { - assertEquals(testInputMsg.data, decodedMessages[index].data); - } - } - /*Test Message.fromEncodedArray(String)*/ - String fixturesArray = Serialisation.gson.toJson(fixtures); - Message[] decodedMessages2 = Message.fromEncodedArray(fixturesArray, null); - for(int index = 0; index < decodedMessages2.length; index++) { - Message testInputMsg = testMessages.messages[index]; - if(testInputMsg.data instanceof byte[]) { - assertArrayEquals((byte[]) testInputMsg.data, (byte[]) decodedMessages2[index].data); - } else { - assertEquals(testInputMsg.data, decodedMessages2[index].data); - } - } - } - - static class MessagesData { - public Message[] messages; - } - - /** - * Publish a message that contains extras of arbitrary creation. Validate that when we receive that message - * echoed back from the service that those extras remain intact. - * - * @see RSL6a2 - */ - @Test - public void opaque_message_extras() throws AblyException { - AblyRealtime ably = null; - try { - final JsonObject opaqueJson = new JsonObject(); - opaqueJson.addProperty("Some Property", "Lorem Ipsum"); - opaqueJson.addProperty("Some Truth", false); - opaqueJson.addProperty("Some Number", 321); - - final MessageExtras extras = new MessageExtras(opaqueJson); - final Message message = new Message(); - message.name = "The Test Message"; - message.data = "Some Value"; - message.extras = extras; - - final ClientOptions clientOptions = createOptions(testVars.keys[0].keyStr); - ably = new AblyRealtime(clientOptions); - - // create a channel and attach to it - final Channel channel = ably.channels.get(createChannelName("opaque_message_extras")); - channel.attach(); - (new ChannelWaiter(channel)).waitFor(ChannelState.attached); - assertEquals(ChannelState.attached, channel.state); - - // subscribe - final MessageWaiter messageWaiter = new MessageWaiter(channel); - - // publish and await success - final CompletionWaiter completionWaiter = new CompletionWaiter(); - channel.publish(message, completionWaiter); - completionWaiter.waitFor(); - assertTrue(completionWaiter.success); - - // wait for the subscriber to receive the message - messageWaiter.waitFor(1); - assertEquals(1, messageWaiter.receivedMessages.size()); - - // validate the contents of the received message - final Message received = messageWaiter.receivedMessages.get(0); - assertEquals("The Test Message", received.name); - assertEquals("Some Value", received.data); - assertEquals(extras, received.extras); - } finally { - if(ably != null) { - ably.close(); - } - } - } + private static final String testMessagesEncodingFile = "ably-common/test-resources/messages-encoding.json"; + private static Gson gson = new Gson(); + + @Rule + public Timeout testTimeout = Timeout.seconds(300); + + /** + * Connect to the service and attach, subscribe to an event, and publish on that channel + */ + @Test + public void single_send() { + AblyRealtime ably = null; + try { + ClientOptions opts = createOptions(testVars.keys[0].keyStr); + ably = new AblyRealtime(opts); + + /* create a channel */ + final Channel channel = ably.channels.get("subscribe_send_binary"); + + /* attach */ + channel.attach(); + (new ChannelWaiter(channel)).waitFor(ChannelState.attached); + assertEquals("Verify attached state reached", channel.state, ChannelState.attached); + + /* subscribe */ + MessageWaiter messageWaiter = new MessageWaiter(channel); + + /* publish to the channel */ + CompletionWaiter msgComplete = new CompletionWaiter(); + channel.publish("test_event", "Test message (subscribe_send_binary)", msgComplete); + + /* wait for the publish callback to be called */ + msgComplete.waitFor(); + assertTrue("Verify success callback was called", msgComplete.success); + + /* wait for the subscription callback to be called */ + messageWaiter.waitFor(1); + assertEquals("Verify message subscription was called", messageWaiter.receivedMessages.size(), 1); + + } catch(AblyException e) { + e.printStackTrace(); + fail("init0: Unexpected exception instantiating library"); + } finally { + if(ably != null) + ably.close(); + } + } + + /** + * Connect to the service on two connections; + * attach, subscribe to an event, publish on one + * connection and confirm receipt on the other. + */ + @Test + public void single_send_noecho() { + AblyRealtime txAbly = null; + AblyRealtime rxAbly = null; + try { + ClientOptions opts = createOptions(testVars.keys[0].keyStr); + opts.echoMessages = false; + txAbly = new AblyRealtime(opts); + rxAbly = new AblyRealtime(opts); + String channelName = "subscribe_send_binary_noecho"; + + /* create a channel */ + final Channel txChannel = txAbly.channels.get(channelName); + final Channel rxChannel = rxAbly.channels.get(channelName); + + /* attach both connections */ + txChannel.attach(); + (new ChannelWaiter(txChannel)).waitFor(ChannelState.attached); + assertEquals("Verify attached state reached", txChannel.state, ChannelState.attached); + rxChannel.attach(); + (new ChannelWaiter(rxChannel)).waitFor(ChannelState.attached); + assertEquals("Verify attached state reached", rxChannel.state, ChannelState.attached); + + /* subscribe on both connections */ + MessageWaiter txMessageWaiter = new MessageWaiter(txChannel); + MessageWaiter rxMessageWaiter = new MessageWaiter(rxChannel); + + /* publish to the channel */ + CompletionWaiter msgComplete = new CompletionWaiter(); + txChannel.publish("test_event", "Test message (subscribe_send_binary_noecho)", msgComplete); + + /* wait for the publish callback to be called */ + msgComplete.waitFor(); + assertTrue("Verify success callback was called", msgComplete.success); + + /* wait for the subscription callback to be called */ + rxMessageWaiter.waitFor(1); + assertEquals("Verify rx message subscription was called", rxMessageWaiter.receivedMessages.size(), 1); + + /* wait to verify that the subscription callback is not called on txConnection */ + txMessageWaiter.waitFor(1, 1000L); + assertEquals("Verify tx message subscription was not called", txMessageWaiter.receivedMessages.size(), 0); + + } catch(AblyException e) { + e.printStackTrace(); + fail("single_send_binary_noecho: Unexpected exception instantiating library"); + } finally { + if(txAbly != null) + txAbly.close(); + if(rxAbly != null) + rxAbly.close(); + } + } + + /** + * Get a channel and subscribe without explicitly attaching. + * Verify that the channel reaches the attached state. + */ + @Test + public void subscribe_implicit_attach() { + AblyRealtime ably = null; + String channelName = "subscribe_implicit_attach_" + testParams.name; + try { + ClientOptions opts = createOptions(testVars.keys[0].keyStr); + ably = new AblyRealtime(opts); + + /* create a channel */ + final Channel channel = ably.channels.get(channelName); + + /* subscribe */ + MessageWaiter messageWaiter = new MessageWaiter(channel); + + /* verify attached state is reached */ + (new ChannelWaiter(channel)).waitFor(ChannelState.attached); + assertEquals("Verify attached state reached", channel.state, ChannelState.attached); + + /* publish to the channel */ + CompletionWaiter msgComplete = new CompletionWaiter(); + channel.publish("test_event", "Test message (" + channelName + ")", msgComplete); + + /* wait for the publish callback to be called */ + msgComplete.waitFor(); + assertTrue("Verify success callback was called", msgComplete.success); + + /* wait for the subscription callback to be called */ + messageWaiter.waitFor(1); + assertEquals("Verify message subscription was called", messageWaiter.receivedMessages.size(), 1); + + } catch(AblyException e) { + e.printStackTrace(); + fail("init0: Unexpected exception instantiating library"); + } finally { + if(ably != null) + ably.close(); + } + } + + /** + * Connect to the service using the default (binary) protocol + * and attach, subscribe to an event, and publish multiple + * messages on that channel + */ + private void _multiple_send(String channelName, int messageCount, int msgSize, boolean binary, long delay) { + AblyRealtime ably = null; + try { + ClientOptions opts = createOptions(testVars.keys[0].keyStr); + ably = new AblyRealtime(opts); + + /* create a channel */ + final Channel channel = ably.channels.get(channelName); + + /* attach */ + channel.attach(); + (new ChannelWaiter(channel)).waitFor(ChannelState.attached); + assertEquals("Verify attached state reached", channel.state, ChannelState.attached); + + /* subscribe */ + MessageWaiter messageWaiter = new MessageWaiter(channel); + + /* publish to the channel */ + CompletionSet msgComplete = new CompletionSet(); + if(binary) { + byte[][] messagesSent = new byte[messageCount][]; + for(int i = 0; i < messageCount; i++) { + byte[] messageData = messagesSent[i] = Helpers.RandomGenerator.generateRandomBuffer(msgSize); + channel.publish("test_event", messageData, msgComplete.add()); + try { + Thread.sleep(delay); + } catch(InterruptedException e) { + } + } + + /* wait for the publish callback to be called */ + ErrorInfo[] errors = msgComplete.waitFor(); + assertTrue("Verify success from all message callbacks", errors.length == 0); + + /* wait for the subscription callback to be called */ + messageWaiter.waitFor(messageCount); + + /* verify received message content */ + List receivedMessages = messageWaiter.receivedMessages; + assertEquals("Verify message subscriptions all called", receivedMessages.size(), messageCount); + for(int i = 0; i < messageCount; i++) { + assertArrayEquals("Verify expected message contents", messagesSent[i], (byte[]) receivedMessages.get(i).data); + } + } else { + String[] messagesSent = new String[messageCount]; + for(int i = 0; i < messageCount; i++) { + String messageData = messagesSent[i] = Helpers.RandomGenerator.generateRandomString(msgSize); + channel.publish("test_event", messageData, msgComplete.add()); + try { + Thread.sleep(delay); + } catch(InterruptedException e) { + } + } + + /* wait for the publish callback to be called */ + ErrorInfo[] errors = msgComplete.waitFor(); + assertTrue("Verify success from all message callbacks", errors.length == 0); + + /* wait for the subscription callback to be called */ + messageWaiter.waitFor(messageCount); + + /* verify received message content */ + List receivedMessages = messageWaiter.receivedMessages; + assertEquals("Verify message subscriptions all called", receivedMessages.size(), messageCount); + for(int i = 0; i < messageCount; i++) { + assertEquals("Verify expected message contents", messagesSent[i], (String) receivedMessages.get(i).data); + } + } + + } catch(AblyException e) { + e.printStackTrace(); + fail("channelName: Unexpected exception instantiating library"); + } finally { + if(ably != null) + ably.close(); + } + } + + /* + * Test right and wrong channel states to publish messages + * Tests RTL6c + */ + @Test + public void publish_channel_state() { + AblyRealtime ably = null; + try { + ClientOptions opts = createOptions(testVars.keys[0].keyStr); + ably = new AblyRealtime(opts); + + Channel pubChannel = ably.channels.get("publish_channel_state"); + ChannelWaiter channelWaiter = new ChannelWaiter(pubChannel); + pubChannel.attach(); + + /* Publish in attaching state */ + pubChannel.publish(new Message("name1", "data1")); + + channelWaiter.waitFor(ChannelState.attached); + + /* Go to suspended state */ + ably.connection.connectionManager.requestState(ConnectionState.suspended); + channelWaiter.waitFor(ChannelState.suspended); + + boolean error = false; + try { + pubChannel.publish(new Message("name2", "data2")); + } catch(AblyException e) { + error = true; + } + assertTrue("Verify exception was thrown on publishing in suspended state", error); + + /* reconnect and try again */ + ably.connection.connectionManager.requestState(ConnectionState.connecting); + channelWaiter.waitFor(ChannelState.attached); + + pubChannel.publish(new Message("name3", "data3")); + + /* fail connection */ + ably.connection.connectionManager.requestState(ConnectionState.failed); + channelWaiter.waitFor(ChannelState.failed); + error = false; + try { + pubChannel.publish(new Message("name4", "data4")); + } catch(AblyException e) { + error = true; + } + assertTrue("Verify exception was thrown on publishing in failed state", error); + + } catch(AblyException e) { + e.printStackTrace(); + fail("Unexpected exception"); + } finally { + if(ably != null) + ably.close(); + } + + } + + /** + * Connect to the service using the default (binary) protocol + * and attach, subscribe to an event, and publish multiple + * messages on that channel + */ + private void _multiple_send_batch(String channelName, int messageCount, int batchCount, long batchDelay) { + AblyRealtime ably = null; + try { + ClientOptions opts = createOptions(testVars.keys[0].keyStr); + ably = new AblyRealtime(opts); + + /* create a channel */ + final Channel channel = ably.channels.get(channelName); + + /* attach */ + channel.attach(); + (new ChannelWaiter(channel)).waitFor(ChannelState.attached); + assertEquals("Verify attached state reached", channel.state, ChannelState.attached); + + /* subscribe */ + MessageWaiter messageWaiter = new MessageWaiter(channel); + + /* publish to the channel */ + CompletionSet msgComplete = new CompletionSet(); + for(int i = 0; i < (int) (messageCount / batchCount); i++) { + for(int j = 0; j < batchCount; j++) { + channel.publish("test_event", "Test message (_multiple_send_batch) " + i * batchCount + j, msgComplete.add()); + } + try { + Thread.sleep(batchDelay); + } catch(InterruptedException e) {/*ignore*/} + } + + /* wait for the publish callback to be called */ + ErrorInfo[] errors = msgComplete.waitFor(); + assertTrue("Verify success from all message callbacks", errors.length == 0); + + /* wait for the subscription callback to be called */ + messageWaiter.waitFor(messageCount); + assertEquals("Verify message subscriptions all called", messageWaiter.receivedMessages.size(), messageCount); + + } catch(AblyException e) { + e.printStackTrace(); + fail("channelName: Unexpected exception instantiating library"); + } finally { + if(ably != null) + ably.close(); + } + } + + @Test + public void multiple_send_10_1000_16_string() { + int messageCount = 10; + long delay = 1000L; + _multiple_send("multiple_send_10_1000_16_string_" + testParams.name, messageCount, 16, false, delay); + } + + @Test + public void multiple_send_10_1000_16_binary() { + int messageCount = 10; + long delay = 1000L; + _multiple_send("multiple_send_10_1000_16_binary_" + testParams.name, messageCount, 16, true, delay); + } + + @Test + public void multiple_send_10_1000_512_string() { + int messageCount = 10; + long delay = 1000L; + _multiple_send("multiple_send_10_1000_512_string_" + testParams.name, messageCount, 512, false, delay); + } + + @Test + public void multiple_send_10_1000_512_binary() { + int messageCount = 10; + long delay = 1000L; + _multiple_send("multiple_send_10_1000_512_binary_" + testParams.name, messageCount, 512, true, delay); + } + + @Test + public void multiple_send_20_200() { + int messageCount = 20; + long delay = 200L; + _multiple_send("multiple_send_20_200_" + testParams.name, messageCount, 256, true, delay); + } + + @Test + public void multiple_send_200_50() { + int messageCount = 200; + long delay = 50L; + _multiple_send("multiple_send_binary_200_50_" + testParams.name, messageCount, 256, true, delay); + } + + @Test + public void multiple_send_1000_10() { + int messageCount = 1000; + long delay = 10L; + _multiple_send("multiple_send_binary_1000_10_" + testParams.name, messageCount, 256, true, delay); + } + + /** + * Connect to the service + * using credentials that are unable to publish,and attach. + * Attempt to publish and verify that an error is received. + */ + @Test + public void single_error() { + AblyRealtime ably = null; + try { + ClientOptions opts = createOptions(testVars.keys[4].keyStr); + ably = new AblyRealtime(opts); + + /* create a channel; channel3 can subscribe but not publish + * with this key */ + final Channel channel = ably.channels.get("channel3"); + + /* attach */ + channel.attach(); + (new ChannelWaiter(channel)).waitFor(ChannelState.attached); + assertEquals("Verify attached state reached", channel.state, ChannelState.attached); + + /* publish to the channel */ + CompletionWaiter msgComplete = new CompletionWaiter(); + channel.publish("test_event", "Test message (single_error_binary)", msgComplete); + + /* wait for the publish callback to be called */ + ErrorInfo fail = msgComplete.waitFor(); + assertEquals("Verify error callback was called", fail.statusCode, 401); + + } catch(AblyException e) { + e.printStackTrace(); + fail("single_error_binary: Unexpected exception instantiating library"); + } finally { + if(ably != null) + ably.close(); + } + } + + @Test + public void ensure_disconnect_with_error_does_not_move_to_failed() { + AblyRealtime ably = null; + try { + ClientOptions opts = createOptions(testVars.keys[0].keyStr); + ably = new AblyRealtime(opts); + + /* wait until connected */ + (new ConnectionWaiter(ably.connection)).waitFor(ConnectionState.connected); + assertEquals("Verify connected state reached", ably.connection.state, ConnectionState.connected); + + ProtocolMessage protoMessage = new ProtocolMessage(ProtocolMessage.Action.disconnect); + protoMessage.error = new ErrorInfo("test error", 123); + + ConnectionManager connectionManager = ably.connection.connectionManager; + connectionManager.onMessage(null, protoMessage); + + // On disconnected we retry right away since we're connected, so we can only + // check that the state is not failed. + assertNotEquals("connection state should not be failed", ably.connection.state, ConnectionState.failed); + } catch(AblyException e) { + e.printStackTrace(); + fail("init0: Unexpected exception instantiating library"); + } finally { + if(ably != null) + ably.close(); + } + } + + @Test + public void messages_encoding_fixtures() { + MessagesEncodingData fixtures; + try { + fixtures = (MessagesEncodingData) Setup.loadJson(testMessagesEncodingFile, MessagesEncodingData.class); + } catch(IOException e) { + fail(); + return; + } + + AblyRealtime ably = null; + try { + ClientOptions opts = createOptions(testVars.keys[0].keyStr); + ably = new AblyRealtime(opts); + final Channel channel = ably.channels.get("test"); + + channel.attach(); + (new ChannelWaiter(channel)).waitFor(ChannelState.attached); + assertEquals("Verify attached state reached", channel.state, ChannelState.attached); + + for(MessagesEncodingDataItem fixtureMessage : fixtures.messages) { + /* subscribe */ + MessageWaiter messageWaiter = new MessageWaiter(channel); + + HttpHelpers.postSync(ably.http, "/channels/" + channel.name + "/messages", null, null, new HttpUtils.JsonRequestBody(fixtureMessage), null, true); + + messageWaiter.waitFor(1); + channel.unsubscribe(messageWaiter); + + Message receivedMessage = messageWaiter.receivedMessages.get(0); + + expectDataToMatch(fixtureMessage, receivedMessage); + + CompletionWaiter msgComplete = new CompletionWaiter(); + channel.publish(receivedMessage, msgComplete); + msgComplete.waitFor(); + + MessagesEncodingDataItem persistedMessage = ably.http.request(new Http.Execute() { + @Override + public void execute(HttpScheduler http, Callback callback) throws AblyException { + http.get("/channels/" + channel.name + "/messages?limit=1", null, null, new HttpCore.ResponseHandler() { + @Override + public MessagesEncodingDataItem[] handleResponse(HttpCore.Response response, ErrorInfo error) throws AblyException { + if(error != null) { + throw AblyException.fromErrorInfo(error); + } + return gson.fromJson(new String(response.body), MessagesEncodingDataItem[].class); + } + }, true, callback); + } + }).sync()[0]; + + assertEquals("Verify persisted message encoding", fixtureMessage.encoding, persistedMessage.encoding); + assertEquals("Verify persisted message data", fixtureMessage.data, persistedMessage.data); + } + } catch(AblyException e) { + e.printStackTrace(); + fail("init0: Unexpected exception instantiating library"); + } finally { + if(ably != null) + ably.close(); + } + } + + @Test + public void messages_msgpack_and_json_encoding_is_compatible() { + MessagesEncodingData fixtures; + try { + fixtures = (MessagesEncodingData) Setup.loadJson(testMessagesEncodingFile, MessagesEncodingData.class); + } catch(IOException e) { + fail(); + return; + } + + // Publish each data type through raw JSON POST and retrieve through MsgPack and JSON. + + AblyRealtime realtimeSubscribeClientMsgPack = null; + AblyRealtime realtimeSubscribeClientJson = null; + try { + ClientOptions jsonOpts = createOptions(testVars.keys[0].keyStr); + + ClientOptions msgpackOpts = createOptions(testVars.keys[0].keyStr); + msgpackOpts.useBinaryProtocol = !testParams.useBinaryProtocol; + + AblyRest restPublishClient = new AblyRest(jsonOpts); + realtimeSubscribeClientMsgPack = new AblyRealtime(msgpackOpts); + realtimeSubscribeClientJson = new AblyRealtime(jsonOpts); + + final Channel realtimeSubscribeChannelMsgPack = realtimeSubscribeClientMsgPack.channels.get("test-subscribe"); + final Channel realtimeSubscribeChannelJson = realtimeSubscribeClientJson.channels.get("test-subscribe"); + + for(Channel realtimeSubscribeChannel : new Channel[]{realtimeSubscribeChannelMsgPack, realtimeSubscribeChannelJson}) { + realtimeSubscribeChannel.attach(); + (new ChannelWaiter(realtimeSubscribeChannel)).waitFor(ChannelState.attached); + assertEquals("Verify attached state reached", realtimeSubscribeChannel.state, ChannelState.attached); + + for(MessagesEncodingDataItem fixtureMessage : fixtures.messages) { + MessageWaiter messageWaiter = new MessageWaiter(realtimeSubscribeChannel); + + HttpHelpers.postSync(restPublishClient.http, "/channels/" + realtimeSubscribeChannel.name + "/messages", null, null, new HttpUtils.JsonRequestBody(fixtureMessage), null, true); + + messageWaiter.waitFor(1); + realtimeSubscribeChannel.unsubscribe(messageWaiter); + + Message receivedMessage = messageWaiter.receivedMessages.get(0); + + expectDataToMatch(fixtureMessage, receivedMessage); + } + } + + for(AblyRealtime realtimeSubscribeClient : new AblyRealtime[]{realtimeSubscribeClientMsgPack, realtimeSubscribeClientJson}) { + realtimeSubscribeClient.close(); + realtimeSubscribeClient = null; + } + + // Publish each data type through MsgPack and JSON and retrieve through raw JSON GET. + + AblyRest restPublishClientMsgPack = new AblyRest(msgpackOpts); + AblyRest restPublishClientJson = new AblyRest(jsonOpts); + AblyRest restRetrieveClient = new AblyRest(jsonOpts); + + final io.ably.lib.rest.Channel restPublishChannelMsgPack = restPublishClientMsgPack.channels.get("test-publish"); + final io.ably.lib.rest.Channel restPublishChannelJson = restPublishClientJson.channels.get("test-publish"); + + for(MessagesEncodingDataItem fixtureMessage : fixtures.messages) { + Object data = fixtureMessage.expectedValue; + if(fixtureMessage.expectedHexValue != null) { + data = hexStringToByteArray(fixtureMessage.expectedHexValue); + } else if(data instanceof JsonPrimitive) { + data = ((JsonPrimitive) data).getAsString(); + } + + for(final io.ably.lib.rest.Channel restPublishChannel : new io.ably.lib.rest.Channel[]{restPublishChannelMsgPack, restPublishChannelJson}) { + restPublishChannel.publish("event", data); + + MessagesEncodingDataItem persistedMessage = restRetrieveClient.http.request(new Http.Execute() { + @Override + public void execute(HttpScheduler http, Callback callback) throws AblyException { + http.get("/channels/" + restPublishChannel.name + "/messages?limit=1", null, null, new HttpCore.ResponseHandler() { + @Override + public MessagesEncodingDataItem[] handleResponse(HttpCore.Response response, ErrorInfo error) throws AblyException { + if(error != null) { + throw AblyException.fromErrorInfo(error); + } + return gson.fromJson(new String(response.body), MessagesEncodingDataItem[].class); + } + }, true, callback); + } + }).sync()[0]; + + assertEquals("Verify persisted message encoding", fixtureMessage.encoding, persistedMessage.encoding); + assertEquals("Verify persisted message data", fixtureMessage.data, persistedMessage.data); + } + } + } catch(AblyException e) { + e.printStackTrace(); + fail("init0: Unexpected exception instantiating library"); + } finally { + if(realtimeSubscribeClientMsgPack != null) + realtimeSubscribeClientMsgPack.close(); + if(realtimeSubscribeClientJson != null) + realtimeSubscribeClientJson.close(); + } + } + + /** + * Test behaviour when message is encoded as encrypted but encryption is not set up + */ + @Test + public void message_inconsistent_encoding() { + AblyRealtime realtimeSubscribeClient = null; + final ArrayList log = new ArrayList<>(); + + try { + ClientOptions apiOptions = createOptions(testVars.keys[0].keyStr); + apiOptions.logHandler = new Log.LogHandler() { + @Override + public void println(int severity, String tag, String msg, Throwable tr) { + synchronized(log) { + log.add(String.format(Locale.US, "%s: %s", tag, msg)); + } + } + }; + apiOptions.logLevel = Log.INFO; + + AblyRest restPublishClient = new AblyRest(apiOptions); + realtimeSubscribeClient = new AblyRealtime(apiOptions); + + final Channel realtimeSubscribeChannelJson = realtimeSubscribeClient.channels.get("test-encoding"); + + realtimeSubscribeChannelJson.attach(); + (new ChannelWaiter(realtimeSubscribeChannelJson)).waitFor(ChannelState.attached); + assertEquals("Verify attached state reached", realtimeSubscribeChannelJson.state, ChannelState.attached); + + MessageWaiter messageWaiter = new MessageWaiter(realtimeSubscribeChannelJson); + + MessagesEncodingDataItem testData = new MessagesEncodingDataItem(); + testData.data = "MDEyMzQ1Njc4OQ=="; /* Base64("0123456789") */ + testData.encoding = "utf-8/cipher+aes-128-cbc/base64"; + testData.expectedType = "binary"; + testData.expectedHexValue = "30313233343536373839"; /* hex for "0123456789" */ + + HttpHelpers.postSync(restPublishClient.http, "/channels/" + realtimeSubscribeChannelJson.name + "/messages", null, null, new HttpUtils.JsonRequestBody(testData), null, true); + + messageWaiter.waitFor(1); + realtimeSubscribeChannelJson.unsubscribe(messageWaiter); + + Message receivedMessage = messageWaiter.receivedMessages.get(0); + + expectDataToMatch(testData, receivedMessage); + assertEquals("Verify resulting encoding", receivedMessage.encoding, "utf-8/cipher+aes-128-cbc"); + + synchronized(log) { + boolean foundErrorMessage = false; + for(String logMessage : log) { + if(logMessage.contains("encryption is not set up")) + foundErrorMessage = true; + } + assertTrue("Verify logged error messages", foundErrorMessage); + } + } catch(AblyException e) { + e.printStackTrace(); + fail("init0: Unexpected exception instantiating library"); + } finally { + if(realtimeSubscribeClient != null) + realtimeSubscribeClient.close(); + } + } + + private void expectDataToMatch(MessagesEncodingDataItem fixtureMessage, Message receivedMessage) { + if(fixtureMessage.expectedType.equals("string")) { + assertEquals("Verify decoded message data", fixtureMessage.expectedValue.getAsString(), receivedMessage.data); + } else if(fixtureMessage.expectedType.equals("jsonObject")) { + assertEquals("Verify decoded message data", fixtureMessage.expectedValue.getAsJsonObject(), receivedMessage.data); + } else if(fixtureMessage.expectedType.equals("jsonArray")) { + assertEquals("Verify decoded message data", fixtureMessage.expectedValue.getAsJsonArray(), receivedMessage.data); + } else if(fixtureMessage.expectedType.equals("binary")) { + byte[] receivedData = (byte[]) receivedMessage.data; + StringBuilder sb = new StringBuilder(receivedData.length * 2); + for(byte b : receivedData) { + sb.append(String.format("%02x", b & 0xff)); + } + String receivedDataHex = sb.toString(); + assertEquals("Verify decoded message data", fixtureMessage.expectedHexValue, receivedDataHex); + } else { + throw new RuntimeException(String.format("unhandled: %s", fixtureMessage.expectedType)); + } + } + + public static byte[] hexStringToByteArray(String s) { + int len = s.length(); + byte[] data = new byte[len / 2]; + for(int i = 0; i < len; i += 2) { + data[i / 2] = (byte) ((Character.digit(s.charAt(i), 16) << 4) + + Character.digit(s.charAt(i + 1), 16)); + } + return data; + } + + static class MessagesEncodingData { + public MessagesEncodingDataItem[] messages; + } + + static class MessagesEncodingDataItem { + public String data; + public String encoding; + public String expectedType; + public JsonElement expectedValue; + public String expectedHexValue; + } + + @Test + public void reject_invalid_message_data() throws AblyException { + HashMap data = new HashMap(); + Message message = new Message("event", data); + Log.LogHandler originalLogHandler = Log.handler; + int originalLogLevel = Log.level; + Log.setLevel(Log.DEBUG); + final ArrayList capturedLog = new ArrayList<>(); + Log.setHandler(new Log.LogHandler() { + @Override + public void println(int severity, String tag, String msg, Throwable tr) { + capturedLog.add(new LogLine(severity, tag, msg, tr)); + } + }); + + try { + message.encode(null); + } catch(AblyException e) { + assertEquals(null, message.encoding); + assertEquals(data, message.data); + assertEquals(1, capturedLog.size()); + LogLine capturedLine = capturedLog.get(0); + assertTrue(capturedLine.tag.contains("ably")); + assertTrue(capturedLine.msg.contains("Message data must be either `byte[]`, `String` or `JSONElement`; implicit coercion of other types to String is deprecated")); + } catch(Throwable t) { + fail("reject_invalid_message_data: Unexpected exception"); + } finally { + Log.setHandler(originalLogHandler); + Log.setLevel(originalLogLevel); + } + } + + public static class LogLine { + public int severity; + public String tag; + public String msg; + public Throwable tr; + + public LogLine(int severity, String tag, String msg, Throwable tr) { + this.severity = severity; + this.tag = tag; + this.msg = msg; + this.tr = tr; + } + } + + /** + * To Test Message.fromEncoded(JsonObject, ChannelOptions) and Message.fromEncoded(String, ChannelOptions) + * Refer Spec TM3 + * @throws AblyException + */ + @Test + public void message_from_encoded_json_object() throws AblyException { + /*Test Base64 data decoding in Message.fromEncoded(JsonObject)*/ + Message sendMsg = new Message("test_from_encoded_method", "0123456789".getBytes()); + sendMsg.clientId = "client-id"; + sendMsg.connectionId = "connection-id"; + sendMsg.timestamp = System.currentTimeMillis(); + sendMsg.encode(null); + + Message receivedMsg = Message.fromEncoded(Serialisation.gson.toJsonTree(sendMsg).getAsJsonObject(), null); + assertEquals(receivedMsg.name, sendMsg.name); + assertArrayEquals((byte[]) receivedMsg.data, "0123456789".getBytes()); + + /*Test JSON Data decoding in Message.fromEncoded(JsonObject)*/ + JsonObject person = new JsonObject(); + person.addProperty("name", "Amit"); + person.addProperty("country", "Interlaken Ost"); + + Message userDetails = new Message("user_details", person); + userDetails.encode(null); + Message decodedMessage1 = Message.fromEncoded(Serialisation.gson.toJsonTree(userDetails).getAsJsonObject(), null); + + assertEquals(userDetails.name, decodedMessage1.name); + assertEquals(person, decodedMessage1.data); + + /*Test Message.fromEncoded(String)*/ + Message decodedMessage2 = Message.fromEncoded(Serialisation.gson.toJson(userDetails), null); + assertEquals(userDetails.name, decodedMessage2.name); + assertEquals(person, decodedMessage2.data); + + /*Test invalid case.*/ + try { + //We pass invalid Message object + Message.fromEncoded("[]", null); + fail(); + } catch(Exception e) {/*ignore as we are expecting it to fail.*/} + } + + /** + * To test Message.fromEncodedArray(JsonArray, ChannelOptions) and Message.fromEncodedArray(String, ChannelOptions) + * Refer Spec. TM3 + * @throws AblyException + */ + @Test + public void messages_from_encoded_json_array() throws AblyException { + JsonArray fixtures = null; + MessagesData testMessages = null; + try { + testMessages = (MessagesData) Setup.loadJson(testMessagesEncodingFile, MessagesData.class); + JsonObject jsonObject = (JsonObject) Setup.loadJson(testMessagesEncodingFile, JsonObject.class); + //We use this as-is for decoding purposes. + fixtures = jsonObject.getAsJsonArray("messages"); + } catch(IOException e) { + fail(); + return; + } + + Message[] decodedMessages = Message.fromEncodedArray(fixtures, null); + for(int index = 0; index < decodedMessages.length; index++) { + Message testInputMsg = testMessages.messages[index]; + testInputMsg.decode(null); + if(testInputMsg.data instanceof byte[]) { + assertArrayEquals((byte[]) testInputMsg.data, (byte[]) decodedMessages[index].data); + } else { + assertEquals(testInputMsg.data, decodedMessages[index].data); + } + } + /*Test Message.fromEncodedArray(String)*/ + String fixturesArray = Serialisation.gson.toJson(fixtures); + Message[] decodedMessages2 = Message.fromEncodedArray(fixturesArray, null); + for(int index = 0; index < decodedMessages2.length; index++) { + Message testInputMsg = testMessages.messages[index]; + if(testInputMsg.data instanceof byte[]) { + assertArrayEquals((byte[]) testInputMsg.data, (byte[]) decodedMessages2[index].data); + } else { + assertEquals(testInputMsg.data, decodedMessages2[index].data); + } + } + } + + static class MessagesData { + public Message[] messages; + } + + /** + * Publish a message that contains extras of arbitrary creation. Validate that when we receive that message + * echoed back from the service that those extras remain intact. + * + * @see RSL6a2 + */ + @Test + public void opaque_message_extras() throws AblyException { + AblyRealtime ably = null; + try { + final JsonObject opaqueJson = new JsonObject(); + opaqueJson.addProperty("Some Property", "Lorem Ipsum"); + opaqueJson.addProperty("Some Truth", false); + opaqueJson.addProperty("Some Number", 321); + + final MessageExtras extras = new MessageExtras(opaqueJson); + final Message message = new Message(); + message.name = "The Test Message"; + message.data = "Some Value"; + message.extras = extras; + + final ClientOptions clientOptions = createOptions(testVars.keys[0].keyStr); + ably = new AblyRealtime(clientOptions); + + // create a channel and attach to it + final Channel channel = ably.channels.get(createChannelName("opaque_message_extras")); + channel.attach(); + (new ChannelWaiter(channel)).waitFor(ChannelState.attached); + assertEquals(ChannelState.attached, channel.state); + + // subscribe + final MessageWaiter messageWaiter = new MessageWaiter(channel); + + // publish and await success + final CompletionWaiter completionWaiter = new CompletionWaiter(); + channel.publish(message, completionWaiter); + completionWaiter.waitFor(); + assertTrue(completionWaiter.success); + + // wait for the subscriber to receive the message + messageWaiter.waitFor(1); + assertEquals(1, messageWaiter.receivedMessages.size()); + + // validate the contents of the received message + final Message received = messageWaiter.receivedMessages.get(0); + assertEquals("The Test Message", received.name); + assertEquals("Some Value", received.data); + assertEquals(extras, received.extras); + } finally { + if(ably != null) { + ably.close(); + } + } + } } diff --git a/lib/src/test/java/io/ably/lib/test/realtime/RealtimePresenceHistoryTest.java b/lib/src/test/java/io/ably/lib/test/realtime/RealtimePresenceHistoryTest.java index eef3a240c..6a0e07f2d 100644 --- a/lib/src/test/java/io/ably/lib/test/realtime/RealtimePresenceHistoryTest.java +++ b/lib/src/test/java/io/ably/lib/test/realtime/RealtimePresenceHistoryTest.java @@ -39,1261 +39,1261 @@ public class RealtimePresenceHistoryTest extends ParameterizedTest { - private static final String testClientId = "testClientId"; - private long timeOffset; - private AblyRest rest; - private Auth.TokenDetails token; - - @Rule - public Timeout testTimeout = Timeout.seconds(300); - - @Before - public void setUpBefore() throws Exception { - /* create tokens for specific clientIds */ - ClientOptions opts = createOptions(testVars.keys[0].keyStr); - rest = new AblyRest(opts); - token = rest.auth.requestToken(new TokenParams() {{ clientId = testClientId; }}, null); - - /* sync */ - long timeFromService = rest.time(); - timeOffset = timeFromService - System.currentTimeMillis(); - } - - /** - * Send a single message on a channel and verify that it can be - * retrieved using channel.history() without needing to wait for - * it to be persisted. - */ - @Test - public void presencehistory_simple() { - AblyRealtime ably = null; - try { - ClientOptions rtOpts = createOptions(); - rtOpts.token = token.token; - rtOpts.clientId = testClientId; - ably = new AblyRealtime(rtOpts); - - String channelName = "persisted:presencehistory_simple_" + testParams.name; - String messageText = "Test message (presencehistory_simple)"; - - /* create a channel */ - final Channel channel = ably.channels.get(channelName); - - /* attach */ - channel.attach(); - (new ChannelWaiter(channel)).waitFor(ChannelState.attached); - assertEquals("Verify attached state reached", channel.state, ChannelState.attached); - - /* enter the channel */ - CompletionWaiter msgComplete = new CompletionWaiter(); - channel.presence.enter(messageText, msgComplete); - - /* wait for the enter callback to be called */ - msgComplete.waitFor(); - assertTrue("Verify success callback was called", msgComplete.success); - - /* get the presence history for this channel */ - PaginatedResult messages = channel.presence.history(null); - assertNotNull("Expected non-null messages", messages); - assertEquals("Expected 1 message", messages.items().length, 1); - - /* verify message contents */ - assertEquals("Expect correct message text", messages.items()[0].data, messageText); - } catch (AblyException e) { - e.printStackTrace(); - fail("single_history_binary: Unexpected exception instantiating library"); - } finally { - if(ably != null) - ably.close(); - } - } - - /** - * Publish events with data of various datatypes - */ - @Test - public void presencehistory_types() { - AblyRealtime ably = null; - try { - ClientOptions rtOpts = createOptions(); - rtOpts.token = token.token; - rtOpts.clientId = testClientId; - ably = new AblyRealtime(rtOpts); - - String channelName = "persisted:presencehistory_types_" + testParams.name; - - /* create a channel */ - final Channel channel = ably.channels.get(channelName); - - /* attach */ - channel.attach(); - (new ChannelWaiter(channel)).waitFor(ChannelState.attached); - assertEquals("Verify attached state reached", channel.state, ChannelState.attached); - - /* publish enter events to the channel */ - CompletionWaiter msgComplete = new CompletionWaiter(); - - channel.presence.enter("This is a string message payload", msgComplete); - channel.presence.enter("This is a byte[] message payload".getBytes(), msgComplete); - - /* wait for the enter callback to be called */ - msgComplete.waitFor(2); - assertTrue("Verify success callback was called", msgComplete.success); - - /* get the history for this channel */ - PaginatedResult messages = channel.presence.history(null); - assertNotNull("Expected non-null messages", messages); - assertEquals("Expected 2 messages", messages.items().length, 2); - - /* verify message contents and order */ - assertEquals("Expect messages.asArray()[1] to be expected String", messages.items()[1].data, "This is a string message payload"); - assertEquals("Expect messages.asArray()[0] to be expected byte[]", new String((byte[])messages.items()[0].data), "This is a byte[] message payload"); - } catch (AblyException e) { - e.printStackTrace(); - fail("presencehistory_types: Unexpected exception"); - } finally { - if(ably != null) - ably.close(); - } - } - - /** - * Publish events with data of various datatypes - */ - @Test - public void presencehistory_types_forward() { - AblyRealtime ably = null; - try { - ClientOptions rtOpts = createOptions(); - rtOpts.token = token.token; - rtOpts.clientId = testClientId; - ably = new AblyRealtime(rtOpts); - - String channelName = "persisted:presencehistory_types_forward_" + testParams.name; - - /* create a channel */ - final Channel channel = ably.channels.get(channelName); - - /* attach */ - channel.attach(); - (new ChannelWaiter(channel)).waitFor(ChannelState.attached); - assertEquals("Verify attached state reached", channel.state, ChannelState.attached); - - /* publish enter events to the channel */ - CompletionWaiter msgComplete = new CompletionWaiter(); - - channel.presence.enter("This is a string message payload", msgComplete); - channel.presence.enter("This is a byte[] message payload".getBytes(), msgComplete); - - /* wait for the enter callback to be called */ - msgComplete.waitFor(2); - assertTrue("Verify success callback was called", msgComplete.success); - - /* get the history for this channel */ - PaginatedResult messages = channel.presence.history(new Param[]{new Param("direction", "forwards")}); - assertNotNull("Expected non-null messages", messages); - assertEquals("Expected 2 messages", messages.items().length, 2); - - /* verify message contents and order */ - assertEquals("Expect messages.asArray()[0] to be expected String", messages.items()[0].data, "This is a string message payload"); - assertEquals("Expect messages.asArray()[1] to be expected byte[]", new String((byte[])messages.items()[1].data), "This is a byte[] message payload"); - } catch (AblyException e) { - e.printStackTrace(); - fail("presencehistory_types: Unexpected exception"); - } finally { - if(ably != null) - ably.close(); - } - } - - /** - * Connect twice to the service, each using the default (binary) protocol. - * Publish messages on one connection to a given channel; then attach - * the second connection to the same channel and verify a complete message - * history can be obtained. - */ - @Test - public void presencehistory_second_channel() { - AblyRealtime txAbly = null, rxAbly = null; - try { - ClientOptions txOpts = createOptions(); - txOpts.token = token.token; - txOpts.clientId = testClientId; - txAbly = new AblyRealtime(txOpts); - - ClientOptions rxOpts = createOptions(testVars.keys[0].keyStr); - rxAbly = new AblyRealtime(rxOpts); - String channelName = "persisted:presencehistory_second_channel_" + testParams.name; - - /* create a channel */ - final Channel txChannel = txAbly.channels.get(channelName); - final Channel rxChannel = rxAbly.channels.get(channelName); - - /* attach sender */ - txChannel.attach(); - (new ChannelWaiter(txChannel)).waitFor(ChannelState.attached); - assertEquals("Verify attached state reached", txChannel.state, ChannelState.attached); - - /* enter the channel */ - String messageText = "Test message (presencehistory_second_channel)"; - CompletionWaiter msgComplete = new CompletionWaiter(); - txChannel.presence.enter(messageText, msgComplete); - - /* wait for the enter callback to be called */ - msgComplete.waitFor(); - assertTrue("Verify success callback was called", msgComplete.success); - - /* attach receiver */ - rxChannel.attach(); - (new ChannelWaiter(rxChannel)).waitFor(ChannelState.attached); - assertEquals("Verify attached state reached", rxChannel.state, ChannelState.attached); - - /* get the history for this channel */ - PaginatedResult messages = rxChannel.presence.history(null); - assertNotNull("Expected non-null messages", messages); - assertEquals("Expected 1 message", messages.items().length, 1); - - /* verify message contents */ - assertEquals("Expect correct message text", messages.items()[0].data, messageText); - - } catch (AblyException e) { - e.printStackTrace(); - fail("presencehistory_second_channel: Unexpected exception instantiating library"); - } finally { - if(txAbly != null) - txAbly.close(); - if(rxAbly != null) - rxAbly.close(); - } - } - - /** - * Send a single message on a channel and verify that it can be - * retrieved using channel.history() after waiting for it to be - * persisted. - */ - @Test - public void presencehistory_wait_b() { - AblyRealtime ably = null; - try { - ClientOptions rtOpts = createOptions(); - rtOpts.token = token.token; - rtOpts.clientId = testClientId; - ably = new AblyRealtime(rtOpts); - String channelName = "persisted:presencehistory_wait_b_" + testParams.name; - String messageText = "Test message (presencehistory_wait_b)"; - - /* create a channel */ - final Channel channel = ably.channels.get(channelName); - - /* attach */ - channel.attach(); - (new ChannelWaiter(channel)).waitFor(ChannelState.attached); - assertEquals("Verify attached state reached", channel.state, ChannelState.attached); - - /* enter the channel */ - CompletionWaiter msgComplete = new CompletionWaiter(); - channel.presence.enter(messageText, msgComplete); - - /* wait for the publish callback to be called */ - msgComplete.waitFor(); - assertTrue("Verify success callback was called", msgComplete.success); - - /* wait for the history to be persisted */ - try { - Thread.sleep(16000); - } catch(InterruptedException ie) {} - - /* get the history for this channel */ - PaginatedResult messages = channel.presence.history(null); - assertNotNull("Expected non-null messages", messages); - assertEquals("Expected 1 message", messages.items().length, 1); - - /* verify message contents */ - assertEquals("Expect correct message text", messages.items()[0].data, messageText); - } catch (AblyException e) { - e.printStackTrace(); - fail("single_history_binary: Unexpected exception instantiating library"); - } finally { - if(ably != null) - ably.close(); - } - } - - /** - * Send a single message on a channel and verify that it can be - * retrieved using channel.history(direction=forwards) after waiting - * for it to be persisted. - */ - @Test - public void presencehistory_wait_f() { - AblyRealtime ably = null; - try { - ClientOptions rtOpts = createOptions(); - rtOpts.token = token.token; - rtOpts.clientId = testClientId; - ably = new AblyRealtime(rtOpts); - String channelName = "persisted:presencehistory_wait_f_" + testParams.name; - String messageText = "Test message (presencehistory_wait_f)"; - - /* create a channel */ - final Channel channel = ably.channels.get(channelName); - - /* attach */ - channel.attach(); - (new ChannelWaiter(channel)).waitFor(ChannelState.attached); - assertEquals("Verify attached state reached", channel.state, ChannelState.attached); - - /* publish to the channel */ - CompletionWaiter msgComplete = new CompletionWaiter(); - channel.presence.enter(messageText, msgComplete); - - /* wait for the publish callback to be called */ - msgComplete.waitFor(); - assertTrue("Verify success callback was called", msgComplete.success); - - /* wait for the history to be persisted */ - try { - Thread.sleep(16000); - } catch(InterruptedException ie) {} - - /* get the history for this channel */ - PaginatedResult messages = channel.presence.history(new Param[]{ new Param("direction", "forwards") }); - assertNotNull("Expected non-null messages", messages); - assertEquals("Expected 1 message", messages.items().length, 1); - - /* verify message contents */ - assertEquals("Expect correct message text", messages.items()[0].data, messageText); - } catch (AblyException e) { - e.printStackTrace(); - fail("presencehistory_wait_binary_f: Unexpected exception instantiating library"); - } finally { - if(ably != null) - ably.close(); - } - } - - /** - * Send a single presence message on a channel, wait enough time for it to - * persist, then send a second message. Verify that both can be - * retrieved using channel.history() without any further wait. - */ - @Test - public void presencehistory_mixed_b() { - AblyRealtime ably = null; - try { - ClientOptions rtOpts = createOptions(); - rtOpts.token = token.token; - rtOpts.clientId = testClientId; - ably = new AblyRealtime(rtOpts); - String channelName = "persisted:presencehistory_mixed_b_" + testParams.name; - String persistMessageText = "test_event (persisted)"; - String liveMessageText = "test_event (live)"; - - /* create a channel */ - final Channel channel = ably.channels.get(channelName); - - /* attach */ - channel.attach(); - (new ChannelWaiter(channel)).waitFor(ChannelState.attached); - assertEquals("Verify attached state reached", channel.state, ChannelState.attached); - - /* publish to the channel */ - CompletionWaiter msgComplete = new CompletionWaiter(); - channel.presence.enter(persistMessageText, msgComplete); - - /* wait for the history to be persisted */ - try { - Thread.sleep(16000); - } catch(InterruptedException ie) {} - - /* publish to the channel */ - channel.presence.enter(liveMessageText, msgComplete); - - /* wait for the publish callback to be called */ - msgComplete.waitFor(2); - assertTrue("Verify success callback was called", msgComplete.success); - - /* get the history for this channel */ - PaginatedResult messages = channel.presence.history(null); - assertNotNull("Expected non-null messages", messages); - assertEquals("Expected 2 messages", messages.items().length, 2); - - /* verify message contents */ - assertEquals("Expect correct message data", messages.items()[0].data, liveMessageText); - assertEquals("Expect correct message data", messages.items()[1].data, persistMessageText); - - } catch (AblyException e) { - e.printStackTrace(); - fail("presencehistory_mixed_binary_b: Unexpected exception instantiating library"); - } finally { - if(ably != null) - ably.close(); - } - } - - /** - * Send a single message on a channel, wait enough time for it to - * persist, then send a second message. Verify that both can be - * retrieved using channel.history(direction=forwards) without any - * further wait. - */ - @Test - public void presencehistory_mixed_f() { - AblyRealtime ably = null; - try { - ClientOptions rtOpts = createOptions(); - rtOpts.token = token.token; - rtOpts.clientId = testClientId; - ably = new AblyRealtime(rtOpts); - String channelName = "persisted:presencehistory_mixed_f_" + testParams.name; - String persistMessageText = "test_event (persisted)"; - String liveMessageText = "test_event (live)"; - - /* create a channel */ - final Channel channel = ably.channels.get(channelName); - - /* attach */ - channel.attach(); - (new ChannelWaiter(channel)).waitFor(ChannelState.attached); - assertEquals("Verify attached state reached", channel.state, ChannelState.attached); - - /* publish to the channel */ - CompletionWaiter msgComplete = new CompletionWaiter(); - channel.presence.enter(persistMessageText, msgComplete); - - /* wait for the history to be persisted */ - try { - Thread.sleep(16000); - } catch(InterruptedException ie) {} - - /* publish to the channel */ - channel.presence.enter(liveMessageText, msgComplete); - - /* wait for the publish callback to be called */ - msgComplete.waitFor(2); - assertTrue("Verify success callback was called", msgComplete.success); - - /* get the history for this channel */ - PaginatedResult messages = channel.presence.history(new Param[]{ new Param("direction", "forwards") }); - assertNotNull("Expected non-null messages", messages); - assertEquals("Expected 2 messages", messages.items().length, 2); - - /* verify message contents */ - assertEquals("Expect correct message data", messages.items()[0].data, persistMessageText); - assertEquals("Expect correct message data", messages.items()[1].data, liveMessageText); - - } catch (AblyException e) { - e.printStackTrace(); - fail("presencehistory_mixed_binary_f: Unexpected exception instantiating library"); - } finally { - if(ably != null) - ably.close(); - } - } - - /** - * Publish events, get limited history and check expected order (forwards) - */ - @Test - public void presencehistory_limit_f() { - AblyRealtime ably = null; - try { - ClientOptions rtOpts = createOptions(); - rtOpts.token = token.token; - rtOpts.clientId = testClientId; - ably = new AblyRealtime(rtOpts); - String channelName = "persisted:presencehistory_limit_f_" + testParams.name; - - /* create a channel */ - final Channel channel = ably.channels.get(channelName); - - /* attach */ - channel.attach(); - (new ChannelWaiter(channel)).waitFor(ChannelState.attached); - assertEquals("Verify attached state reached", channel.state, ChannelState.attached); - - /* publish to the channel */ - CompletionSet msgComplete = new CompletionSet(); - for(int i = 0; i < 50; i++) { - try { - channel.presence.enter(String.valueOf(i), msgComplete.add()); - } catch(AblyException e) { - e.printStackTrace(); - fail("presencehistory_limit_f: Unexpected exception"); - return; - } - } - - /* wait for the publish callbacks to be called */ - msgComplete.waitFor(); - assertTrue("Verify success callback was called", msgComplete.errors.isEmpty()); - - /* get the history for this channel */ - PaginatedResult messages = channel.presence.history(new Param[] { new Param("direction", "forwards"), new Param("limit", "25") }); - assertNotNull("Expected non-null messages", messages); - assertEquals("Expected 25 messages", messages.items().length, 25); - - /* verify message order */ - for(int i = 0; i < 25; i++) - assertEquals("Expect correct message data", messages.items()[i].data, String.valueOf(i)); - - } catch (AblyException e) { - e.printStackTrace(); - fail("presencehistory_limit_f: Unexpected exception"); - } finally { - if(ably != null) - ably.close(); - } - } - - /** - * Publish events, get limited history and check expected order (backwards) - */ - @Test - public void presencehistory_limit_b() { - AblyRealtime ably = null; - try { - ClientOptions rtOpts = createOptions(); - rtOpts.token = token.token; - rtOpts.clientId = testClientId; - ably = new AblyRealtime(rtOpts); - String channelName = "persisted:presencehistory_limit_b_" + testParams.name; - - /* create a channel */ - final Channel channel = ably.channels.get(channelName); - - /* attach */ - channel.attach(); - (new ChannelWaiter(channel)).waitFor(ChannelState.attached); - assertEquals("Verify attached state reached", channel.state, ChannelState.attached); - - /* publish to the channel */ - CompletionSet msgComplete = new CompletionSet(); - for(int i = 0; i < 50; i++) { - try { - channel.presence.enter(String.valueOf(i), msgComplete.add()); - } catch(AblyException e) { - e.printStackTrace(); - fail("presencehistory_limit_b: Unexpected exception"); - return; - } - } - - /* wait for the publish callbacks to be called */ - msgComplete.waitFor(); - assertTrue("Verify success callback was called", msgComplete.errors.isEmpty()); - - /* get the history for this channel */ - PaginatedResult messages = channel.presence.history(new Param[] { new Param("direction", "backwards"), new Param("limit", "25") }); - assertNotNull("Expected non-null messages", messages); - assertEquals("Expected 25 messages", messages.items().length, 25); - - /* verify message order */ - for(int i = 0; i < 25; i++) - assertEquals("Expect correct message data", messages.items()[i].data, String.valueOf(49 - i)); - - } catch (AblyException e) { - e.printStackTrace(); - fail("presencehistory_limit_b: Unexpected exception"); - return; - } finally { - if(ably != null) - ably.close(); - } - } - - /** - * Publish events and check expected history based on time slice (forwards) - */ - @Test - public void presencehistory_time_f() { - AblyRealtime ably = null; - try { - /* first, publish some messages */ - long intervalStart = 0, intervalEnd = 0; - ClientOptions rtOpts = createOptions(); - rtOpts.token = token.token; - rtOpts.clientId = testClientId; - ably = new AblyRealtime(rtOpts); - String channelName = "persisted:presencehistory_time_f_" + testParams.name; - - /* create a channel */ - final Channel channel = ably.channels.get(channelName); - - /* attach */ - channel.attach(); - (new ChannelWaiter(channel)).waitFor(ChannelState.attached); - assertEquals("Verify attached state reached", channel.state, ChannelState.attached); - - /* send batches of messages with short inter-message delay */ - CompletionSet msgComplete = new CompletionSet(); - for(int i = 0; i < 20; i++) { - channel.presence.enter(String.valueOf(i), msgComplete.add()); - Thread.sleep(100L); - } - Thread.sleep(1000L); - intervalStart = timeOffset + System.currentTimeMillis(); - for(int i = 20; i < 40; i++) { - channel.presence.enter(String.valueOf(i), msgComplete.add()); - Thread.sleep(100L); - } - intervalEnd = timeOffset + System.currentTimeMillis() - 1; - Thread.sleep(1000L); - for(int i = 40; i < 60; i++) { - channel.presence.enter(String.valueOf(i), msgComplete.add()); - Thread.sleep(100L); - } - - /* get the history for this channel */ - PaginatedResult messages = channel.presence.history(new Param[] { - new Param("direction", "forwards"), - new Param("start", String.valueOf(intervalStart - 500)), - new Param("end", String.valueOf(intervalEnd + 500)) - }); - assertNotNull("Expected non-null messages", messages); - assertEquals("Expected 20 messages", messages.items().length, 20); - - /* verify message order */ - for(int i = 20; i < 40; i++) - assertEquals("Expect correct message data", messages.items()[i - 20].data, String.valueOf(i)); - - } catch (AblyException e) { - e.printStackTrace(); - fail("presencehistory_time_f: Unexpected exception"); - } catch (InterruptedException e) { - e.printStackTrace(); - fail("presencehistory_time_f: Unexpected exception"); - } finally { - if(ably != null) - ably.close(); - } - } - - /** - * Publish events and check expected history based on time slice (backwards) - */ - @Test - public void presencehistory_time_b() { - AblyRealtime ably = null; - try { - /* first, publish some messages */ - long intervalStart = 0, intervalEnd = 0; - ClientOptions rtOpts = createOptions(); - rtOpts.token = token.token; - rtOpts.clientId = testClientId; - ably = new AblyRealtime(rtOpts); - String channelName = "persisted:presencehistory_time_b_" + testParams.name; - - /* create a channel */ - final Channel channel = ably.channels.get(channelName); - - /* attach */ - channel.attach(); - (new ChannelWaiter(channel)).waitFor(ChannelState.attached); - assertEquals("Verify attached state reached", channel.state, ChannelState.attached); - - /* send batches of messages with shprt inter-message delay */ - CompletionSet msgComplete = new CompletionSet(); - for(int i = 0; i < 20; i++) { - channel.presence.enter(String.valueOf(i), msgComplete.add()); - Thread.sleep(100L); - } - Thread.sleep(1000L); - intervalStart = timeOffset + System.currentTimeMillis(); - for(int i = 20; i < 40; i++) { - channel.presence.enter(String.valueOf(i), msgComplete.add()); - Thread.sleep(100L); - } - intervalEnd = timeOffset + System.currentTimeMillis() - 1; - Thread.sleep(1000L); - for(int i = 40; i < 60; i++) { - channel.presence.enter(String.valueOf(i), msgComplete.add()); - Thread.sleep(100L); - } - - /* get the history for this channel */ - PaginatedResult messages = channel.presence.history(new Param[] { - new Param("direction", "backwards"), - new Param("start", String.valueOf(intervalStart - 500)), - new Param("end", String.valueOf(intervalEnd + 500)) - }); - assertNotNull("Expected non-null messages", messages); - assertEquals("Expected 20 messages", messages.items().length, 20); - - /* verify message order */ - for(int i = 20; i < 40; i++) - assertEquals("Expect correct message data", messages.items()[i - 20].data, String.valueOf(59 - i)); - - } catch (AblyException e) { - e.printStackTrace(); - fail("presencehistory_time_b: Unexpected exception"); - } catch (InterruptedException e) { - e.printStackTrace(); - fail("presencehistory_time_b: Unexpected exception"); - } finally { - if(ably != null) - ably.close(); - } - } - - /** - * Check query pagination (forwards) - */ - @Test - public void presencehistory_paginate_f() { - AblyRealtime ably = null; - try { - ClientOptions rtOpts = createOptions(); - rtOpts.token = token.token; - rtOpts.clientId = testClientId; - ably = new AblyRealtime(rtOpts); - String channelName = "persisted:presencehistory_paginate_f_" + testParams.name; - - /* create a channel */ - final Channel channel = ably.channels.get(channelName); - - /* attach */ - channel.attach(); - (new ChannelWaiter(channel)).waitFor(ChannelState.attached); - assertEquals("Verify attached state reached", channel.state, ChannelState.attached); - - /* publish to the channel */ - CompletionSet msgComplete = new CompletionSet(); - for(int i = 0; i < 50; i++) { - try { - channel.presence.enter(String.valueOf(i), msgComplete.add()); - } catch(AblyException e) { - e.printStackTrace(); - fail("presencehistory_paginate_f: Unexpected exception"); - return; - } - } - - /* wait for the publish callbacks to be called */ - msgComplete.waitFor(); - assertTrue("Verify success callback was called", msgComplete.errors.isEmpty()); - - /* get the history for this channel */ - PaginatedResult messages = channel.presence.history(new Param[] { new Param("direction", "forwards"), new Param("limit", "10") }); - assertNotNull("Expected non-null messages", messages); - assertEquals("Expected 10 messages", messages.items().length, 10); - - /* verify message order */ - for(int i = 0; i < 10; i++) - assertEquals("Expect correct message data", messages.items()[i].data, String.valueOf(i)); - - /* get next page */ - messages = messages.next(); - assertNotNull("Expected non-null messages", messages); - assertEquals("Expected 10 messages", messages.items().length, 10); - - /* verify message order */ - for(int i = 0; i < 10; i++) - assertEquals("Expect correct message data", messages.items()[i].data, String.valueOf(i + 10)); - - /* get next page */ - messages = messages.next(); - assertNotNull("Expected non-null messages", messages); - assertEquals("Expected 10 messages", messages.items().length, 10); - - /* verify message order */ - for(int i = 0; i < 10; i++) - assertEquals("Expect correct message data", messages.items()[i].data, String.valueOf(i + 20)); - - } catch (AblyException e) { - e.printStackTrace(); - fail("presencehistory_paginate_f: Unexpected exception"); - } finally { - if(ably != null) - ably.close(); - } - } - - /** - * Check query pagination (backwards) - */ - @Test - public void presencehistory_paginate_b() { - AblyRealtime ably = null; - try { - ClientOptions rtOpts = createOptions(); - rtOpts.token = token.token; - rtOpts.clientId = testClientId; - ably = new AblyRealtime(rtOpts); - String channelName = "persisted:presencehistory_paginate_b_" + testParams.name; - - /* create a channel */ - final Channel channel = ably.channels.get(channelName); - - /* attach */ - channel.attach(); - (new ChannelWaiter(channel)).waitFor(ChannelState.attached); - assertEquals("Verify attached state reached", channel.state, ChannelState.attached); - - /* publish to the channel */ - CompletionSet msgComplete = new CompletionSet(); - for(int i = 0; i < 50; i++) { - try { - channel.presence.enter(String.valueOf(i), msgComplete.add()); - } catch(AblyException e) { - e.printStackTrace(); - fail("presencehistory_paginate_f: Unexpected exception"); - return; - } - } - - /* wait for the publish callbacks to be called */ - msgComplete.waitFor(); - assertTrue("Verify success callback was called", msgComplete.errors.isEmpty()); - - /* get the history for this channel */ - PaginatedResult messages = channel.presence.history(new Param[] { new Param("direction", "backwards"), new Param("limit", "10") }); - assertNotNull("Expected non-null messages", messages); - assertEquals("Expected 10 messages", messages.items().length, 10); - - /* verify message order */ - for(int i = 0; i < 10; i++) - assertEquals("Expect correct message data", messages.items()[i].data, String.valueOf(49 - i)); - - /* get next page */ - messages = messages.next(); - assertNotNull("Expected non-null messages", messages); - assertEquals("Expected 10 messages", messages.items().length, 10); - - /* verify message order */ - for(int i = 0; i < 10; i++) - assertEquals("Expect correct message data", messages.items()[i].data, String.valueOf(39 - i)); - - /* get next page */ - messages = messages.next(); - assertNotNull("Expected non-null messages", messages); - assertEquals("Expected 10 messages", messages.items().length, 10); - - /* verify message order */ - for(int i = 0; i < 10; i++) - assertEquals("Expect correct message data", messages.items()[i].data, String.valueOf(29 - i)); - - } catch (AblyException e) { - e.printStackTrace(); - fail("presencehistory_paginate_b: Unexpected exception"); - } finally { - if(ably != null) - ably.close(); - } - } - - /** - * Check query pagination "rel=first" (forwards) - */ - @Test - public void presencehistory_paginate_first_f() { - AblyRealtime ably = null; - try { - ClientOptions rtOpts = createOptions(); - rtOpts.token = token.token; - rtOpts.clientId = testClientId; - ably = new AblyRealtime(rtOpts); - String channelName = "persisted:presencehistory_paginate_first_f_" + testParams.name; - - /* create a channel */ - final Channel channel = ably.channels.get(channelName); - - /* attach */ - channel.attach(); - (new ChannelWaiter(channel)).waitFor(ChannelState.attached); - assertEquals("Verify attached state reached", channel.state, ChannelState.attached); - - /* publish to the channel */ - CompletionSet msgComplete = new CompletionSet(); - for(int i = 0; i < 50; i++) { - try { - channel.presence.enter(String.valueOf(i), msgComplete.add()); - } catch(AblyException e) { - e.printStackTrace(); - fail("presencehistory_paginate_f: Unexpected exception"); - return; - } - } - - /* wait for the publish callbacks to be called */ - msgComplete.waitFor(); - assertTrue("Verify success callback was called", msgComplete.errors.isEmpty()); - - /* get the history for this channel */ - PaginatedResult messages = channel.presence.history(new Param[] { new Param("direction", "forwards"), new Param("limit", "10") }); - assertNotNull("Expected non-null messages", messages); - assertEquals("Expected 10 messages", messages.items().length, 10); - - /* verify message order */ - for(int i = 0; i < 10; i++) - assertEquals("Expect correct message data", messages.items()[i].data, String.valueOf(i)); - - /* get next page */ - messages = messages.next(); - assertNotNull("Expected non-null messages", messages); - assertEquals("Expected 10 messages", messages.items().length, 10); - - /* verify message order */ - for(int i = 0; i < 10; i++) - assertEquals("Expect correct message data", messages.items()[i].data, String.valueOf(i + 10)); - - /* get first page */ - messages = messages.first(); - assertNotNull("Expected non-null messages", messages); - assertEquals("Expected 10 messages", messages.items().length, 10); - - /* verify message order */ - for(int i = 0; i < 10; i++) - assertEquals("Expect correct message data", messages.items()[i].data, String.valueOf(i)); - - } catch (AblyException e) { - e.printStackTrace(); - fail("presencehistory_paginate_first_f: Unexpected exception"); - } finally { - if(ably != null) - ably.close(); - } - } - - /** - * Check query pagination "rel=first" (backwards) - */ - @Test - public void presencehistory_paginate_first_b() { - AblyRealtime ably = null; - try { - ClientOptions rtOpts = createOptions(); - rtOpts.token = token.token; - rtOpts.clientId = testClientId; - ably = new AblyRealtime(rtOpts); - String channelName = "persisted:presencehistory_paginate_first_b_" + testParams.name; - - /* create a channel */ - final Channel channel = ably.channels.get(channelName); - - /* attach */ - channel.attach(); - (new ChannelWaiter(channel)).waitFor(ChannelState.attached); - assertEquals("Verify attached state reached", channel.state, ChannelState.attached); - - /* publish to the channel */ - CompletionSet msgComplete = new CompletionSet(); - for(int i = 0; i < 50; i++) { - try { - channel.presence.enter(String.valueOf(i), msgComplete.add()); - } catch(AblyException e) { - e.printStackTrace(); - fail("presencehistory_paginate_f: Unexpected exception"); - return; - } - } - - /* wait for the publish callbacks to be called */ - msgComplete.waitFor(); - assertTrue("Verify success callback was called", msgComplete.errors.isEmpty()); - - /* get the history for this channel */ - PaginatedResult messages = channel.presence.history(new Param[] { new Param("direction", "backwards"), new Param("limit", "10") }); - assertNotNull("Expected non-null messages", messages); - assertEquals("Expected 10 messages", messages.items().length, 10); - - /* verify message order */ - for(int i = 0; i < 10; i++) - assertEquals("Expect correct message data", messages.items()[i].data, String.valueOf(49 - i)); - - /* get next page */ - messages = messages.next(); - assertNotNull("Expected non-null messages", messages); - assertEquals("Expected 10 messages", messages.items().length, 10); - - /* verify message order */ - for(int i = 0; i < 10; i++) - assertEquals("Expect correct message data", messages.items()[i].data, String.valueOf(39 - i)); - - /* get first page */ - messages = messages.first(); - assertNotNull("Expected non-null messages", messages); - assertEquals("Expected 10 messages", messages.items().length, 10); - - /* verify message order */ - for(int i = 0; i < 10; i++) - assertEquals("Expect correct message data", messages.items()[i].data, String.valueOf(49 - i)); - - } catch (AblyException e) { - e.printStackTrace(); - fail("presencehistory_paginate_first_b: Unexpected exception"); - } finally { - if(ably != null) - ably.close(); - } - } - - /** - * Connect twice to the service. - * Publish messages on one connection to a given channel; while in progress, - * attach the second connection to the same channel and verify a message - * history up to the point of attachment can be obtained. - */ - @Test - @Ignore("Fails due to issues in sandbox. See https://github.com/ably/realtime/issues/1845 for details.") - public void presencehistory_from_attach() { - AblyRealtime txAbly = null, rxAbly = null; - try { - ClientOptions txOpts = createOptions(); - txOpts.token = token.token; - txOpts.clientId = testClientId; - txAbly = new AblyRealtime(txOpts); - - DebugOptions rxOpts = new DebugOptions(testVars.keys[0].keyStr); - fillInOptions(rxOpts); - RawProtocolMonitor rawPresenceWaiter = RawProtocolMonitor.createReceiver(Action.presence); - rxOpts.protocolListener = rawPresenceWaiter; - rxAbly = new AblyRealtime(rxOpts); - String channelName = "persisted:presencehistory_from_attach_" + testParams.name; - - /* create a channel */ - final Channel txChannel = txAbly.channels.get(channelName); - final Channel rxChannel = rxAbly.channels.get(channelName); - - /* attach sender */ - txChannel.attach(); - (new ChannelWaiter(txChannel)).waitFor(ChannelState.attached); - assertEquals("Verify attached state reached", txChannel.state, ChannelState.attached); - - /* publish messages to the channel */ - final CompletionSet msgComplete = new CompletionSet(); - Thread publisherThread = new Thread() { - @Override - public void run() { - for(int i = 0; i < 50; i++) { - try { - txChannel.presence.enter(String.valueOf(i), msgComplete.add()); - try { - sleep(100L); - } catch(InterruptedException ie) {} - } catch(AblyException e) { - e.printStackTrace(); - fail("presencehistory_from_attach: Unexpected exception"); - return; - } - } - msgComplete.waitFor(); - } - }; - publisherThread.start(); - - /* wait 2 seconds */ - try { - Thread.sleep(2000L); - } catch(InterruptedException ie) { - fail("presencehistory_from_attach: exception in publisher thread"); - } - - /* subscribe; this will trigger the attach */ - PresenceWaiter presenceWaiter = new PresenceWaiter(rxChannel); - - /* get the channel history from the attachSerial when we get the attach indication */ - (new ChannelWaiter(rxChannel)).waitFor(ChannelState.attached); - assertEquals("Verify attached state reached", rxChannel.state, ChannelState.attached); - assertNotNull("Verify attachSerial provided", rxChannel.properties.attachSerial); - - /* the subscription callback will be called first on the "sync" presence message - * delivered immediately following attach; so wait for this and then the first - * "realtime" message to be received */ - presenceWaiter.waitFor(2); - PresenceMessage firstReceivedRealtimeMessage = null; - for(ProtocolMessage msg : rawPresenceWaiter.receivedMessages) { - if(msg.channelSerial != null) { - firstReceivedRealtimeMessage = msg.presence[0]; - break; - } - } - - /* wait for the end of the tx thread */ - try { - publisherThread.join(); - } catch (InterruptedException e) {} - assertTrue("Verify success callback was called", msgComplete.errors.isEmpty()); - - /* get the history for this channel */ - PaginatedResult messages = rxChannel.presence.history(new Param[] { new Param("from_serial", rxChannel.properties.attachSerial)}); - assertNotNull("Expected non-null messages", messages); - assertTrue("Expected at least one message", messages.items().length >= 1); - - /* verify that the history and received messages meet */ - int earliestReceivedOnConnection = Integer.valueOf((String)firstReceivedRealtimeMessage.data); - int latestReceivedInHistory = Integer.valueOf((String)messages.items()[0].data); - assertEquals("Verify that the history and received messages meet", earliestReceivedOnConnection, latestReceivedInHistory + 1); - - } catch (AblyException e) { - e.printStackTrace(); - fail("presencehistory_from_attach: Unexpected exception instantiating library"); - } finally { - if(txAbly != null) - txAbly.close(); - if(rxAbly != null) - rxAbly.close(); - } - } - - /** - * Connect twice to the service, each using the default (binary) protocol. - * Publish messages on one connection to a given channel; while in progress, - * attach the second connection to the same channel and verify a message - * history up to the point of attachment can be obtained. - */ - @Test - public void presencehistory_until_attach() { - AblyRealtime txAbly = null, rxAbly = null; - try { - ClientOptions txOpts = createOptions(); - txOpts.token = token.token; - txOpts.clientId = testClientId; - txAbly = new AblyRealtime(txOpts); - - DebugOptions rxOpts = new DebugOptions(testVars.keys[0].keyStr); - fillInOptions(rxOpts); - RawProtocolMonitor rawPresenceWaiter = RawProtocolMonitor.createReceiver(Action.presence); - rxOpts.protocolListener = rawPresenceWaiter; - rxAbly = new AblyRealtime(rxOpts); - String channelName = "persisted:presencehistory_until_attach_" + testParams.name; - - /* create a channel */ - final Channel txChannel = txAbly.channels.get(channelName); - final Channel rxChannel = rxAbly.channels.get(channelName); - - /* attach sender */ - txChannel.attach(); - (new ChannelWaiter(txChannel)).waitFor(ChannelState.attached); - assertEquals("Verify attached state reached", txChannel.state, ChannelState.attached); - - /* publish messages to the channel */ - CompletionSet msgComplete = new CompletionSet(); - int messageCount = 25; - for (int i = 0; i < messageCount; i++) { - txChannel.presence.enter(String.valueOf(i), msgComplete.add()); - } - - msgComplete.waitFor(); - - /* get the channel history from the attachSerial when we get the attach indication */ - rxChannel.attach(); - new ChannelWaiter(rxChannel).waitFor(ChannelState.attached); - assertEquals("Verify attached state reached", rxChannel.state, ChannelState.attached); - assertNotNull("Verify attachSerial provided", rxChannel.properties.attachSerial); - - /* get the history for this channel */ - PaginatedResult messages = rxChannel.presence.history(new Param[] { new Param("untilAttach", "true") }); - assertNotNull("Expected non-null messages", messages); - assertTrue("Expected at least one message", messages.items().length >= 1); - - /* verify that the history and received messages meet */ - for (int i = 0; i < messageCount; i++) { - /* 0 --> "24" - * 1 --> "23" - * ... - * 24 --> "0" - */ - String actual = (String) messages.items()[messageCount - 1 - i].data; - String expected = String.valueOf(i); - assertThat(actual, is(equalTo(expected))); - } - } catch (AblyException e) { - e.printStackTrace(); - fail("presencehistory_from_attach: Unexpected exception instantiating library"); - } finally { - if(txAbly != null) - txAbly.close(); - if(rxAbly != null) - rxAbly.close(); - } - } - - /** - * Verifies an Exception is thrown, when a presence history is requested - * with parameter {"untilAttach":"true}" before client is attached to the channel - * - * @throws AblyException - */ - @Test(expected=AblyException.class) - public void presencehistory_until_attach_before_attached() throws AblyException { - ClientOptions options = createOptions(testVars.keys[0].keyStr); - AblyRealtime ably = new AblyRealtime(options); - - ably.channels.get("test").presence.history(new Param[]{ new Param("untilAttach", "true")}); - ably.close(); - } - - /** - * Verifies an Exception is thrown, when a presence history is requested - * with invalid "untilAttach" parameter value. - * - * @throws AblyException - */ - @Test(expected=AblyException.class) - public void presencehistory_until_attach_invalid_value() throws AblyException { - ClientOptions options = createOptions(testVars.keys[0].keyStr); - AblyRealtime ably = new AblyRealtime(options); - - ably.channels.get("test").presence.history(new Param[]{ new Param("untilAttach", "affirmative")}); - ably.close(); - } - - /** - * Publish enough presence to fill 2 pages. - * Verify that, - * - {@code PaginatedQuery#isLast} returns false, when we are at the first page. - * - {@code PaginatedQuery#isLast} returns true, when we are at the second page. - */ - @Test - public void presencehistory_islast() throws AblyException { - AblyRealtime ably = null; - try { - ClientOptions opts = createOptions(testVars.keys[0].keyStr); - opts.token = token.token; - opts.clientId = testClientId; - ably = new AblyRealtime(opts); - String channelName = "persisted:presencehistory_islast_" + testParams.name; - int pageMessageCount = 10; - - /* create a channel */ - final Channel channel = ably.channels.get(channelName); - - /* attach */ - channel.attach(); - new ChannelWaiter(channel).waitFor(ChannelState.attached); - assertEquals("Verify attached state reached", channel.state, ChannelState.attached); - - /* publish to the channel */ - CompletionSet msgComplete = new CompletionSet(); - for (int i = 0; i < (pageMessageCount * 2 - 1); i++) { - channel.presence.update(String.valueOf(i), msgComplete.add()); - } - - /* wait for the publish callbacks to be called */ - msgComplete.waitFor(); - assertTrue("Verify success callback was called", msgComplete.errors.isEmpty()); - - /* get the history for this channel */ - PaginatedResult messages = channel.presence.history(new Param[]{new Param("limit", String.format(Locale.ENGLISH, "%d", pageMessageCount))}); - assertNotNull("Expected non-null messages", messages); - assertEquals("Expected " + pageMessageCount + " messages", messages.items().length, pageMessageCount); - - /* Verify that current page is not the last */ - assertThat(messages.isLast(), is(false)); - - /* get next page */ - messages = messages.next(); - --pageMessageCount; - assertNotNull("Expected non-null messages", messages); - assertEquals("Expected " + pageMessageCount + " messages", messages.items().length, pageMessageCount); - - /* Verify that current page is the last */ - assertThat(messages.isLast(), is(true)); - } finally { - if (ably != null) - ably.close(); - } - } + private static final String testClientId = "testClientId"; + private long timeOffset; + private AblyRest rest; + private Auth.TokenDetails token; + + @Rule + public Timeout testTimeout = Timeout.seconds(300); + + @Before + public void setUpBefore() throws Exception { + /* create tokens for specific clientIds */ + ClientOptions opts = createOptions(testVars.keys[0].keyStr); + rest = new AblyRest(opts); + token = rest.auth.requestToken(new TokenParams() {{ clientId = testClientId; }}, null); + + /* sync */ + long timeFromService = rest.time(); + timeOffset = timeFromService - System.currentTimeMillis(); + } + + /** + * Send a single message on a channel and verify that it can be + * retrieved using channel.history() without needing to wait for + * it to be persisted. + */ + @Test + public void presencehistory_simple() { + AblyRealtime ably = null; + try { + ClientOptions rtOpts = createOptions(); + rtOpts.token = token.token; + rtOpts.clientId = testClientId; + ably = new AblyRealtime(rtOpts); + + String channelName = "persisted:presencehistory_simple_" + testParams.name; + String messageText = "Test message (presencehistory_simple)"; + + /* create a channel */ + final Channel channel = ably.channels.get(channelName); + + /* attach */ + channel.attach(); + (new ChannelWaiter(channel)).waitFor(ChannelState.attached); + assertEquals("Verify attached state reached", channel.state, ChannelState.attached); + + /* enter the channel */ + CompletionWaiter msgComplete = new CompletionWaiter(); + channel.presence.enter(messageText, msgComplete); + + /* wait for the enter callback to be called */ + msgComplete.waitFor(); + assertTrue("Verify success callback was called", msgComplete.success); + + /* get the presence history for this channel */ + PaginatedResult messages = channel.presence.history(null); + assertNotNull("Expected non-null messages", messages); + assertEquals("Expected 1 message", messages.items().length, 1); + + /* verify message contents */ + assertEquals("Expect correct message text", messages.items()[0].data, messageText); + } catch (AblyException e) { + e.printStackTrace(); + fail("single_history_binary: Unexpected exception instantiating library"); + } finally { + if(ably != null) + ably.close(); + } + } + + /** + * Publish events with data of various datatypes + */ + @Test + public void presencehistory_types() { + AblyRealtime ably = null; + try { + ClientOptions rtOpts = createOptions(); + rtOpts.token = token.token; + rtOpts.clientId = testClientId; + ably = new AblyRealtime(rtOpts); + + String channelName = "persisted:presencehistory_types_" + testParams.name; + + /* create a channel */ + final Channel channel = ably.channels.get(channelName); + + /* attach */ + channel.attach(); + (new ChannelWaiter(channel)).waitFor(ChannelState.attached); + assertEquals("Verify attached state reached", channel.state, ChannelState.attached); + + /* publish enter events to the channel */ + CompletionWaiter msgComplete = new CompletionWaiter(); + + channel.presence.enter("This is a string message payload", msgComplete); + channel.presence.enter("This is a byte[] message payload".getBytes(), msgComplete); + + /* wait for the enter callback to be called */ + msgComplete.waitFor(2); + assertTrue("Verify success callback was called", msgComplete.success); + + /* get the history for this channel */ + PaginatedResult messages = channel.presence.history(null); + assertNotNull("Expected non-null messages", messages); + assertEquals("Expected 2 messages", messages.items().length, 2); + + /* verify message contents and order */ + assertEquals("Expect messages.asArray()[1] to be expected String", messages.items()[1].data, "This is a string message payload"); + assertEquals("Expect messages.asArray()[0] to be expected byte[]", new String((byte[])messages.items()[0].data), "This is a byte[] message payload"); + } catch (AblyException e) { + e.printStackTrace(); + fail("presencehistory_types: Unexpected exception"); + } finally { + if(ably != null) + ably.close(); + } + } + + /** + * Publish events with data of various datatypes + */ + @Test + public void presencehistory_types_forward() { + AblyRealtime ably = null; + try { + ClientOptions rtOpts = createOptions(); + rtOpts.token = token.token; + rtOpts.clientId = testClientId; + ably = new AblyRealtime(rtOpts); + + String channelName = "persisted:presencehistory_types_forward_" + testParams.name; + + /* create a channel */ + final Channel channel = ably.channels.get(channelName); + + /* attach */ + channel.attach(); + (new ChannelWaiter(channel)).waitFor(ChannelState.attached); + assertEquals("Verify attached state reached", channel.state, ChannelState.attached); + + /* publish enter events to the channel */ + CompletionWaiter msgComplete = new CompletionWaiter(); + + channel.presence.enter("This is a string message payload", msgComplete); + channel.presence.enter("This is a byte[] message payload".getBytes(), msgComplete); + + /* wait for the enter callback to be called */ + msgComplete.waitFor(2); + assertTrue("Verify success callback was called", msgComplete.success); + + /* get the history for this channel */ + PaginatedResult messages = channel.presence.history(new Param[]{new Param("direction", "forwards")}); + assertNotNull("Expected non-null messages", messages); + assertEquals("Expected 2 messages", messages.items().length, 2); + + /* verify message contents and order */ + assertEquals("Expect messages.asArray()[0] to be expected String", messages.items()[0].data, "This is a string message payload"); + assertEquals("Expect messages.asArray()[1] to be expected byte[]", new String((byte[])messages.items()[1].data), "This is a byte[] message payload"); + } catch (AblyException e) { + e.printStackTrace(); + fail("presencehistory_types: Unexpected exception"); + } finally { + if(ably != null) + ably.close(); + } + } + + /** + * Connect twice to the service, each using the default (binary) protocol. + * Publish messages on one connection to a given channel; then attach + * the second connection to the same channel and verify a complete message + * history can be obtained. + */ + @Test + public void presencehistory_second_channel() { + AblyRealtime txAbly = null, rxAbly = null; + try { + ClientOptions txOpts = createOptions(); + txOpts.token = token.token; + txOpts.clientId = testClientId; + txAbly = new AblyRealtime(txOpts); + + ClientOptions rxOpts = createOptions(testVars.keys[0].keyStr); + rxAbly = new AblyRealtime(rxOpts); + String channelName = "persisted:presencehistory_second_channel_" + testParams.name; + + /* create a channel */ + final Channel txChannel = txAbly.channels.get(channelName); + final Channel rxChannel = rxAbly.channels.get(channelName); + + /* attach sender */ + txChannel.attach(); + (new ChannelWaiter(txChannel)).waitFor(ChannelState.attached); + assertEquals("Verify attached state reached", txChannel.state, ChannelState.attached); + + /* enter the channel */ + String messageText = "Test message (presencehistory_second_channel)"; + CompletionWaiter msgComplete = new CompletionWaiter(); + txChannel.presence.enter(messageText, msgComplete); + + /* wait for the enter callback to be called */ + msgComplete.waitFor(); + assertTrue("Verify success callback was called", msgComplete.success); + + /* attach receiver */ + rxChannel.attach(); + (new ChannelWaiter(rxChannel)).waitFor(ChannelState.attached); + assertEquals("Verify attached state reached", rxChannel.state, ChannelState.attached); + + /* get the history for this channel */ + PaginatedResult messages = rxChannel.presence.history(null); + assertNotNull("Expected non-null messages", messages); + assertEquals("Expected 1 message", messages.items().length, 1); + + /* verify message contents */ + assertEquals("Expect correct message text", messages.items()[0].data, messageText); + + } catch (AblyException e) { + e.printStackTrace(); + fail("presencehistory_second_channel: Unexpected exception instantiating library"); + } finally { + if(txAbly != null) + txAbly.close(); + if(rxAbly != null) + rxAbly.close(); + } + } + + /** + * Send a single message on a channel and verify that it can be + * retrieved using channel.history() after waiting for it to be + * persisted. + */ + @Test + public void presencehistory_wait_b() { + AblyRealtime ably = null; + try { + ClientOptions rtOpts = createOptions(); + rtOpts.token = token.token; + rtOpts.clientId = testClientId; + ably = new AblyRealtime(rtOpts); + String channelName = "persisted:presencehistory_wait_b_" + testParams.name; + String messageText = "Test message (presencehistory_wait_b)"; + + /* create a channel */ + final Channel channel = ably.channels.get(channelName); + + /* attach */ + channel.attach(); + (new ChannelWaiter(channel)).waitFor(ChannelState.attached); + assertEquals("Verify attached state reached", channel.state, ChannelState.attached); + + /* enter the channel */ + CompletionWaiter msgComplete = new CompletionWaiter(); + channel.presence.enter(messageText, msgComplete); + + /* wait for the publish callback to be called */ + msgComplete.waitFor(); + assertTrue("Verify success callback was called", msgComplete.success); + + /* wait for the history to be persisted */ + try { + Thread.sleep(16000); + } catch(InterruptedException ie) {} + + /* get the history for this channel */ + PaginatedResult messages = channel.presence.history(null); + assertNotNull("Expected non-null messages", messages); + assertEquals("Expected 1 message", messages.items().length, 1); + + /* verify message contents */ + assertEquals("Expect correct message text", messages.items()[0].data, messageText); + } catch (AblyException e) { + e.printStackTrace(); + fail("single_history_binary: Unexpected exception instantiating library"); + } finally { + if(ably != null) + ably.close(); + } + } + + /** + * Send a single message on a channel and verify that it can be + * retrieved using channel.history(direction=forwards) after waiting + * for it to be persisted. + */ + @Test + public void presencehistory_wait_f() { + AblyRealtime ably = null; + try { + ClientOptions rtOpts = createOptions(); + rtOpts.token = token.token; + rtOpts.clientId = testClientId; + ably = new AblyRealtime(rtOpts); + String channelName = "persisted:presencehistory_wait_f_" + testParams.name; + String messageText = "Test message (presencehistory_wait_f)"; + + /* create a channel */ + final Channel channel = ably.channels.get(channelName); + + /* attach */ + channel.attach(); + (new ChannelWaiter(channel)).waitFor(ChannelState.attached); + assertEquals("Verify attached state reached", channel.state, ChannelState.attached); + + /* publish to the channel */ + CompletionWaiter msgComplete = new CompletionWaiter(); + channel.presence.enter(messageText, msgComplete); + + /* wait for the publish callback to be called */ + msgComplete.waitFor(); + assertTrue("Verify success callback was called", msgComplete.success); + + /* wait for the history to be persisted */ + try { + Thread.sleep(16000); + } catch(InterruptedException ie) {} + + /* get the history for this channel */ + PaginatedResult messages = channel.presence.history(new Param[]{ new Param("direction", "forwards") }); + assertNotNull("Expected non-null messages", messages); + assertEquals("Expected 1 message", messages.items().length, 1); + + /* verify message contents */ + assertEquals("Expect correct message text", messages.items()[0].data, messageText); + } catch (AblyException e) { + e.printStackTrace(); + fail("presencehistory_wait_binary_f: Unexpected exception instantiating library"); + } finally { + if(ably != null) + ably.close(); + } + } + + /** + * Send a single presence message on a channel, wait enough time for it to + * persist, then send a second message. Verify that both can be + * retrieved using channel.history() without any further wait. + */ + @Test + public void presencehistory_mixed_b() { + AblyRealtime ably = null; + try { + ClientOptions rtOpts = createOptions(); + rtOpts.token = token.token; + rtOpts.clientId = testClientId; + ably = new AblyRealtime(rtOpts); + String channelName = "persisted:presencehistory_mixed_b_" + testParams.name; + String persistMessageText = "test_event (persisted)"; + String liveMessageText = "test_event (live)"; + + /* create a channel */ + final Channel channel = ably.channels.get(channelName); + + /* attach */ + channel.attach(); + (new ChannelWaiter(channel)).waitFor(ChannelState.attached); + assertEquals("Verify attached state reached", channel.state, ChannelState.attached); + + /* publish to the channel */ + CompletionWaiter msgComplete = new CompletionWaiter(); + channel.presence.enter(persistMessageText, msgComplete); + + /* wait for the history to be persisted */ + try { + Thread.sleep(16000); + } catch(InterruptedException ie) {} + + /* publish to the channel */ + channel.presence.enter(liveMessageText, msgComplete); + + /* wait for the publish callback to be called */ + msgComplete.waitFor(2); + assertTrue("Verify success callback was called", msgComplete.success); + + /* get the history for this channel */ + PaginatedResult messages = channel.presence.history(null); + assertNotNull("Expected non-null messages", messages); + assertEquals("Expected 2 messages", messages.items().length, 2); + + /* verify message contents */ + assertEquals("Expect correct message data", messages.items()[0].data, liveMessageText); + assertEquals("Expect correct message data", messages.items()[1].data, persistMessageText); + + } catch (AblyException e) { + e.printStackTrace(); + fail("presencehistory_mixed_binary_b: Unexpected exception instantiating library"); + } finally { + if(ably != null) + ably.close(); + } + } + + /** + * Send a single message on a channel, wait enough time for it to + * persist, then send a second message. Verify that both can be + * retrieved using channel.history(direction=forwards) without any + * further wait. + */ + @Test + public void presencehistory_mixed_f() { + AblyRealtime ably = null; + try { + ClientOptions rtOpts = createOptions(); + rtOpts.token = token.token; + rtOpts.clientId = testClientId; + ably = new AblyRealtime(rtOpts); + String channelName = "persisted:presencehistory_mixed_f_" + testParams.name; + String persistMessageText = "test_event (persisted)"; + String liveMessageText = "test_event (live)"; + + /* create a channel */ + final Channel channel = ably.channels.get(channelName); + + /* attach */ + channel.attach(); + (new ChannelWaiter(channel)).waitFor(ChannelState.attached); + assertEquals("Verify attached state reached", channel.state, ChannelState.attached); + + /* publish to the channel */ + CompletionWaiter msgComplete = new CompletionWaiter(); + channel.presence.enter(persistMessageText, msgComplete); + + /* wait for the history to be persisted */ + try { + Thread.sleep(16000); + } catch(InterruptedException ie) {} + + /* publish to the channel */ + channel.presence.enter(liveMessageText, msgComplete); + + /* wait for the publish callback to be called */ + msgComplete.waitFor(2); + assertTrue("Verify success callback was called", msgComplete.success); + + /* get the history for this channel */ + PaginatedResult messages = channel.presence.history(new Param[]{ new Param("direction", "forwards") }); + assertNotNull("Expected non-null messages", messages); + assertEquals("Expected 2 messages", messages.items().length, 2); + + /* verify message contents */ + assertEquals("Expect correct message data", messages.items()[0].data, persistMessageText); + assertEquals("Expect correct message data", messages.items()[1].data, liveMessageText); + + } catch (AblyException e) { + e.printStackTrace(); + fail("presencehistory_mixed_binary_f: Unexpected exception instantiating library"); + } finally { + if(ably != null) + ably.close(); + } + } + + /** + * Publish events, get limited history and check expected order (forwards) + */ + @Test + public void presencehistory_limit_f() { + AblyRealtime ably = null; + try { + ClientOptions rtOpts = createOptions(); + rtOpts.token = token.token; + rtOpts.clientId = testClientId; + ably = new AblyRealtime(rtOpts); + String channelName = "persisted:presencehistory_limit_f_" + testParams.name; + + /* create a channel */ + final Channel channel = ably.channels.get(channelName); + + /* attach */ + channel.attach(); + (new ChannelWaiter(channel)).waitFor(ChannelState.attached); + assertEquals("Verify attached state reached", channel.state, ChannelState.attached); + + /* publish to the channel */ + CompletionSet msgComplete = new CompletionSet(); + for(int i = 0; i < 50; i++) { + try { + channel.presence.enter(String.valueOf(i), msgComplete.add()); + } catch(AblyException e) { + e.printStackTrace(); + fail("presencehistory_limit_f: Unexpected exception"); + return; + } + } + + /* wait for the publish callbacks to be called */ + msgComplete.waitFor(); + assertTrue("Verify success callback was called", msgComplete.errors.isEmpty()); + + /* get the history for this channel */ + PaginatedResult messages = channel.presence.history(new Param[] { new Param("direction", "forwards"), new Param("limit", "25") }); + assertNotNull("Expected non-null messages", messages); + assertEquals("Expected 25 messages", messages.items().length, 25); + + /* verify message order */ + for(int i = 0; i < 25; i++) + assertEquals("Expect correct message data", messages.items()[i].data, String.valueOf(i)); + + } catch (AblyException e) { + e.printStackTrace(); + fail("presencehistory_limit_f: Unexpected exception"); + } finally { + if(ably != null) + ably.close(); + } + } + + /** + * Publish events, get limited history and check expected order (backwards) + */ + @Test + public void presencehistory_limit_b() { + AblyRealtime ably = null; + try { + ClientOptions rtOpts = createOptions(); + rtOpts.token = token.token; + rtOpts.clientId = testClientId; + ably = new AblyRealtime(rtOpts); + String channelName = "persisted:presencehistory_limit_b_" + testParams.name; + + /* create a channel */ + final Channel channel = ably.channels.get(channelName); + + /* attach */ + channel.attach(); + (new ChannelWaiter(channel)).waitFor(ChannelState.attached); + assertEquals("Verify attached state reached", channel.state, ChannelState.attached); + + /* publish to the channel */ + CompletionSet msgComplete = new CompletionSet(); + for(int i = 0; i < 50; i++) { + try { + channel.presence.enter(String.valueOf(i), msgComplete.add()); + } catch(AblyException e) { + e.printStackTrace(); + fail("presencehistory_limit_b: Unexpected exception"); + return; + } + } + + /* wait for the publish callbacks to be called */ + msgComplete.waitFor(); + assertTrue("Verify success callback was called", msgComplete.errors.isEmpty()); + + /* get the history for this channel */ + PaginatedResult messages = channel.presence.history(new Param[] { new Param("direction", "backwards"), new Param("limit", "25") }); + assertNotNull("Expected non-null messages", messages); + assertEquals("Expected 25 messages", messages.items().length, 25); + + /* verify message order */ + for(int i = 0; i < 25; i++) + assertEquals("Expect correct message data", messages.items()[i].data, String.valueOf(49 - i)); + + } catch (AblyException e) { + e.printStackTrace(); + fail("presencehistory_limit_b: Unexpected exception"); + return; + } finally { + if(ably != null) + ably.close(); + } + } + + /** + * Publish events and check expected history based on time slice (forwards) + */ + @Test + public void presencehistory_time_f() { + AblyRealtime ably = null; + try { + /* first, publish some messages */ + long intervalStart = 0, intervalEnd = 0; + ClientOptions rtOpts = createOptions(); + rtOpts.token = token.token; + rtOpts.clientId = testClientId; + ably = new AblyRealtime(rtOpts); + String channelName = "persisted:presencehistory_time_f_" + testParams.name; + + /* create a channel */ + final Channel channel = ably.channels.get(channelName); + + /* attach */ + channel.attach(); + (new ChannelWaiter(channel)).waitFor(ChannelState.attached); + assertEquals("Verify attached state reached", channel.state, ChannelState.attached); + + /* send batches of messages with short inter-message delay */ + CompletionSet msgComplete = new CompletionSet(); + for(int i = 0; i < 20; i++) { + channel.presence.enter(String.valueOf(i), msgComplete.add()); + Thread.sleep(100L); + } + Thread.sleep(1000L); + intervalStart = timeOffset + System.currentTimeMillis(); + for(int i = 20; i < 40; i++) { + channel.presence.enter(String.valueOf(i), msgComplete.add()); + Thread.sleep(100L); + } + intervalEnd = timeOffset + System.currentTimeMillis() - 1; + Thread.sleep(1000L); + for(int i = 40; i < 60; i++) { + channel.presence.enter(String.valueOf(i), msgComplete.add()); + Thread.sleep(100L); + } + + /* get the history for this channel */ + PaginatedResult messages = channel.presence.history(new Param[] { + new Param("direction", "forwards"), + new Param("start", String.valueOf(intervalStart - 500)), + new Param("end", String.valueOf(intervalEnd + 500)) + }); + assertNotNull("Expected non-null messages", messages); + assertEquals("Expected 20 messages", messages.items().length, 20); + + /* verify message order */ + for(int i = 20; i < 40; i++) + assertEquals("Expect correct message data", messages.items()[i - 20].data, String.valueOf(i)); + + } catch (AblyException e) { + e.printStackTrace(); + fail("presencehistory_time_f: Unexpected exception"); + } catch (InterruptedException e) { + e.printStackTrace(); + fail("presencehistory_time_f: Unexpected exception"); + } finally { + if(ably != null) + ably.close(); + } + } + + /** + * Publish events and check expected history based on time slice (backwards) + */ + @Test + public void presencehistory_time_b() { + AblyRealtime ably = null; + try { + /* first, publish some messages */ + long intervalStart = 0, intervalEnd = 0; + ClientOptions rtOpts = createOptions(); + rtOpts.token = token.token; + rtOpts.clientId = testClientId; + ably = new AblyRealtime(rtOpts); + String channelName = "persisted:presencehistory_time_b_" + testParams.name; + + /* create a channel */ + final Channel channel = ably.channels.get(channelName); + + /* attach */ + channel.attach(); + (new ChannelWaiter(channel)).waitFor(ChannelState.attached); + assertEquals("Verify attached state reached", channel.state, ChannelState.attached); + + /* send batches of messages with shprt inter-message delay */ + CompletionSet msgComplete = new CompletionSet(); + for(int i = 0; i < 20; i++) { + channel.presence.enter(String.valueOf(i), msgComplete.add()); + Thread.sleep(100L); + } + Thread.sleep(1000L); + intervalStart = timeOffset + System.currentTimeMillis(); + for(int i = 20; i < 40; i++) { + channel.presence.enter(String.valueOf(i), msgComplete.add()); + Thread.sleep(100L); + } + intervalEnd = timeOffset + System.currentTimeMillis() - 1; + Thread.sleep(1000L); + for(int i = 40; i < 60; i++) { + channel.presence.enter(String.valueOf(i), msgComplete.add()); + Thread.sleep(100L); + } + + /* get the history for this channel */ + PaginatedResult messages = channel.presence.history(new Param[] { + new Param("direction", "backwards"), + new Param("start", String.valueOf(intervalStart - 500)), + new Param("end", String.valueOf(intervalEnd + 500)) + }); + assertNotNull("Expected non-null messages", messages); + assertEquals("Expected 20 messages", messages.items().length, 20); + + /* verify message order */ + for(int i = 20; i < 40; i++) + assertEquals("Expect correct message data", messages.items()[i - 20].data, String.valueOf(59 - i)); + + } catch (AblyException e) { + e.printStackTrace(); + fail("presencehistory_time_b: Unexpected exception"); + } catch (InterruptedException e) { + e.printStackTrace(); + fail("presencehistory_time_b: Unexpected exception"); + } finally { + if(ably != null) + ably.close(); + } + } + + /** + * Check query pagination (forwards) + */ + @Test + public void presencehistory_paginate_f() { + AblyRealtime ably = null; + try { + ClientOptions rtOpts = createOptions(); + rtOpts.token = token.token; + rtOpts.clientId = testClientId; + ably = new AblyRealtime(rtOpts); + String channelName = "persisted:presencehistory_paginate_f_" + testParams.name; + + /* create a channel */ + final Channel channel = ably.channels.get(channelName); + + /* attach */ + channel.attach(); + (new ChannelWaiter(channel)).waitFor(ChannelState.attached); + assertEquals("Verify attached state reached", channel.state, ChannelState.attached); + + /* publish to the channel */ + CompletionSet msgComplete = new CompletionSet(); + for(int i = 0; i < 50; i++) { + try { + channel.presence.enter(String.valueOf(i), msgComplete.add()); + } catch(AblyException e) { + e.printStackTrace(); + fail("presencehistory_paginate_f: Unexpected exception"); + return; + } + } + + /* wait for the publish callbacks to be called */ + msgComplete.waitFor(); + assertTrue("Verify success callback was called", msgComplete.errors.isEmpty()); + + /* get the history for this channel */ + PaginatedResult messages = channel.presence.history(new Param[] { new Param("direction", "forwards"), new Param("limit", "10") }); + assertNotNull("Expected non-null messages", messages); + assertEquals("Expected 10 messages", messages.items().length, 10); + + /* verify message order */ + for(int i = 0; i < 10; i++) + assertEquals("Expect correct message data", messages.items()[i].data, String.valueOf(i)); + + /* get next page */ + messages = messages.next(); + assertNotNull("Expected non-null messages", messages); + assertEquals("Expected 10 messages", messages.items().length, 10); + + /* verify message order */ + for(int i = 0; i < 10; i++) + assertEquals("Expect correct message data", messages.items()[i].data, String.valueOf(i + 10)); + + /* get next page */ + messages = messages.next(); + assertNotNull("Expected non-null messages", messages); + assertEquals("Expected 10 messages", messages.items().length, 10); + + /* verify message order */ + for(int i = 0; i < 10; i++) + assertEquals("Expect correct message data", messages.items()[i].data, String.valueOf(i + 20)); + + } catch (AblyException e) { + e.printStackTrace(); + fail("presencehistory_paginate_f: Unexpected exception"); + } finally { + if(ably != null) + ably.close(); + } + } + + /** + * Check query pagination (backwards) + */ + @Test + public void presencehistory_paginate_b() { + AblyRealtime ably = null; + try { + ClientOptions rtOpts = createOptions(); + rtOpts.token = token.token; + rtOpts.clientId = testClientId; + ably = new AblyRealtime(rtOpts); + String channelName = "persisted:presencehistory_paginate_b_" + testParams.name; + + /* create a channel */ + final Channel channel = ably.channels.get(channelName); + + /* attach */ + channel.attach(); + (new ChannelWaiter(channel)).waitFor(ChannelState.attached); + assertEquals("Verify attached state reached", channel.state, ChannelState.attached); + + /* publish to the channel */ + CompletionSet msgComplete = new CompletionSet(); + for(int i = 0; i < 50; i++) { + try { + channel.presence.enter(String.valueOf(i), msgComplete.add()); + } catch(AblyException e) { + e.printStackTrace(); + fail("presencehistory_paginate_f: Unexpected exception"); + return; + } + } + + /* wait for the publish callbacks to be called */ + msgComplete.waitFor(); + assertTrue("Verify success callback was called", msgComplete.errors.isEmpty()); + + /* get the history for this channel */ + PaginatedResult messages = channel.presence.history(new Param[] { new Param("direction", "backwards"), new Param("limit", "10") }); + assertNotNull("Expected non-null messages", messages); + assertEquals("Expected 10 messages", messages.items().length, 10); + + /* verify message order */ + for(int i = 0; i < 10; i++) + assertEquals("Expect correct message data", messages.items()[i].data, String.valueOf(49 - i)); + + /* get next page */ + messages = messages.next(); + assertNotNull("Expected non-null messages", messages); + assertEquals("Expected 10 messages", messages.items().length, 10); + + /* verify message order */ + for(int i = 0; i < 10; i++) + assertEquals("Expect correct message data", messages.items()[i].data, String.valueOf(39 - i)); + + /* get next page */ + messages = messages.next(); + assertNotNull("Expected non-null messages", messages); + assertEquals("Expected 10 messages", messages.items().length, 10); + + /* verify message order */ + for(int i = 0; i < 10; i++) + assertEquals("Expect correct message data", messages.items()[i].data, String.valueOf(29 - i)); + + } catch (AblyException e) { + e.printStackTrace(); + fail("presencehistory_paginate_b: Unexpected exception"); + } finally { + if(ably != null) + ably.close(); + } + } + + /** + * Check query pagination "rel=first" (forwards) + */ + @Test + public void presencehistory_paginate_first_f() { + AblyRealtime ably = null; + try { + ClientOptions rtOpts = createOptions(); + rtOpts.token = token.token; + rtOpts.clientId = testClientId; + ably = new AblyRealtime(rtOpts); + String channelName = "persisted:presencehistory_paginate_first_f_" + testParams.name; + + /* create a channel */ + final Channel channel = ably.channels.get(channelName); + + /* attach */ + channel.attach(); + (new ChannelWaiter(channel)).waitFor(ChannelState.attached); + assertEquals("Verify attached state reached", channel.state, ChannelState.attached); + + /* publish to the channel */ + CompletionSet msgComplete = new CompletionSet(); + for(int i = 0; i < 50; i++) { + try { + channel.presence.enter(String.valueOf(i), msgComplete.add()); + } catch(AblyException e) { + e.printStackTrace(); + fail("presencehistory_paginate_f: Unexpected exception"); + return; + } + } + + /* wait for the publish callbacks to be called */ + msgComplete.waitFor(); + assertTrue("Verify success callback was called", msgComplete.errors.isEmpty()); + + /* get the history for this channel */ + PaginatedResult messages = channel.presence.history(new Param[] { new Param("direction", "forwards"), new Param("limit", "10") }); + assertNotNull("Expected non-null messages", messages); + assertEquals("Expected 10 messages", messages.items().length, 10); + + /* verify message order */ + for(int i = 0; i < 10; i++) + assertEquals("Expect correct message data", messages.items()[i].data, String.valueOf(i)); + + /* get next page */ + messages = messages.next(); + assertNotNull("Expected non-null messages", messages); + assertEquals("Expected 10 messages", messages.items().length, 10); + + /* verify message order */ + for(int i = 0; i < 10; i++) + assertEquals("Expect correct message data", messages.items()[i].data, String.valueOf(i + 10)); + + /* get first page */ + messages = messages.first(); + assertNotNull("Expected non-null messages", messages); + assertEquals("Expected 10 messages", messages.items().length, 10); + + /* verify message order */ + for(int i = 0; i < 10; i++) + assertEquals("Expect correct message data", messages.items()[i].data, String.valueOf(i)); + + } catch (AblyException e) { + e.printStackTrace(); + fail("presencehistory_paginate_first_f: Unexpected exception"); + } finally { + if(ably != null) + ably.close(); + } + } + + /** + * Check query pagination "rel=first" (backwards) + */ + @Test + public void presencehistory_paginate_first_b() { + AblyRealtime ably = null; + try { + ClientOptions rtOpts = createOptions(); + rtOpts.token = token.token; + rtOpts.clientId = testClientId; + ably = new AblyRealtime(rtOpts); + String channelName = "persisted:presencehistory_paginate_first_b_" + testParams.name; + + /* create a channel */ + final Channel channel = ably.channels.get(channelName); + + /* attach */ + channel.attach(); + (new ChannelWaiter(channel)).waitFor(ChannelState.attached); + assertEquals("Verify attached state reached", channel.state, ChannelState.attached); + + /* publish to the channel */ + CompletionSet msgComplete = new CompletionSet(); + for(int i = 0; i < 50; i++) { + try { + channel.presence.enter(String.valueOf(i), msgComplete.add()); + } catch(AblyException e) { + e.printStackTrace(); + fail("presencehistory_paginate_f: Unexpected exception"); + return; + } + } + + /* wait for the publish callbacks to be called */ + msgComplete.waitFor(); + assertTrue("Verify success callback was called", msgComplete.errors.isEmpty()); + + /* get the history for this channel */ + PaginatedResult messages = channel.presence.history(new Param[] { new Param("direction", "backwards"), new Param("limit", "10") }); + assertNotNull("Expected non-null messages", messages); + assertEquals("Expected 10 messages", messages.items().length, 10); + + /* verify message order */ + for(int i = 0; i < 10; i++) + assertEquals("Expect correct message data", messages.items()[i].data, String.valueOf(49 - i)); + + /* get next page */ + messages = messages.next(); + assertNotNull("Expected non-null messages", messages); + assertEquals("Expected 10 messages", messages.items().length, 10); + + /* verify message order */ + for(int i = 0; i < 10; i++) + assertEquals("Expect correct message data", messages.items()[i].data, String.valueOf(39 - i)); + + /* get first page */ + messages = messages.first(); + assertNotNull("Expected non-null messages", messages); + assertEquals("Expected 10 messages", messages.items().length, 10); + + /* verify message order */ + for(int i = 0; i < 10; i++) + assertEquals("Expect correct message data", messages.items()[i].data, String.valueOf(49 - i)); + + } catch (AblyException e) { + e.printStackTrace(); + fail("presencehistory_paginate_first_b: Unexpected exception"); + } finally { + if(ably != null) + ably.close(); + } + } + + /** + * Connect twice to the service. + * Publish messages on one connection to a given channel; while in progress, + * attach the second connection to the same channel and verify a message + * history up to the point of attachment can be obtained. + */ + @Test + @Ignore("Fails due to issues in sandbox. See https://github.com/ably/realtime/issues/1845 for details.") + public void presencehistory_from_attach() { + AblyRealtime txAbly = null, rxAbly = null; + try { + ClientOptions txOpts = createOptions(); + txOpts.token = token.token; + txOpts.clientId = testClientId; + txAbly = new AblyRealtime(txOpts); + + DebugOptions rxOpts = new DebugOptions(testVars.keys[0].keyStr); + fillInOptions(rxOpts); + RawProtocolMonitor rawPresenceWaiter = RawProtocolMonitor.createReceiver(Action.presence); + rxOpts.protocolListener = rawPresenceWaiter; + rxAbly = new AblyRealtime(rxOpts); + String channelName = "persisted:presencehistory_from_attach_" + testParams.name; + + /* create a channel */ + final Channel txChannel = txAbly.channels.get(channelName); + final Channel rxChannel = rxAbly.channels.get(channelName); + + /* attach sender */ + txChannel.attach(); + (new ChannelWaiter(txChannel)).waitFor(ChannelState.attached); + assertEquals("Verify attached state reached", txChannel.state, ChannelState.attached); + + /* publish messages to the channel */ + final CompletionSet msgComplete = new CompletionSet(); + Thread publisherThread = new Thread() { + @Override + public void run() { + for(int i = 0; i < 50; i++) { + try { + txChannel.presence.enter(String.valueOf(i), msgComplete.add()); + try { + sleep(100L); + } catch(InterruptedException ie) {} + } catch(AblyException e) { + e.printStackTrace(); + fail("presencehistory_from_attach: Unexpected exception"); + return; + } + } + msgComplete.waitFor(); + } + }; + publisherThread.start(); + + /* wait 2 seconds */ + try { + Thread.sleep(2000L); + } catch(InterruptedException ie) { + fail("presencehistory_from_attach: exception in publisher thread"); + } + + /* subscribe; this will trigger the attach */ + PresenceWaiter presenceWaiter = new PresenceWaiter(rxChannel); + + /* get the channel history from the attachSerial when we get the attach indication */ + (new ChannelWaiter(rxChannel)).waitFor(ChannelState.attached); + assertEquals("Verify attached state reached", rxChannel.state, ChannelState.attached); + assertNotNull("Verify attachSerial provided", rxChannel.properties.attachSerial); + + /* the subscription callback will be called first on the "sync" presence message + * delivered immediately following attach; so wait for this and then the first + * "realtime" message to be received */ + presenceWaiter.waitFor(2); + PresenceMessage firstReceivedRealtimeMessage = null; + for(ProtocolMessage msg : rawPresenceWaiter.receivedMessages) { + if(msg.channelSerial != null) { + firstReceivedRealtimeMessage = msg.presence[0]; + break; + } + } + + /* wait for the end of the tx thread */ + try { + publisherThread.join(); + } catch (InterruptedException e) {} + assertTrue("Verify success callback was called", msgComplete.errors.isEmpty()); + + /* get the history for this channel */ + PaginatedResult messages = rxChannel.presence.history(new Param[] { new Param("from_serial", rxChannel.properties.attachSerial)}); + assertNotNull("Expected non-null messages", messages); + assertTrue("Expected at least one message", messages.items().length >= 1); + + /* verify that the history and received messages meet */ + int earliestReceivedOnConnection = Integer.valueOf((String)firstReceivedRealtimeMessage.data); + int latestReceivedInHistory = Integer.valueOf((String)messages.items()[0].data); + assertEquals("Verify that the history and received messages meet", earliestReceivedOnConnection, latestReceivedInHistory + 1); + + } catch (AblyException e) { + e.printStackTrace(); + fail("presencehistory_from_attach: Unexpected exception instantiating library"); + } finally { + if(txAbly != null) + txAbly.close(); + if(rxAbly != null) + rxAbly.close(); + } + } + + /** + * Connect twice to the service, each using the default (binary) protocol. + * Publish messages on one connection to a given channel; while in progress, + * attach the second connection to the same channel and verify a message + * history up to the point of attachment can be obtained. + */ + @Test + public void presencehistory_until_attach() { + AblyRealtime txAbly = null, rxAbly = null; + try { + ClientOptions txOpts = createOptions(); + txOpts.token = token.token; + txOpts.clientId = testClientId; + txAbly = new AblyRealtime(txOpts); + + DebugOptions rxOpts = new DebugOptions(testVars.keys[0].keyStr); + fillInOptions(rxOpts); + RawProtocolMonitor rawPresenceWaiter = RawProtocolMonitor.createReceiver(Action.presence); + rxOpts.protocolListener = rawPresenceWaiter; + rxAbly = new AblyRealtime(rxOpts); + String channelName = "persisted:presencehistory_until_attach_" + testParams.name; + + /* create a channel */ + final Channel txChannel = txAbly.channels.get(channelName); + final Channel rxChannel = rxAbly.channels.get(channelName); + + /* attach sender */ + txChannel.attach(); + (new ChannelWaiter(txChannel)).waitFor(ChannelState.attached); + assertEquals("Verify attached state reached", txChannel.state, ChannelState.attached); + + /* publish messages to the channel */ + CompletionSet msgComplete = new CompletionSet(); + int messageCount = 25; + for (int i = 0; i < messageCount; i++) { + txChannel.presence.enter(String.valueOf(i), msgComplete.add()); + } + + msgComplete.waitFor(); + + /* get the channel history from the attachSerial when we get the attach indication */ + rxChannel.attach(); + new ChannelWaiter(rxChannel).waitFor(ChannelState.attached); + assertEquals("Verify attached state reached", rxChannel.state, ChannelState.attached); + assertNotNull("Verify attachSerial provided", rxChannel.properties.attachSerial); + + /* get the history for this channel */ + PaginatedResult messages = rxChannel.presence.history(new Param[] { new Param("untilAttach", "true") }); + assertNotNull("Expected non-null messages", messages); + assertTrue("Expected at least one message", messages.items().length >= 1); + + /* verify that the history and received messages meet */ + for (int i = 0; i < messageCount; i++) { + /* 0 --> "24" + * 1 --> "23" + * ... + * 24 --> "0" + */ + String actual = (String) messages.items()[messageCount - 1 - i].data; + String expected = String.valueOf(i); + assertThat(actual, is(equalTo(expected))); + } + } catch (AblyException e) { + e.printStackTrace(); + fail("presencehistory_from_attach: Unexpected exception instantiating library"); + } finally { + if(txAbly != null) + txAbly.close(); + if(rxAbly != null) + rxAbly.close(); + } + } + + /** + * Verifies an Exception is thrown, when a presence history is requested + * with parameter {"untilAttach":"true}" before client is attached to the channel + * + * @throws AblyException + */ + @Test(expected=AblyException.class) + public void presencehistory_until_attach_before_attached() throws AblyException { + ClientOptions options = createOptions(testVars.keys[0].keyStr); + AblyRealtime ably = new AblyRealtime(options); + + ably.channels.get("test").presence.history(new Param[]{ new Param("untilAttach", "true")}); + ably.close(); + } + + /** + * Verifies an Exception is thrown, when a presence history is requested + * with invalid "untilAttach" parameter value. + * + * @throws AblyException + */ + @Test(expected=AblyException.class) + public void presencehistory_until_attach_invalid_value() throws AblyException { + ClientOptions options = createOptions(testVars.keys[0].keyStr); + AblyRealtime ably = new AblyRealtime(options); + + ably.channels.get("test").presence.history(new Param[]{ new Param("untilAttach", "affirmative")}); + ably.close(); + } + + /** + * Publish enough presence to fill 2 pages. + * Verify that, + * - {@code PaginatedQuery#isLast} returns false, when we are at the first page. + * - {@code PaginatedQuery#isLast} returns true, when we are at the second page. + */ + @Test + public void presencehistory_islast() throws AblyException { + AblyRealtime ably = null; + try { + ClientOptions opts = createOptions(testVars.keys[0].keyStr); + opts.token = token.token; + opts.clientId = testClientId; + ably = new AblyRealtime(opts); + String channelName = "persisted:presencehistory_islast_" + testParams.name; + int pageMessageCount = 10; + + /* create a channel */ + final Channel channel = ably.channels.get(channelName); + + /* attach */ + channel.attach(); + new ChannelWaiter(channel).waitFor(ChannelState.attached); + assertEquals("Verify attached state reached", channel.state, ChannelState.attached); + + /* publish to the channel */ + CompletionSet msgComplete = new CompletionSet(); + for (int i = 0; i < (pageMessageCount * 2 - 1); i++) { + channel.presence.update(String.valueOf(i), msgComplete.add()); + } + + /* wait for the publish callbacks to be called */ + msgComplete.waitFor(); + assertTrue("Verify success callback was called", msgComplete.errors.isEmpty()); + + /* get the history for this channel */ + PaginatedResult messages = channel.presence.history(new Param[]{new Param("limit", String.format(Locale.ENGLISH, "%d", pageMessageCount))}); + assertNotNull("Expected non-null messages", messages); + assertEquals("Expected " + pageMessageCount + " messages", messages.items().length, pageMessageCount); + + /* Verify that current page is not the last */ + assertThat(messages.isLast(), is(false)); + + /* get next page */ + messages = messages.next(); + --pageMessageCount; + assertNotNull("Expected non-null messages", messages); + assertEquals("Expected " + pageMessageCount + " messages", messages.items().length, pageMessageCount); + + /* Verify that current page is the last */ + assertThat(messages.isLast(), is(true)); + } finally { + if (ably != null) + ably.close(); + } + } } diff --git a/lib/src/test/java/io/ably/lib/test/realtime/RealtimePresenceTest.java b/lib/src/test/java/io/ably/lib/test/realtime/RealtimePresenceTest.java index a19f720df..1994b9411 100644 --- a/lib/src/test/java/io/ably/lib/test/realtime/RealtimePresenceTest.java +++ b/lib/src/test/java/io/ably/lib/test/realtime/RealtimePresenceTest.java @@ -43,3356 +43,3356 @@ public class RealtimePresenceTest extends ParameterizedTest { - private static final String testMessagesEncodingFile = "ably-common/test-resources/presence-messages-encoding.json"; - private static final String testClientId1 = "testClientId1"; - private static final String testClientId2 = "testClientId2"; - private Auth.TokenDetails token1; - private Auth.TokenDetails token2; - private Auth.TokenDetails wildcardToken; - - private static PresenceMessage contains(PresenceMessage[] messages, String clientId) { - for(PresenceMessage message : messages) - if(clientId.equals(message.clientId)) - return message; - return null; - } - - private PresenceMessage contains(PresenceMessage[] messages, String clientId, PresenceMessage.Action action) { - for(PresenceMessage message : messages) - if(clientId.equals(message.clientId) && action == message.action) - return message; - return null; - } - - private static String random() { - return UUID.randomUUID().toString(); - } - - private class TestChannel { - TestChannel() { - try { - ClientOptions opts = createOptions(testVars.keys[0].keyStr); - rest = new AblyRest(opts); - restChannel = rest.channels.get(channelName); - realtime = new AblyRealtime(opts); - realtimeChannel = realtime.channels.get(channelName); - realtimeChannel.attach(); - (new ChannelWaiter(realtimeChannel)).waitFor(ChannelState.attached); - } catch(AblyException ae) {} - } - - void dispose() { - if(realtime != null) - realtime.close(); - } - - String channelName = random(); - AblyRest rest; - AblyRealtime realtime; - io.ably.lib.rest.Channel restChannel; - io.ably.lib.realtime.Channel realtimeChannel; - } - - @Rule - public Timeout testTimeout = Timeout.seconds(300); - - @Before - public void setUpBefore() throws Exception { - /* create tokens for specific clientIds */ - ClientOptions opts = createOptions(testVars.keys[0].keyStr); - AblyRest rest = new AblyRest(opts); - token1 = rest.auth.requestToken(new TokenParams() {{ clientId = testClientId1; }}, null); - token2 = rest.auth.requestToken(new TokenParams() {{ clientId = testClientId2; }}, null); - wildcardToken = rest.auth.requestToken(new TokenParams() {{ clientId = "*"; }}, null); - } - - /** - * Attach to channel, enter presence channel and await entered event - */ - @Test - public void enter_simple() { - AblyRealtime clientAbly1 = null; - TestChannel testChannel = new TestChannel(); - try { - /* subscribe for presence events in the anonymous connection */ - PresenceWaiter presenceWaiter = new PresenceWaiter(testChannel.realtimeChannel); - /* set up a connection with specific clientId */ - ClientOptions client1Opts = new ClientOptions() {{ - tokenDetails = token1; - clientId = testClientId1; - }}; - fillInOptions(client1Opts); - clientAbly1 = new AblyRealtime(client1Opts); - - /* wait until connected */ - (new ConnectionWaiter(clientAbly1.connection)).waitFor(ConnectionState.connected); - assertEquals("Verify connected state reached", clientAbly1.connection.state, ConnectionState.connected); - - /* get channel and attach */ - Channel client1Channel = clientAbly1.channels.get(testChannel.channelName); - client1Channel.attach(); - (new ChannelWaiter(client1Channel)).waitFor(ChannelState.attached); - assertEquals("Verify attached state reached", client1Channel.state, ChannelState.attached); - - /* let client1 enter the channel and wait for the entered event to be delivered */ - CompletionWaiter enterComplete = new CompletionWaiter(); - String enterString = "Test data (enter_simple)"; - client1Channel.presence.enter(enterString, enterComplete); - presenceWaiter.waitFor(testClientId1, Action.enter); - assertNotNull(presenceWaiter.contains(testClientId1, Action.enter)); - assertEquals(presenceWaiter.receivedMessages.get(0).data, enterString); - - /* verify enter callback called on completion */ - enterComplete.waitFor(); - assertTrue("Verify enter callback called on completion", enterComplete.success); - } catch(AblyException e) { - e.printStackTrace(); - fail("Unexpected exception running test: " + e.getMessage()); - } finally { - if(clientAbly1 != null) - clientAbly1.close(); - if(testChannel != null) - testChannel.dispose(); - } - } - - /** - * Enter presence channel without prior attach and await entered event - */ - @Test - public void enter_before_attach() { - AblyRealtime clientAbly1 = null; - TestChannel testChannel = new TestChannel(); - try { - /* subscribe for presence events in the anonymous connection */ - PresenceWaiter presenceWaiter = new PresenceWaiter(testChannel.realtimeChannel); - /* set up a connection with specific clientId */ - ClientOptions client1Opts = new ClientOptions() {{ - tokenDetails = token1; - clientId = testClientId1; - }}; - fillInOptions(client1Opts); - clientAbly1 = new AblyRealtime(client1Opts); - - /* wait until connected */ - (new ConnectionWaiter(clientAbly1.connection)).waitFor(ConnectionState.connected); - assertEquals("Verify connected state reached", clientAbly1.connection.state, ConnectionState.connected); - - /* get channel */ - Channel client1Channel = clientAbly1.channels.get(testChannel.channelName); - - /* let client1 enter the channel and wait for the entered event to be delivered */ - CompletionWaiter enterComplete = new CompletionWaiter(); - String enterString = "Test data (enter_before_attach)"; - client1Channel.presence.enter(enterString, enterComplete); - presenceWaiter.waitFor(testClientId1, Action.enter); - PresenceMessage expectedPresent = presenceWaiter.contains(testClientId1, clientAbly1.connection.id, Action.enter); - assertNotNull(expectedPresent); - assertEquals(expectedPresent.data, enterString); - - /* verify enter callback called on completion */ - enterComplete.waitFor(); - assertTrue("Verify enter callback called on completion", enterComplete.success); - } catch(AblyException e) { - e.printStackTrace(); - fail("Unexpected exception running test: " + e.getMessage()); - } finally { - if(clientAbly1 != null) - clientAbly1.close(); - if(testChannel != null) - testChannel.dispose(); - } - } - - /** - * Enter presence channel without prior connect and await entered event - */ - @Test - public void enter_before_connect() { - AblyRealtime clientAbly1 = null; - TestChannel testChannel = new TestChannel(); - try { - /* subscribe for presence events in the anonymous connection */ - PresenceWaiter presenceWaiter = new PresenceWaiter(testChannel.realtimeChannel); - /* set up a connection with specific clientId */ - ClientOptions client1Opts = new ClientOptions() {{ - tokenDetails = token1; - clientId = testClientId1; - }}; - fillInOptions(client1Opts); - clientAbly1 = new AblyRealtime(client1Opts); - - /* get channel */ - Channel client1Channel = clientAbly1.channels.get(testChannel.channelName); - - /* let client1 enter the channel and wait for the entered event to be delivered */ - CompletionWaiter enterComplete = new CompletionWaiter(); - String enterString = "Test data (enter_before_connect)"; - client1Channel.presence.enter(enterString, enterComplete); - presenceWaiter.waitFor(testClientId1, Action.enter); - PresenceMessage expectedPresent = presenceWaiter.contains(testClientId1, clientAbly1.connection.id, Action.enter); - assertNotNull(expectedPresent); - assertEquals(expectedPresent.data, enterString); - - /* verify enter callback called on completion */ - enterComplete.waitFor(); - assertTrue("Verify enter callback called on completion", enterComplete.success); - } catch(AblyException e) { - e.printStackTrace(); - fail("Unexpected exception running test: " + e.getMessage()); - } finally { - if(clientAbly1 != null) - clientAbly1.close(); - if(testChannel != null) - testChannel.dispose(); - } - } - - /** - * Enter, then leave, presence channel and await leave event - * Verify that the item is removed from the presence map (RTP2e) - */ - @Test - public void enter_leave_simple() { - AblyRealtime clientAbly1 = null; - TestChannel testChannel = new TestChannel(); - try { - /* subscribe for presence events in the anonymous connection */ - PresenceWaiter presenceWaiter = new PresenceWaiter(testChannel.realtimeChannel); - /* set up a connection with specific clientId */ - ClientOptions client1Opts = new ClientOptions() {{ - tokenDetails = token1; - clientId = testClientId1; - }}; - fillInOptions(client1Opts); - clientAbly1 = new AblyRealtime(client1Opts); - - /* get channel */ - Channel client1Channel = clientAbly1.channels.get(testChannel.channelName); - - /* let client1 enter the channel and wait for the entered event to be delivered */ - CompletionWaiter enterComplete = new CompletionWaiter(); - String enterString = "Test data (enter_before_connect)"; - client1Channel.presence.enter(enterString, enterComplete); - presenceWaiter.waitFor(testClientId1, Action.enter); - assertNotNull(presenceWaiter.contains(testClientId1, Action.enter)); - assertEquals(presenceWaiter.receivedMessages.get(0).data, enterString); - presenceWaiter.reset(); - - /* verify enter callback called on completion */ - enterComplete.waitFor(); - assertTrue("Verify enter callback called on completion", enterComplete.success); - - /* let client1 leave the channel and wait for the leave event to be delivered */ - CompletionWaiter leaveComplete = new CompletionWaiter(); - String leaveString = "Test data (enter_before_connect), leaving"; - client1Channel.presence.leave(leaveString, leaveComplete); - presenceWaiter.waitFor(testClientId1, Action.leave); - PresenceMessage expectedLeft = presenceWaiter.contains(testClientId1, clientAbly1.connection.id, Action.leave); - assertNotNull(expectedLeft); - assertEquals(expectedLeft.data, leaveString); - - /* verify leave callback called on completion */ - leaveComplete.waitFor(); - assertTrue("Verify leave callback called on completion", leaveComplete.success); - - assertEquals("Verify item is removed from the presence map", client1Channel.presence.get(testClientId1, false).length, 0); - - } catch(AblyException e) { - e.printStackTrace(); - fail("Unexpected exception running test: " + e.getMessage()); - } finally { - if(clientAbly1 != null) - clientAbly1.close(); - if(testChannel != null) - testChannel.dispose(); - } - } - - /** - * Enter, then enter again, expecting update event - */ - @Test - public void enter_enter_simple() { - AblyRealtime clientAbly1 = null; - TestChannel testChannel = new TestChannel(); - try { - /* subscribe for presence events in the anonymous connection */ - PresenceWaiter presenceWaiter = new PresenceWaiter(testChannel.realtimeChannel); - /* set up a connection with specific clientId */ - ClientOptions client1Opts = new ClientOptions() {{ - tokenDetails = token1; - clientId = testClientId1; - }}; - fillInOptions(client1Opts); - clientAbly1 = new AblyRealtime(client1Opts); - - /* get channel */ - Channel client1Channel = clientAbly1.channels.get(testChannel.channelName); - - /* let client1 enter the channel and wait for the entered event to be delivered */ - CompletionWaiter enterComplete = new CompletionWaiter(); - String enterString = "Test data (enter_enter_simple)"; - client1Channel.presence.enter(enterString, enterComplete); - presenceWaiter.waitFor(testClientId1, Action.enter); - assertNotNull(presenceWaiter.contains(testClientId1, Action.enter)); - assertEquals(presenceWaiter.receivedMessages.get(0).data, enterString); - presenceWaiter.reset(); - - /* verify enter callback called on completion */ - enterComplete.waitFor(); - assertTrue("Verify enter callback called on completion", enterComplete.success); - - /* let client1 reenter the channel and wait for the update event to be delivered */ - CompletionWaiter reenterComplete = new CompletionWaiter(); - String reenterString = "Test data (enter_enter_simple), reentering"; - client1Channel.presence.enter(reenterString, reenterComplete); - presenceWaiter.waitFor(testClientId1, Action.update); - assertNotNull(presenceWaiter.contains(testClientId1, Action.update)); - assertEquals(presenceWaiter.receivedMessages.get(0).data, reenterString); - - /* verify reenter callback called on completion */ - reenterComplete.waitFor(); - assertTrue("Verify reenter callback called on completion", reenterComplete.success); - - /* let client1 leave the channel and wait for the leave event to be delivered */ - CompletionWaiter leaveComplete = new CompletionWaiter(); - String leaveString = "Test data (enter_enter_simple), leaving"; - client1Channel.presence.leave(leaveString, leaveComplete); - presenceWaiter.waitFor(testClientId1, Action.leave); - PresenceMessage expectedLeft = presenceWaiter.contains(testClientId1, clientAbly1.connection.id, Action.leave); - assertNotNull(expectedLeft); - assertEquals(expectedLeft.data, leaveString); - - /* verify leave callback called on completion */ - leaveComplete.waitFor(); - assertTrue("Verify leave callback called on completion", leaveComplete.success); - } catch(AblyException e) { - e.printStackTrace(); - fail("Unexpected exception running test: " + e.getMessage()); - } finally { - if(clientAbly1 != null) - clientAbly1.close(); - if(testChannel != null) - testChannel.dispose(); - } - } - - /** - * Enter, then update, expecting update event - */ - @Test - public void enter_update_simple() { - AblyRealtime clientAbly1 = null; - TestChannel testChannel = new TestChannel(); - try { - /* subscribe for presence events in the anonymous connection */ - PresenceWaiter presenceWaiter = new PresenceWaiter(testChannel.realtimeChannel); - /* set up a connection with specific clientId */ - ClientOptions client1Opts = new ClientOptions() {{ - tokenDetails = token1; - clientId = testClientId1; - }}; - fillInOptions(client1Opts); - clientAbly1 = new AblyRealtime(client1Opts); - - /* get channel */ - Channel client1Channel = clientAbly1.channels.get(testChannel.channelName); - - /* let client1 enter the channel and wait for the entered event to be delivered */ - CompletionWaiter enterComplete = new CompletionWaiter(); - String enterString = "Test data (enter_update_simple)"; - client1Channel.presence.enter(enterString, enterComplete); - presenceWaiter.waitFor(testClientId1, Action.enter); - assertNotNull(presenceWaiter.contains(testClientId1, Action.enter)); - assertEquals(presenceWaiter.receivedMessages.get(0).data, enterString); - presenceWaiter.reset(); - - /* verify enter callback called on completion */ - enterComplete.waitFor(); - assertTrue("Verify enter callback called on completion", enterComplete.success); - - /* let client1 update the channel and wait for the update event to be delivered */ - CompletionWaiter updateComplete = new CompletionWaiter(); - String reenterString = "Test data (enter_update_simple), updating"; - client1Channel.presence.enter(reenterString, updateComplete); - presenceWaiter.waitFor(testClientId1, Action.update); - assertNotNull(presenceWaiter.contains(testClientId1, Action.update)); - assertEquals(presenceWaiter.receivedMessages.get(0).data, reenterString); - - /* verify reenter callback called on completion */ - updateComplete.waitFor(); - assertTrue("Verify reenter callback called on completion", updateComplete.success); - - /* let client1 leave the channel and wait for the leave event to be delivered */ - CompletionWaiter leaveComplete = new CompletionWaiter(); - String leaveString = "Test data (enter_update_simple), leaving"; - client1Channel.presence.leave(leaveString, leaveComplete); - presenceWaiter.waitFor(testClientId1, Action.leave); - PresenceMessage expectedLeft = presenceWaiter.contains(testClientId1, clientAbly1.connection.id, Action.leave); - assertNotNull(expectedLeft); - assertEquals(expectedLeft.data, leaveString); - - /* verify leave callback called on completion */ - leaveComplete.waitFor(); - assertTrue("Verify leave callback called on completion", leaveComplete.success); - } catch(AblyException e) { - e.printStackTrace(); - fail("Unexpected exception running test: " + e.getMessage()); - } finally { - if(clientAbly1 != null) - clientAbly1.close(); - if(testChannel != null) - testChannel.dispose(); - } - } - - /** - * Enter, then update with null data, expecting previous data to be superseded - */ - @Test - public void enter_update_null() { - AblyRealtime clientAbly1 = null; - TestChannel testChannel = new TestChannel(); - try { - /* subscribe for presence events in the anonymous connection */ - PresenceWaiter presenceWaiter = new PresenceWaiter(testChannel.realtimeChannel); - /* set up a connection with specific clientId */ - ClientOptions client1Opts = new ClientOptions() {{ - tokenDetails = token1; - clientId = testClientId1; - }}; - fillInOptions(client1Opts); - client1Opts.useBinaryProtocol = true; - clientAbly1 = new AblyRealtime(client1Opts); - - /* get channel */ - Channel client1Channel = clientAbly1.channels.get(testChannel.channelName); - - /* let client1 enter the channel and wait for the entered event to be delivered */ - CompletionWaiter enterComplete = new CompletionWaiter(); - String enterString = "Test data (enter_update_null)"; - client1Channel.presence.enter(enterString, enterComplete); - presenceWaiter.waitFor(testClientId1, Action.enter); - assertNotNull(presenceWaiter.contains(testClientId1, Action.enter)); - assertEquals(presenceWaiter.receivedMessages.get(0).data, enterString); - presenceWaiter.reset(); - - /* verify enter callback called on completion */ - enterComplete.waitFor(); - assertTrue("Verify enter callback called on completion", enterComplete.success); - - /* let client1 update the channel and wait for the update event to be delivered */ - CompletionWaiter updateComplete = new CompletionWaiter(); - String updateString = null; - client1Channel.presence.enter(updateString, updateComplete); - presenceWaiter.waitFor(testClientId1, Action.update); - assertNotNull(presenceWaiter.contains(testClientId1, Action.update)); - assertEquals(presenceWaiter.receivedMessages.get(0).data, updateString); - - /* verify reenter callback called on completion */ - updateComplete.waitFor(); - assertTrue("Verify reenter callback called on completion", updateComplete.success); - - /* let client1 leave the channel and wait for the leave event to be delivered */ - CompletionWaiter leaveComplete = new CompletionWaiter(); - String leaveString = "Test data (enter_update_null), leaving"; - client1Channel.presence.leave(leaveString, leaveComplete); - presenceWaiter.waitFor(testClientId1, Action.leave); - PresenceMessage expectedLeft = presenceWaiter.contains(testClientId1, clientAbly1.connection.id, Action.leave); - assertNotNull(expectedLeft); - assertEquals(expectedLeft.data, leaveString); - - /* verify leave callback called on completion */ - leaveComplete.waitFor(); - assertTrue("Verify leave callback called on completion", leaveComplete.success); - } catch(AblyException e) { - e.printStackTrace(); - fail("Unexpected exception running test: " + e.getMessage()); - } finally { - if(clientAbly1 != null) - clientAbly1.close(); - if(testChannel != null) - testChannel.dispose(); - } - } - - /** - * Update without having first entered, expecting enter event - */ - @Test - public void update_noenter() { - AblyRealtime clientAbly1 = null; - TestChannel testChannel = new TestChannel(); - try { - /* subscribe for presence events in the anonymous connection */ - PresenceWaiter presenceWaiter = new PresenceWaiter(testChannel.realtimeChannel); - /* set up a connection with specific clientId */ - ClientOptions client1Opts = new ClientOptions() {{ - tokenDetails = token1; - clientId = testClientId1; - }}; - fillInOptions(client1Opts); - clientAbly1 = new AblyRealtime(client1Opts); - - /* get channel */ - Channel client1Channel = clientAbly1.channels.get(testChannel.channelName); - - /* let client1 enter the channel and wait for the entered event to be delivered */ - CompletionWaiter enterComplete = new CompletionWaiter(); - String updateString = "Test data (update_noenter)"; - client1Channel.presence.update(updateString, enterComplete); - presenceWaiter.waitFor(testClientId1, Action.enter); - assertNotNull(presenceWaiter.contains(testClientId1, Action.enter)); - assertEquals(presenceWaiter.receivedMessages.get(0).data, updateString); - presenceWaiter.reset(); - - /* verify enter callback called on completion */ - enterComplete.waitFor(); - assertTrue("Verify enter callback called on completion", enterComplete.success); - - /* let client1 leave the channel and wait for the leave event to be delivered */ - CompletionWaiter leaveComplete = new CompletionWaiter(); - String leaveString = "Test data (update_noenter), leaving"; - client1Channel.presence.leave(leaveString, leaveComplete); - presenceWaiter.waitFor(testClientId1, Action.leave); - PresenceMessage expectedLeft = presenceWaiter.contains(testClientId1, clientAbly1.connection.id, Action.leave); - assertNotNull(expectedLeft); - assertEquals(expectedLeft.data, leaveString); - - /* verify leave callback called on completion */ - leaveComplete.waitFor(); - assertTrue("Verify leave callback called on completion", leaveComplete.success); - } catch(AblyException e) { - e.printStackTrace(); - fail("Unexpected exception running test: " + e.getMessage()); - } finally { - if(clientAbly1 != null) - clientAbly1.close(); - if(testChannel != null) - testChannel.dispose(); - } - } - - /** - * Enter, then leave (with no data) and await leave event, - * expecting enter data to be in leave event - */ - @Test - public void enter_leave_nodata() { - AblyRealtime clientAbly1 = null; - TestChannel testChannel = new TestChannel(); - try { - /* subscribe for presence events in the anonymous connection */ - PresenceWaiter presenceWaiter = new PresenceWaiter(testChannel.realtimeChannel); - /* set up a connection with specific clientId */ - ClientOptions client1Opts = new ClientOptions() {{ - tokenDetails = token1; - clientId = testClientId1; - }}; - fillInOptions(client1Opts); - clientAbly1 = new AblyRealtime(client1Opts); - - /* get channel */ - Channel client1Channel = clientAbly1.channels.get(testChannel.channelName); - - /* let client1 enter the channel and wait for the entered event to be delivered */ - CompletionWaiter enterComplete = new CompletionWaiter(); - String enterString = "Test data (enter_leave_nodata)"; - client1Channel.presence.enter(enterString, enterComplete); - presenceWaiter.waitFor(testClientId1, Action.enter); - assertNotNull(presenceWaiter.contains(testClientId1, Action.enter)); - assertEquals(presenceWaiter.receivedMessages.get(0).data, enterString); - presenceWaiter.reset(); - - /* verify enter callback called on completion */ - enterComplete.waitFor(); - assertTrue("Verify enter callback called on completion", enterComplete.success); - - /* let client1 leave the channel and wait for the leave event to be delivered */ - CompletionWaiter leaveComplete = new CompletionWaiter(); - client1Channel.presence.leave(leaveComplete); - presenceWaiter.waitFor(testClientId1, Action.leave); - assertNotNull(presenceWaiter.contains(testClientId1, Action.leave)); - assertEquals(presenceWaiter.receivedMessages.get(0).data, enterString); - - /* verify leave callback called on completion */ - leaveComplete.waitFor(); - assertTrue("Verify leave callback called on completion", leaveComplete.success); - - } catch(AblyException e) { - e.printStackTrace(); - } finally { - if(clientAbly1 != null) - clientAbly1.close(); - if(testChannel != null) - testChannel.dispose(); - } - } - - /** - * Attach to channel, enter presence channel and get presence using realtime get() - */ - @Test - public void realtime_get_simple() { - AblyRealtime clientAbly1 = null; - TestChannel testChannel = new TestChannel(); - try { - /* subscribe for presence events in the anonymous connection */ - PresenceWaiter presenceWaiter = new PresenceWaiter(testChannel.realtimeChannel); - - /* set up a connection with specific clientId */ - ClientOptions client1Opts = new ClientOptions() {{ - tokenDetails = token1; - clientId = testClientId1; - }}; - fillInOptions(client1Opts); - clientAbly1 = new AblyRealtime(client1Opts); - - /* wait until connected */ - (new ConnectionWaiter(clientAbly1.connection)).waitFor(ConnectionState.connected); - assertEquals("Verify connected state reached", clientAbly1.connection.state, ConnectionState.connected); - - /* get channel and attach */ - Channel client1Channel = clientAbly1.channels.get(testChannel.channelName); - client1Channel.attach(); - (new ChannelWaiter(client1Channel)).waitFor(ChannelState.attached); - assertEquals("Verify attached state reached", client1Channel.state, ChannelState.attached); - - /* let client1 enter the channel and wait for the success callback */ - CompletionWaiter enterComplete = new CompletionWaiter(); - String enterString = "Test data (get_simple)"; - client1Channel.presence.enter(enterString, enterComplete); - enterComplete.waitFor(); - assertTrue("Verify enter callback called on completion", enterComplete.success); - - /* get presence set and verify client present */ - presenceWaiter.waitFor(testClientId1); - PresenceMessage[] presences = testChannel.realtimeChannel.presence.get(false); - PresenceMessage expectedPresent = contains(presences, testClientId1, Action.present); - assertNotNull("Verify expected client is in presence set", expectedPresent); - assertEquals(expectedPresent.data, enterString); - - } catch(AblyException e) { - e.printStackTrace(); - fail("Unexpected exception running test: " + e.getMessage()); - } finally { - if(clientAbly1 != null) - clientAbly1.close(); - if(testChannel != null) - testChannel.dispose(); - } - } - - /** - * Attach to channel, enter+leave presence channel and get presence with realtime get() - */ - @Test - public void realtime_get_leave() { - AblyRealtime clientAbly1 = null; - TestChannel testChannel = new TestChannel(); - try { - /* subscribe for presence events in the anonymous connection */ - PresenceWaiter presenceWaiter = new PresenceWaiter(testChannel.realtimeChannel); - /* set up a connection with specific clientId */ - ClientOptions client1Opts = new ClientOptions() {{ - tokenDetails = token1; - clientId = testClientId1; - }}; - fillInOptions(client1Opts); - clientAbly1 = new AblyRealtime(client1Opts); - - /* wait until connected */ - (new ConnectionWaiter(clientAbly1.connection)).waitFor(ConnectionState.connected); - assertEquals("Verify connected state reached", clientAbly1.connection.state, ConnectionState.connected); - - /* get channel and attach */ - Channel client1Channel = clientAbly1.channels.get(testChannel.channelName); - client1Channel.attach(); - (new ChannelWaiter(client1Channel)).waitFor(ChannelState.attached); - assertEquals("Verify attached state reached", client1Channel.state, ChannelState.attached); - - /* let client1 enter the channel and wait for the success callback */ - CompletionWaiter enterComplete = new CompletionWaiter(); - client1Channel.presence.enter("Test data (get_leave)", enterComplete); - enterComplete.waitFor(); - assertTrue("Verify enter callback called on completion", enterComplete.success); - - /* let client1 leave the channel; wait for the success callback and event */ - CompletionWaiter leaveComplete = new CompletionWaiter(); - client1Channel.presence.leave(leaveComplete); - leaveComplete.waitFor(); - assertTrue("Verify leave callback called on completion", leaveComplete.success); - presenceWaiter.waitFor(testClientId1, Action.leave); - assertTrue("Verify leave callback called on completion", leaveComplete.success); - - /* get presence set and verify client absent */ - PresenceMessage[] presences = testChannel.realtimeChannel.presence.get(false); - assertNull("Verify expected client is in presence set", contains(presences, testClientId1)); - - } catch(AblyException e) { - e.printStackTrace(); - fail("Unexpected exception running test: " + e.getMessage()); - } finally { - if(clientAbly1 != null) - clientAbly1.close(); - if(testChannel != null) - testChannel.dispose(); - } - } - - /** - * Attach to channel, enter presence channel, then initiate second - * connection, seeing existing member in message subsequent to second attach response - */ - @Test - public void attach_enter_simple() { - AblyRealtime clientAbly1 = null; - AblyRealtime clientAbly2 = null; - TestChannel testChannel = new TestChannel(); - try { - /* subscribe for presence events in the anonymous connection */ - new PresenceWaiter(testChannel.realtimeChannel); - /* set up a connection with specific clientId */ - ClientOptions client1Opts = new ClientOptions() {{ - tokenDetails = token1; - clientId = testClientId1; - }}; - fillInOptions(client1Opts); - clientAbly1 = new AblyRealtime(client1Opts); - - /* wait until connected */ - (new ConnectionWaiter(clientAbly1.connection)).waitFor(ConnectionState.connected); - assertEquals("Verify connected state reached", clientAbly1.connection.state, ConnectionState.connected); - - /* get channel and attach */ - Channel client1Channel = clientAbly1.channels.get(testChannel.channelName); - client1Channel.attach(); - (new ChannelWaiter(client1Channel)).waitFor(ChannelState.attached); - assertEquals("Verify attached state reached", client1Channel.state, ChannelState.attached); - - /* let client1 enter the channel and wait for the success callback */ - CompletionWaiter enterComplete = new CompletionWaiter(); - String enterString = "Test data (attach_enter)"; - client1Channel.presence.enter(enterString, enterComplete); - enterComplete.waitFor(); - assertTrue("Verify enter callback called on completion", enterComplete.success); - - /* set up a second connection with different clientId */ - ClientOptions client2Opts = new ClientOptions() {{ - tokenDetails = token2; - clientId = testClientId2; - }}; - fillInOptions(client2Opts); - clientAbly2 = new AblyRealtime(client2Opts); - - /* wait until connected */ - (new ConnectionWaiter(clientAbly2.connection)).waitFor(ConnectionState.connected); - assertEquals("Verify connected state reached", clientAbly2.connection.state, ConnectionState.connected); - - /* get channel and subscribe to presence */ - Channel client2Channel = clientAbly2.channels.get(testChannel.channelName); - PresenceWaiter client2Waiter = new PresenceWaiter(client2Channel); - client2Waiter.waitFor(testClientId1, Action.present); - - /* get presence set and verify client present */ - PresenceMessage[] presences = client2Channel.presence.get(false); - PresenceMessage expectedPresent = contains(presences, testClientId1, Action.present); - assertNotNull("Verify expected client is in presence set", expectedPresent); - assertEquals(expectedPresent.data, enterString); - - } catch(AblyException e) { - e.printStackTrace(); - fail("Unexpected exception running test: " + e.getMessage()); - } finally { - if(clientAbly1 != null) - clientAbly1.close(); - if(clientAbly2 != null) - clientAbly2.close(); - if(testChannel != null) - testChannel.dispose(); - } - } - - /** - * Attach to channel, enter presence channel with large number of clientIds, - * then initiate second connection, seeing existing members in sync subsequent - * to second attach response - * - * Test RTP4 - */ - @Test - public void attach_enter_multiple() { - AblyRealtime clientAbly1 = null; - AblyRealtime clientAbly2 = null; - TestChannel testChannel = new TestChannel(); - int clientCount = 250; - try { - /* subscribe for presence events in the anonymous connection */ - new PresenceWaiter(testChannel.realtimeChannel); - /* set up a connection with specific clientId */ - ClientOptions client1Opts = new ClientOptions() {{ - tokenDetails = wildcardToken; - }}; - fillInOptions(client1Opts); - clientAbly1 = new AblyRealtime(client1Opts); - - /* wait until connected */ - (new ConnectionWaiter(clientAbly1.connection)).waitFor(ConnectionState.connected); - assertEquals("Verify connected state reached", clientAbly1.connection.state, ConnectionState.connected); - - /* get channel and attach */ - Channel client1Channel = clientAbly1.channels.get(testChannel.channelName); - client1Channel.attach(); - (new ChannelWaiter(client1Channel)).waitFor(ChannelState.attached); - assertEquals("Verify attached state reached", client1Channel.state, ChannelState.attached); - - /* let client1 enter the channel for multiple clients and wait for the success callback */ - CompletionSet enterComplete = new CompletionSet(); - for(int i = 0; i < clientCount; i++) { - client1Channel.presence.enterClient("client" + i, "Test data (attach_enter_multiple) " + i, enterComplete.add()); - } - enterComplete.waitFor(); - assertTrue("Verify enter callback called on completion", enterComplete.pending.isEmpty()); - assertTrue("Verify no enter errors", enterComplete.errors.isEmpty()); - - /* set up a second connection with different clientId */ - ClientOptions client2Opts = new ClientOptions() {{ - tokenDetails = token2; - clientId = testClientId2; - }}; - fillInOptions(client2Opts); - clientAbly2 = new AblyRealtime(client2Opts); - - /* wait until connected */ - (new ConnectionWaiter(clientAbly2.connection)).waitFor(ConnectionState.connected); - assertEquals("Verify connected state reached", clientAbly2.connection.state, ConnectionState.connected); - - /* get channel */ - Channel client2Channel = clientAbly2.channels.get(testChannel.channelName); - client2Channel.attach(); - (new ChannelWaiter(client2Channel)).waitFor(ChannelState.attached); - assertEquals("Verify attached state reached", client2Channel.state, ChannelState.attached); - - /* get presence set and verify client present */ - HashMap memberIndex = new HashMap(); - PresenceMessage[] members = client2Channel.presence.get(true); - assertNotNull("Expected non-null messages", members); - assertEquals("Expected " + clientCount + " messages", members.length, clientCount); - - /* index received messages */ - for(PresenceMessage member: members) - memberIndex.put(member.clientId, member); - - /* verify that all clientIds were received */ - assertEquals("Expected " + clientCount + " members", memberIndex.size(), clientCount); - for(int i = 0; i < clientCount; i++) { - String clientId = "client" + i; - assertTrue("Expected client with id " + clientId, memberIndex.containsKey(clientId)); - } - } catch(AblyException e) { - e.printStackTrace(); - fail("Unexpected exception running test: " + e.getMessage()); - } finally { - if(clientAbly1 != null) - clientAbly1.close(); - if(clientAbly2 != null) - clientAbly2.close(); - testChannel.dispose(); - } - } - - /** - * Attach and enter channel on two connections, seeing - * both members in presence returned by realtime get() */ - @Test - public void realtime_enter_multiple() { - AblyRealtime clientAbly1 = null; - AblyRealtime clientAbly2 = null; - TestChannel testChannel = new TestChannel(); - try { - /* subscribe for presence events in the anonymous connection */ - PresenceWaiter waiter = new PresenceWaiter(testChannel.realtimeChannel); - - /* set up a connection with specific clientId */ - ClientOptions client1Opts = new ClientOptions() {{ - tokenDetails = token1; - clientId = testClientId1; - }}; - fillInOptions(client1Opts); - clientAbly1 = new AblyRealtime(client1Opts); - - /* get channel and attach */ - Channel client1Channel = clientAbly1.channels.get(testChannel.channelName); - CompletionWaiter enter1Complete = new CompletionWaiter(); - String enterString1 = "Test data (enter_multiple, clientId1)"; - client1Channel.presence.enter(enterString1, enter1Complete); - enter1Complete.waitFor(); - assertTrue("Verify enter callback called on completion", enter1Complete.success); - - /* set up a second connection with different clientId */ - ClientOptions client2Opts = new ClientOptions() {{ - tokenDetails = token2; - clientId = testClientId2; - }}; - fillInOptions(client2Opts); - clientAbly2 = new AblyRealtime(client2Opts); - - /* get channel and subscribe to presence */ - Channel client2Channel = clientAbly2.channels.get(testChannel.channelName); - CompletionWaiter enter2Complete = new CompletionWaiter(); - String enterString2 = "Test data (enter_multiple, clientId2)"; - client2Channel.presence.enter(enterString2, enter2Complete); - enter2Complete.waitFor(); - assertTrue("Verify enter callback called on completion", enter2Complete.success); - - /* verify enter events for both clients are received */ - waiter.waitFor(testClientId1, Action.enter); - waiter.waitFor(testClientId2, Action.enter); - - /* get presence set and verify clients present */ - PresenceMessage[] presences = testChannel.realtimeChannel.presence.get(false); - PresenceMessage expectedPresent1 = contains(presences, testClientId1, Action.present); - PresenceMessage expectedPresent2 = contains(presences, testClientId2, Action.present); - assertNotNull("Verify expected clients are in presence set", expectedPresent1); - assertNotNull("Verify expected clients are in presence set", expectedPresent2); - assertEquals(expectedPresent1.data, enterString1); - assertEquals(expectedPresent2.data, enterString2); - - } catch(AblyException e) { - e.printStackTrace(); - fail("Unexpected exception running test: " + e.getMessage()); - } finally { - if(clientAbly1 != null) - clientAbly1.close(); - if(clientAbly2 != null) - clientAbly2.close(); - if(testChannel != null) - testChannel.dispose(); - } - } - - /** - * Attach to channel, enter presence channel and get presence using rest get() - */ - @Test - public void rest_get_simple() { - AblyRealtime clientAbly1 = null; - TestChannel testChannel = new TestChannel(); - try { - /* subscribe for presence events in the anonymous connection */ - new PresenceWaiter(testChannel.realtimeChannel); - /* set up a connection with specific clientId */ - ClientOptions client1Opts = new ClientOptions() {{ - tokenDetails = token1; - clientId = testClientId1; - }}; - fillInOptions(client1Opts); - clientAbly1 = new AblyRealtime(client1Opts); - - /* wait until connected */ - (new ConnectionWaiter(clientAbly1.connection)).waitFor(ConnectionState.connected); - assertEquals("Verify connected state reached", clientAbly1.connection.state, ConnectionState.connected); - - /* get channel and attach */ - Channel client1Channel = clientAbly1.channels.get(testChannel.channelName); - client1Channel.attach(); - (new ChannelWaiter(client1Channel)).waitFor(ChannelState.attached); - assertEquals("Verify attached state reached", client1Channel.state, ChannelState.attached); - - /* let client1 enter the channel and wait for the success callback */ - CompletionWaiter enterComplete = new CompletionWaiter(); - String enterString = "Test data (get_simple)"; - client1Channel.presence.enter(enterString, enterComplete); - enterComplete.waitFor(); - assertTrue("Verify enter callback called on completion", enterComplete.success); - - /* get presence set and verify client present */ - PresenceMessage[] presences = testChannel.restChannel.presence.get(null).items(); - PresenceMessage expectedPresent = contains(presences, testClientId1, Action.present); - assertNotNull("Verify expected client is in presence set", expectedPresent); - assertEquals(expectedPresent.data, enterString); - - } catch(AblyException e) { - e.printStackTrace(); - fail("Unexpected exception running test: " + e.getMessage()); - } finally { - if(clientAbly1 != null) - clientAbly1.close(); - if(testChannel != null) - testChannel.dispose(); - } - } - - /** - * Attach to channel, enter+leave presence channel and get presence with rest get() - */ - @Test - public void rest_get_leave() { - AblyRealtime clientAbly1 = null; - TestChannel testChannel = new TestChannel(); - try { - /* subscribe for presence events in the anonymous connection */ - PresenceWaiter presenceWaiter = new PresenceWaiter(testChannel.realtimeChannel); - /* set up a connection with specific clientId */ - ClientOptions client1Opts = new ClientOptions() {{ - tokenDetails = token1; - clientId = testClientId1; - }}; - fillInOptions(client1Opts); - clientAbly1 = new AblyRealtime(client1Opts); - - /* wait until connected */ - (new ConnectionWaiter(clientAbly1.connection)).waitFor(ConnectionState.connected); - assertEquals("Verify connected state reached", clientAbly1.connection.state, ConnectionState.connected); - - /* get channel and attach */ - Channel client1Channel = clientAbly1.channels.get(testChannel.channelName); - client1Channel.attach(); - (new ChannelWaiter(client1Channel)).waitFor(ChannelState.attached); - assertEquals("Verify attached state reached", client1Channel.state, ChannelState.attached); - - /* let client1 enter the channel and wait for the success callback */ - CompletionWaiter enterComplete = new CompletionWaiter(); - client1Channel.presence.enter("Test data (get_leave)", enterComplete); - enterComplete.waitFor(); - assertTrue("Verify enter callback called on completion", enterComplete.success); - - /* let client1 leave the channel; wait for the success callback and event */ - CompletionWaiter leaveComplete = new CompletionWaiter(); - client1Channel.presence.leave(leaveComplete); - leaveComplete.waitFor(); - assertTrue("Verify leave callback called on completion", leaveComplete.success); - presenceWaiter.waitFor(testClientId1, Action.leave); - assertTrue("Verify leave callback called on completion", leaveComplete.success); - - /* get presence set and verify client absent */ - PresenceMessage[] presences = testChannel.restChannel.presence.get(null).items(); - assertNull("Verify expected client is in presence set", contains(presences, testClientId1)); - - } catch(AblyException e) { - e.printStackTrace(); - fail("Unexpected exception running test: " + e.getMessage()); - } finally { - if(clientAbly1 != null) - clientAbly1.close(); - if(testChannel != null) - testChannel.dispose(); - } - } - - /** - * Attach and enter channel on two connections, seeing - * both members in presence returned by rest get() */ - @Test - public void rest_enter_multiple() { - AblyRealtime clientAbly1 = null; - AblyRealtime clientAbly2 = null; - TestChannel testChannel = new TestChannel(); - try { - /* subscribe for presence events in the anonymous connection */ - new PresenceWaiter(testChannel.realtimeChannel); - /* set up a connection with specific clientId */ - ClientOptions client1Opts = new ClientOptions() {{ - tokenDetails = token1; - clientId = testClientId1; - }}; - fillInOptions(client1Opts); - clientAbly1 = new AblyRealtime(client1Opts); - - /* get channel and attach */ - Channel client1Channel = clientAbly1.channels.get(testChannel.channelName); - CompletionWaiter enter1Complete = new CompletionWaiter(); - String enterString1 = "Test data (enter_multiple, clientId1)"; - client1Channel.presence.enter(enterString1, enter1Complete); - enter1Complete.waitFor(); - assertTrue("Verify enter callback called on completion", enter1Complete.success); - - /* set up a second connection with different clientId */ - ClientOptions client2Opts = new ClientOptions() {{ - tokenDetails = token2; - clientId = testClientId2; - }}; - fillInOptions(client2Opts); - clientAbly2 = new AblyRealtime(client2Opts); - - /* get channel and subscribe to presence */ - Channel client2Channel = clientAbly2.channels.get(testChannel.channelName); - CompletionWaiter enter2Complete = new CompletionWaiter(); - String enterString2 = "Test data (enter_multiple, clientId2)"; - client2Channel.presence.enter(enterString2, enter2Complete); - enter2Complete.waitFor(); - assertTrue("Verify enter callback called on completion", enter2Complete.success); - - /* get presence set and verify client present */ - PresenceMessage[] presences = testChannel.restChannel.presence.get(null).items(); - PresenceMessage expectedPresent1 = contains(presences, testClientId1, Action.present); - PresenceMessage expectedPresent2 = contains(presences, testClientId2, Action.present); - assertNotNull("Verify expected clients are in presence set", expectedPresent1); - assertNotNull("Verify expected clients are in presence set", expectedPresent2); - assertEquals(expectedPresent1.data, enterString1); - assertEquals(expectedPresent2.data, enterString2); - - } catch(AblyException e) { - e.printStackTrace(); - fail("Unexpected exception running test: " + e.getMessage()); - } finally { - if(clientAbly1 != null) - clientAbly1.close(); - if(clientAbly2 != null) - clientAbly2.close(); - if(testChannel != null) - testChannel.dispose(); - } - } - - /** - * Attach and enter channel multiple times on a single connection, - * retrieving members using paginated rest get() */ - @Test - public void rest_paginated_get() { - AblyRealtime clientAbly1 = null; - TestChannel testChannel = new TestChannel(); - int clientCount = 30; - long delay = 100L; - try { - /* subscribe for presence events in the anonymous connection */ - new PresenceWaiter(testChannel.realtimeChannel); - /* set up a connection with specific clientId */ - ClientOptions client1Opts = new ClientOptions() {{ - tokenDetails = wildcardToken; - }}; - fillInOptions(client1Opts); - clientAbly1 = new AblyRealtime(client1Opts); - - /* get channel and attach */ - Channel client1Channel = clientAbly1.channels.get(testChannel.channelName); - - /* enter multiple clients */ - CompletionSet enterComplete = new CompletionSet(); - for(int i = 0; i < clientCount; i++) { - client1Channel.presence.enterClient("client" + i, "Test data (rest_paginated_get) " + i, enterComplete.add()); - try { Thread.sleep(delay); } catch(InterruptedException e){} - } - enterComplete.waitFor(); - assertTrue("Verify enter callback called on completion", enterComplete.errors.isEmpty()); - - /* get the presence for this channel */ - HashMap memberIndex = new HashMap(); - PaginatedResult members = testChannel.restChannel.presence.get(new Param[] { new Param("limit", "10") }); - assertNotNull("Expected non-null messages", members); - assertEquals("Expected 10 messages", members.items().length, 10); - - /* index received messages */ - for(int i = 0; i < 10; i++) { - PresenceMessage member = members.items()[i]; - memberIndex.put(member.clientId, member); - } - - /* get next page */ - members = members.next(); - assertNotNull("Expected non-null messages", members); - assertEquals("Expected 10 messages", members.items().length, 10); - - /* index received messages */ - for(int i = 0; i < 10; i++) { - PresenceMessage member = members.items()[i]; - memberIndex.put(member.clientId, member); - } - - /* get next page */ - members = members.next(); - assertNotNull("Expected non-null messages", members); - assertEquals("Expected 10 messages", members.items().length, 10); - - /* index received messages */ - for(int i = 0; i < 10; i++) { - PresenceMessage member = members.items()[i]; - memberIndex.put(member.clientId, member); - } - - /* verify there is no next page */ - assertFalse("Expected null next page", members.hasNext()); - - /* verify that all clientIds were received */ - assertEquals("Expected " + clientCount + " members", memberIndex.size(), clientCount); - for(int i = 0; i < clientCount; i++) { - String clientId = "client" + i; - assertTrue("Expected client with id " + clientId, memberIndex.containsKey(clientId)); - } - } catch(AblyException e) { - e.printStackTrace(); - fail("Unexpected exception running test: " + e.getMessage()); - } finally { - if(clientAbly1 != null) - clientAbly1.close(); - if(testChannel != null) - testChannel.dispose(); - } - } - - /** - * Attach to channel, enter presence channel, disconnect and await leave event - */ - @Test - public void disconnect_leave() { - AblyRealtime clientAbly1 = null; - TestChannel testChannel = new TestChannel(); - boolean requiresClose = false; - try { - /* subscribe for presence events in the anonymous connection */ - PresenceWaiter presenceWaiter = new PresenceWaiter(testChannel.realtimeChannel); - /* set up a connection with specific clientId */ - ClientOptions client1Opts = new ClientOptions() {{ - tokenDetails = token1; - clientId = testClientId1; - }}; - fillInOptions(client1Opts); - clientAbly1 = new AblyRealtime(client1Opts); - requiresClose = true; - - /* get channel */ - Channel client1Channel = clientAbly1.channels.get(testChannel.channelName); - - /* let client1 enter the channel and wait for the entered event to be delivered */ - CompletionWaiter enterComplete = new CompletionWaiter(); - String enterString = "Test data (disconnect_leave)"; - client1Channel.presence.enter(enterString, enterComplete); - presenceWaiter.waitFor(testClientId1, Action.enter); - PresenceMessage expectedPresent = presenceWaiter.contains(testClientId1, Action.enter); - assertNotNull(expectedPresent); - assertEquals(expectedPresent.data, enterString); - - /* verify enter callback called on completion */ - enterComplete.waitFor(); - assertTrue("Verify enter callback called on completion", enterComplete.success); - - /* close client1 connection and wait for the leave event to be delivered */ - clientAbly1.close(); - requiresClose = false; - presenceWaiter.waitFor(testClientId1, Action.leave); - PresenceMessage expectedLeft = presenceWaiter.contains(testClientId1, Action.leave); - assertNotNull(expectedLeft); - /* verify leave message contains data that was published with enter */ - assertEquals(expectedLeft.data, enterString); - - } catch(AblyException e) { - e.printStackTrace(); - fail("Unexpected exception running test: " + e.getMessage()); - } finally { - if(requiresClose) - clientAbly1.close(); - if(testChannel != null) - testChannel.dispose(); - } - } - - /** - *

- * Validates channel removes all subscribers, - * when {@code Channel#unsubscribe()} with no argument gets called. - *

- * - * Tests RTP7a - * - * @throws AblyException - */ - @Test - public void realtime_presence_unsubscribe_all() throws AblyException { - /* Ably instance that will emit presence events */ - AblyRealtime ably1 = null; - /* Ably instance that will receive presence events */ - AblyRealtime ably2 = null; - - String channelName = "test.presence.unsubscribe.all" + System.currentTimeMillis(); - - try { - ClientOptions option1 = createOptions(testVars.keys[0].keyStr); - option1.clientId = "emitter client"; - ClientOptions option2 = createOptions(testVars.keys[0].keyStr); - option2.clientId = "receiver client"; - - ably1 = new AblyRealtime(option1); - ably2 = new AblyRealtime(option2); - - Channel channel1 = ably1.channels.get(channelName); - channel1.attach(); - (new ChannelWaiter(channel1)).waitFor(ChannelState.attached); - - Channel channel2 = ably2.channels.get(channelName); - channel2.attach(); - (new ChannelWaiter(channel2)).waitFor(ChannelState.attached); - - ArrayList receivedMessageStack = new ArrayList<>(); - Presence.PresenceListener listener = new Presence.PresenceListener() { - List messageStack; - - @Override - public void onPresenceMessage(PresenceMessage message) { - messageStack.add(message); - } - - public Presence.PresenceListener setMessageStack(List messageStack) { - this.messageStack = messageStack; - return this; - } - }.setMessageStack(receivedMessageStack); - - /* Subscribe using various alternatives of {@code Presence#subscribe()} */ - channel2.presence.subscribe(listener); - channel2.presence.subscribe(Action.present, listener); - channel2.presence.subscribe(EnumSet.of(Action.update, Action.leave), listener); - - /* Unsubscribe */ - channel2.presence.unsubscribe(); - - /* Start emitting channel with ably client 1 (emitter) */ - channel1.presence.enter("Hello, #2!", null); - channel1.presence.update("Lorem ipsum", null); - channel1.presence.update("Dolor sit!", null); - channel1.presence.leave(null); - - /* Wait until receiver client (ably2) observes {@code Action.leave} - * is emitted from emitter client (ably1) - */ - Helpers.PresenceWaiter leavePresenceWaiter = new Helpers.PresenceWaiter(channel2); - leavePresenceWaiter.waitFor(ably1.options.clientId, Action.leave); - - /* Validate that we didn't received anything - */ - assertThat(receivedMessageStack, is(emptyCollectionOf(PresenceMessage.class))); - } finally { - if (ably1 != null) ably1.close(); - if (ably2 != null) ably2.close(); - } - } - - /** - *

- * Validates channel removes a subscriber, - * when {@code Channel#unsubscribe()} gets called with a listener. - *

- * - * @throws AblyException - */ - @Test - public void realtime_presence_unsubscribe_single() throws AblyException { - /* Ably instance that will emit presence events */ - AblyRealtime ably1 = null; - /* Ably instance that will receive presence events */ - AblyRealtime ably2 = null; - - String channelName = "test.presence.unsubscribe.single" + System.currentTimeMillis(); - - try { - ClientOptions option1 = createOptions(testVars.keys[0].keyStr); - option1.clientId = "emitter client"; - ClientOptions option2 = createOptions(testVars.keys[0].keyStr); - option2.clientId = "receiver client"; - - ably1 = new AblyRealtime(option1); - ably2 = new AblyRealtime(option2); - - Channel channel1 = ably1.channels.get(channelName); - channel1.attach(); - (new ChannelWaiter(channel1)).waitFor(ChannelState.attached); - - Channel channel2 = ably2.channels.get(channelName); - channel2.attach(); - (new ChannelWaiter(channel2)).waitFor(ChannelState.attached); - - ArrayList receivedMessageStack = new ArrayList<>(); - Presence.PresenceListener listener = new Presence.PresenceListener() { - List messageStack; - - @Override - public void onPresenceMessage(PresenceMessage message) { - messageStack.add(message); - } - - public Presence.PresenceListener setMessageStack(List messageStack) { - this.messageStack = messageStack; - return this; - } - }.setMessageStack(receivedMessageStack); - - /* Subscribe using various alternatives of {@code Presence#subscribe()} */ - channel2.presence.subscribe(listener); - channel2.presence.subscribe(Action.present, listener); - channel2.presence.subscribe(EnumSet.of(Action.update, Action.leave), listener); - - /* Unsubscribe */ - channel2.presence.unsubscribe(listener); - - /* Start emitting channel with ably client 1 (emitter) */ - channel1.presence.enter("Hello, #2!", null); - channel1.presence.update("Lorem ipsum", null); - channel1.presence.update("Dolor sit!", null); - channel1.presence.leave(null); - - /* Wait until receiver client (ably2) observes {@code Action.leave} - * is emitted from emitter client (ably1) - */ - Helpers.PresenceWaiter leavePresenceWaiter = new Helpers.PresenceWaiter(channel2); - leavePresenceWaiter.waitFor(ably1.options.clientId, Action.leave); - - /* Validate that we didn't received anything - */ - assertThat(receivedMessageStack, is(emptyCollectionOf(PresenceMessage.class))); - } finally { - if (ably1 != null) ably1.close(); - if (ably2 != null) ably2.close(); - } - } - - /** - *

- * Validates a client can observe presence messages of other client, - * when they entered to the same channel and observing client subscribed - * to multiple actions. - *

- * - * @throws AblyException - */ - @Test - public void realtime_presence_subscribe_all() throws AblyException { - /* Ably instance that will emit presence events */ - AblyRealtime ably1 = null; - /* Ably instance that will receive presence events */ - AblyRealtime ably2 = null; - - String channelName = "test.presence.subscribe.all" + System.currentTimeMillis(); - - try { - ClientOptions option1 = createOptions(testVars.keys[0].keyStr); - option1.clientId = "emitter client"; - ClientOptions option2 = createOptions(testVars.keys[0].keyStr); - option2.clientId = "receiver client"; - - ably1 = new AblyRealtime(option1); - ably2 = new AblyRealtime(option2); - - Channel channel1 = ably1.channels.get(channelName); - channel1.attach(); - (new ChannelWaiter(channel1)).waitFor(ChannelState.attached); - - Channel channel2 = ably2.channels.get(channelName); - channel2.attach(); - (new ChannelWaiter(channel2)).waitFor(ChannelState.attached); - - ArrayList receivedMessageStack = new ArrayList<>(); - channel2.presence.subscribe(new Presence.PresenceListener() { - List messageStack; - - @Override - public void onPresenceMessage(PresenceMessage message) { - messageStack.add(message); - } - - public Presence.PresenceListener setMessageStack(List messageStack) { - this.messageStack = messageStack; - return this; - } - }.setMessageStack(receivedMessageStack)); - - /* Start emitting channel with ably client 1 (emitter) */ - channel1.presence.enter("Hello, #2!", null); - channel1.presence.update("Lorem ipsum", null); - channel1.presence.update("Dolor sit!", null); - channel1.presence.leave(null); - - /* Wait until receiver client (ably2) observes {@code Action.leave} - * is emitted from emitter client (ably1) - */ - Helpers.PresenceWaiter leavePresenceWaiter = new Helpers.PresenceWaiter(channel2); - leavePresenceWaiter.waitFor(ably1.options.clientId, Action.leave); - - /* Validate that, - * - we received all actions - */ - assertThat(receivedMessageStack.size(), is(equalTo(4))); - for (PresenceMessage message : receivedMessageStack) { - assertThat(message.action, isOneOf(Action.enter, Action.update, Action.leave)); - } - } finally { - if (ably1 != null) ably1.close(); - if (ably2 != null) ably2.close(); - } - } - - /** - *

- * Validates a client can observe presence messages of other client, - * when they entered to the same channel and observing client subscribed - * to multiple actions. - *

- * - * @throws AblyException - */ - @Test - public void realtime_presence_subscribe_multiple() throws AblyException { - /* Ably instance that will emit presence events */ - AblyRealtime ably1 = null; - /* Ably instance that will receive presence events */ - AblyRealtime ably2 = null; - - String channelName = "test.presence.subscribe.multiple" + System.currentTimeMillis(); - EnumSet actions = EnumSet.of(Action.update, Action.leave); - - try { - ClientOptions option1 = createOptions(testVars.keys[0].keyStr); - option1.clientId = "emitter client"; - ClientOptions option2 = createOptions(testVars.keys[0].keyStr); - option2.clientId = "receiver client"; - - ably1 = new AblyRealtime(option1); - ably2 = new AblyRealtime(option2); - - Channel channel1 = ably1.channels.get(channelName); - channel1.attach(); - (new ChannelWaiter(channel1)).waitFor(ChannelState.attached); - - Channel channel2 = ably2.channels.get(channelName); - channel2.attach(); - (new ChannelWaiter(channel2)).waitFor(ChannelState.attached); - - final ArrayList receivedMessageStack = new ArrayList<>(); - channel2.presence.subscribe(actions, new Presence.PresenceListener() { - @Override - public void onPresenceMessage(PresenceMessage message) { - synchronized (receivedMessageStack) { - receivedMessageStack.add(message); - receivedMessageStack.notify(); - } - } - }); - - /* Start emitting channel with ably client 1 (emitter) */ - channel1.presence.enter("Hello, #2!", null); - channel1.presence.update("Lorem ipsum", null); - channel1.presence.update("Dolor sit!", null); - channel1.presence.leave(null); - - /* Wait until receiver client (ably2) observes {@code Action.leave} - * is emitted from emitter client (ably1) - */ - try { - synchronized (receivedMessageStack) { - while (receivedMessageStack.size() == 0 || - !receivedMessageStack.get(receivedMessageStack.size()-1).clientId.equals(ably1.options.clientId) || - receivedMessageStack.get(receivedMessageStack.size()-1).action != Action.leave) - receivedMessageStack.wait(); - } - } catch(InterruptedException e) {} - - /* Validate that, - * - we received specific actions - */ - assertThat(receivedMessageStack.size(), is(equalTo(3))); - for (PresenceMessage message : receivedMessageStack) { - assertTrue(actions.contains(message.action)); - } - } finally { - if (ably1 != null) ably1.close(); - if (ably2 != null) ably2.close(); - } - } - - /** - *

- * Validates a client can observe presence messages of other client, - * when they entered to the same channel and observing client subscribed - * to a single action. - *

- * - * @throws AblyException - */ - @Test - public void realtime_presence_subscribe_single() throws AblyException { - /* Ably instance that will emit presence events */ - AblyRealtime ably1 = null; - /* Ably instance that will receive presence events */ - AblyRealtime ably2 = null; - - String channelName = "test.presence.subscribe.single." + System.currentTimeMillis(); - PresenceMessage.Action action = Action.enter; - - try { - ClientOptions option1 = createOptions(testVars.keys[0].keyStr); - option1.clientId = "emitter client"; - ClientOptions option2 = createOptions(testVars.keys[0].keyStr); - option2.clientId = "receiver client"; - - ably1 = new AblyRealtime(option1); - ably2 = new AblyRealtime(option2); - - Channel channel1 = ably1.channels.get(channelName); - channel1.attach(); - (new ChannelWaiter(channel1)).waitFor(ChannelState.attached); - - Channel channel2 = ably2.channels.get(channelName); - channel2.attach(); - (new ChannelWaiter(channel2)).waitFor(ChannelState.attached); - - ArrayList receivedMessageStack = new ArrayList<>(); - channel2.presence.subscribe(action, new Presence.PresenceListener() { - List messageStack; - - @Override - public void onPresenceMessage(PresenceMessage message) { - messageStack.add(message); - } - - public Presence.PresenceListener setMessageStack(List messageStack) { - this.messageStack = messageStack; - return this; - } - }.setMessageStack(receivedMessageStack)); - - Helpers.PresenceWaiter waiter = new Helpers.PresenceWaiter(channel2); - - /* Start emitting presence with ably client 1 (emitter) */ - channel1.presence.enter("Hello, #2!", null); - channel1.presence.updatePresence(new PresenceMessage(Action.update, ably1.options.clientId), null); - channel1.presence.update("Lorem Ipsum", null); - channel1.presence.leave(null); - - /* Wait until receiver client (ably2) observes {@code Action.leave} - * is emitted from emitter client (ably1) - */ - waiter.waitFor(ably1.options.clientId, Action.leave); - - /* Validate that, - * - we received specific actions - */ - assertThat(receivedMessageStack, is(not(empty()))); - for (PresenceMessage message : receivedMessageStack) { - assertThat(message.action, is(equalTo(action))); - } - } finally { - if (ably1 != null) ably1.close(); - if (ably2 != null) ably2.close(); - } - } - - /** - *

- * Validate {@code Presence#subscribe(...)} will result in the listener not being - * registered and an error being indicated, when the channel moves to the FAILED - * state before the operation succeeds - *

- *

- * Spec: RTP6c - *

- * - * @throws AblyException - */ - @Test - public void realtime_presence_attach_implicit_subscribe_fail() throws AblyException { - AblyRealtime ably = null; - try { - ClientOptions optsForToken = createOptions(testVars.keys[0].keyStr); - final AblyRest ablyForToken = new AblyRest(optsForToken); - final String channelName = "realtime_presence_attach_implicit_subscribe_fail" + testParams.name; - - /* get first token */ - Auth.TokenParams tokenParams = new Auth.TokenParams(); - Capability capability = new Capability(); - capability.addResource("otherchannel", "publish"); - tokenParams.capability = capability.toString(); - tokenParams.clientId = testClientId1; - - Auth.TokenDetails token = ablyForToken.auth.requestToken(tokenParams, null); - - /* get second token */ - Auth.TokenParams tokenParams2 = new Auth.TokenParams(); - Capability capability2 = new Capability(); - capability2.addResource(channelName, "publish"); - capability2.addOperation(channelName, "presence"); - capability2.addOperation(channelName, "subscribe"); - tokenParams2.capability = capability2.toString(); - tokenParams2.clientId = testClientId1; - - final Auth.TokenDetails token2 = ablyForToken.auth.requestToken(tokenParams2, null); - assertNotNull("Expected token value", token2.token); - - ClientOptions opts = createOptions(testVars.keys[0].keyStr); - opts.autoConnect = false; - opts.tokenDetails = token; - opts.clientId = testClientId1; - ably = new AblyRealtime(opts); - - final ArrayList presenceMessages = new ArrayList<>(); - Presence.PresenceListener listener = new Presence.PresenceListener() { - @Override - public void onPresenceMessage(PresenceMessage message) { - synchronized (presenceMessages) { - presenceMessages.add(message); - presenceMessages.notify(); - } - } - }; - - /* create a channel and subscribe, implicitly initiate attach */ - CompletionWaiter completionWaiter = new CompletionWaiter(); - final Channel channel = ably.channels.get(channelName); - channel.presence.subscribe(listener, completionWaiter); - - ably.connection.connect(); - - completionWaiter.waitFor(1); - assertFalse("Verify subscribe failed", completionWaiter.success); - assertEquals("Verify subscribe failure error status", completionWaiter.error.statusCode, 401); - assertEquals("Verify failed state reached", channel.state, ChannelState.failed); - - try { - channel.presence.subscribe(new PresenceWaiter(channel)); - fail("Presence.subscribe() shouldn't succeed"); - } catch (AblyException e) { - assertEquals("Verify failure error code", e.errorInfo.code, 90001); - } - - /* Change token to allow channel subscription so we can enter client and verify listener was set despite the failure */ - final boolean[] authUpdated = new boolean[]{false}; - ably.connection.on(ConnectionEvent.update, new ConnectionStateListener() { - @Override - public void onConnectionStateChanged(ConnectionStateChange state) { - synchronized (authUpdated) { - authUpdated[0] = true; - authUpdated.notify(); - } - } - }); - - - ably.auth.authorize(null, new Auth.AuthOptions() {{ - tokenDetails = token2; - }}); - - try { - synchronized (authUpdated) { - while (!authUpdated[0]) - authUpdated.wait(); - } - } catch (InterruptedException e) {} - - channel.attach(); - new ChannelWaiter(channel).waitFor(ChannelState.attached); - - /* Now to ensure listener was set despite the error we enter a client */ - channel.presence.enter(null, null); - try { - synchronized (presenceMessages) { - while (presenceMessages.size() == 0) - presenceMessages.wait(); - } - } catch (InterruptedException e) {} - - assertTrue("Verify listener was set despite channel attach failure", - presenceMessages.size() == 1 && - presenceMessages.get(0).action == Action.enter && presenceMessages.get(0).clientId.equals(testClientId1)); - - } finally { - if(ably != null) - ably.close(); - } - } - - /** - *

- * Validate {@code Presence#enter(...)} will result in the listener not being - * registered and an error being indicated, when the channel moves to the - * FAILED state before the operation succeeds - *

- *

- * Spec: RTP8d - *

- * - * @throws AblyException - */ - @Test - public void realtime_presence_attach_implicit_enter_fail() throws AblyException { - AblyRealtime ably = null; - try { - ClientOptions opts = createOptions(testVars.keys[1].keyStr); - opts.clientId = "theClient"; - ably = new AblyRealtime(opts); - - /* wait until connected */ - new ConnectionWaiter(ably.connection).waitFor(ConnectionState.connected); - assertEquals("Verify connected state reached", ably.connection.state, ConnectionState.connected); - - /* create a channel and subscribe */ - final Channel channel = ably.channels.get("enter_fail_" + testParams.name); - CompletionWaiter completionWaiter = new CompletionWaiter(); - channel.presence.enter("Lorem Ipsum", completionWaiter); - assertEquals("Verify attaching state reached", channel.state, ChannelState.attaching); - - ErrorInfo errorInfo = completionWaiter.waitFor(); - - new ChannelWaiter(channel).waitFor(ChannelState.failed); - assertEquals("Verify failed state reached", channel.state, ChannelState.failed); - assertEquals("Verify reason code gives correct failure reason", errorInfo.statusCode, 401); - } finally { - if(ably != null) - ably.close(); - } - } - - /** - *

- * Validate {@code Presence#get(...)} will result in an error, when the channel - * moves to the FAILED state before the operation succeeds - *

- *

- * Spec: RTP11b - *

- * - * @throws AblyException - */ - @Test - public void realtime_presence_attach_implicit_get_fail() throws AblyException { - AblyRealtime ably = null; - try { - ClientOptions opts = createOptions(testVars.keys[1].keyStr); - ably = new AblyRealtime(opts); - - /* wait until connected */ - new ConnectionWaiter(ably.connection).waitFor(ConnectionState.connected); - assertEquals("Verify connected state reached", ably.connection.state, ConnectionState.connected); - - /* create a channel and subscribe */ - final Channel channel = ably.channels.get("get_fail"); - channel.presence.get(false); - assertEquals("Verify attaching state reached", channel.state, ChannelState.attaching); - - ErrorInfo fail = new ChannelWaiter(channel).waitFor(ChannelState.failed); - assertEquals("Verify failed state reached", channel.state, ChannelState.failed); - assertEquals("Verify reason code gives correct failure reason", fail.statusCode, 401); - } finally { - if(ably != null) - ably.close(); - } - } - - /** - *

- * Validate {@code Presence#enterClient(...)} will result in the listener not being - * registered and an error being indicated, when the channel moves to the FAILED - * state before the operation succeeds - *

- *

- * Spec: RTP15e - *

- * - * @throws AblyException - */ - @Test - public void realtime_presence_attach_implicit_enterclient_fail() throws AblyException { - AblyRealtime ably = null; - try { - ClientOptions opts = createOptions(testVars.keys[1].keyStr); - ably = new AblyRealtime(opts); - - /* wait until connected */ - new ConnectionWaiter(ably.connection).waitFor(ConnectionState.connected); - assertEquals("Verify connected state reached", ably.connection.state, ConnectionState.connected); - - /* create a channel and subscribe */ - final Channel channel = ably.channels.get("enterclient_fail_" + testParams.name); - CompletionWaiter completionWaiter = new CompletionWaiter(); - channel.presence.enterClient("theClient", "Lorem Ipsum", completionWaiter); - assertEquals("Verify attaching state reached", channel.state, ChannelState.attaching); - - ErrorInfo errorInfo = completionWaiter.waitFor(); - - new ChannelWaiter(channel).waitFor(ChannelState.failed); - assertEquals("Verify failed state reached", channel.state, ChannelState.failed); - assertEquals("Verify reason code gives correct failure reason", errorInfo.statusCode, 401); - } finally { - if(ably != null) - ably.close(); - } - } - - /** - *

- * Validate {@code Presence#updateClient(...)} will result in the listener not being - * registered and an error being indicated, when the channel is in or moves to the - * FAILED state before the operation succeeds - *

- *

- * Spec: RTP15e - *

- * - * @throws AblyException - */ - @Test - public void realtime_presence_attach_implicit_updateclient_fail() throws AblyException { - AblyRealtime ably = null; - try { - ClientOptions opts = createOptions(testVars.keys[1].keyStr); - ably = new AblyRealtime(opts); - - /* wait until connected */ - new ConnectionWaiter(ably.connection).waitFor(ConnectionState.connected); - assertEquals("Verify connected state reached", ably.connection.state, ConnectionState.connected); - - /* create a channel and subscribe */ - final Channel channel = ably.channels.get("updateclient_fail_" + testParams.name); - CompletionWaiter completionWaiter = new CompletionWaiter(); - channel.presence.updateClient("theClient", "Lorem Ipsum", completionWaiter); - assertEquals("Verify attaching state reached", channel.state, ChannelState.attaching); - - ErrorInfo errorInfo = completionWaiter.waitFor(); - - new ChannelWaiter(channel).waitFor(ChannelState.failed); - assertEquals("Verify failed state reached", channel.state, ChannelState.failed); - assertEquals("Verify reason code gives correct failure reason", errorInfo.statusCode, 401); - } finally { - if(ably != null) - ably.close(); - } - } - - /** - *

- * Validate {@code Presence#leaveClient(...)} will result in the listener not being - * registered and an error being indicated, when the channel is in or moves to the - * FAILED state before the operation succeeds - *

- *

- * Spec: RTP15e - *

- * - * @throws AblyException - */ - @Test - public void realtime_presence_attach_implicit_leaveclient_fail() throws AblyException { - AblyRealtime ably = null; - try { - ClientOptions opts = createOptions(testVars.keys[1].keyStr); - ably = new AblyRealtime(opts); - - /* wait until connected */ - new ConnectionWaiter(ably.connection).waitFor(ConnectionState.connected); - assertEquals("Verify connected state reached", ably.connection.state, ConnectionState.connected); - - /* create a channel and subscribe */ - final Channel channel = ably.channels.get("leaveclient_fail+" + testParams.name); - CompletionWaiter completionWaiter = new CompletionWaiter(); - channel.presence.leaveClient("theClient", "Lorem Ipsum", completionWaiter); - assertEquals("Verify attaching state reached", channel.state, ChannelState.attaching); - completionWaiter.waitFor(); - - ErrorInfo errorInfo = completionWaiter.waitFor(); - - new ChannelWaiter(channel).waitFor(ChannelState.failed); - assertEquals("Verify failed state reached", channel.state, ChannelState.failed); - assertEquals("Verify reason code gives correct failure reason", errorInfo.statusCode, 401); - } finally { - if(ably != null) - ably.close(); - } - } - - /** - *

- * Validate {@code Presence#get(...)} throws an exception, when the channel - * is in the FAILED state - *

- * - * @throws AblyException - */ - @Test - public void realtime_presence_get_throws_when_channel_failed() throws AblyException { - AblyRealtime ably = null; - try { - ClientOptions opts = createOptions(testVars.keys[1].keyStr); - ably = new AblyRealtime(opts); - - /* wait until connected */ - new ConnectionWaiter(ably.connection).waitFor(ConnectionState.connected); - assertEquals("Verify connected state reached", ably.connection.state, ConnectionState.connected); - - /* create a channel and subscribe */ - final Channel channel = ably.channels.get("get_fail"); - channel.attach(); - new ChannelWaiter(channel).waitFor(ChannelState.failed); - - try { - channel.presence.get(false); - fail("Presence#get(...) should throw an exception when channel is in failed state"); - } catch(AblyException e) { - assertThat(e.errorInfo.code, is(equalTo(90001))); - assertThat(e.errorInfo.message, is(equalTo("channel operation failed (invalid channel state)"))); - } - } finally { - if(ably != null) - ably.close(); - } - } - - /** - * Test if after reattach when returning from suspended mode client re-enters the channel with the same data - * @throws AblyException - * - * Tests RTP17, RTP19, RTP19a, RTP5f, RTP6b - */ - @Test - public void realtime_presence_suspended_reenter() throws AblyException { - AblyRealtime ably = null; - try { - MockWebsocketFactory mockTransport = new MockWebsocketFactory(); - DebugOptions opts = new DebugOptions(testVars.keys[0].keyStr); - fillInOptions(opts); - opts.transportFactory = mockTransport; - - for (int i=0; i<2; i++) { - final String channelName = "presence_suspended_reenter" + testParams.name + String.valueOf(i); - - mockTransport.allowSend(); - - ably = new AblyRealtime(opts); - - ConnectionWaiter connectionWaiter = new ConnectionWaiter(ably.connection); - connectionWaiter.waitFor(ConnectionState.connected); - - final Channel channel = ably.channels.get(channelName); - channel.attach(); - ChannelWaiter channelWaiter = new ChannelWaiter(channel); - - channelWaiter.waitFor(ChannelState.attached); - - final String presenceData = "PRESENCE_DATA"; - final String connId = ably.connection.id; - - /* - * On the first run to test RTP19a we don't enter client1 so the server on - * return from suspend sees no presence data and sends ATTACHED without HAS_PRESENCE - * The client then should remove all the members from the presence map and then - * re-enter client2. On the second loop run we enter client1 and receive ATTACHED with - * HAS_PRESENCE - */ - final boolean[] wrongPresenceEmitted = new boolean[] {false}; - if (i == 1) { - CompletionWaiter completionWaiter = new CompletionWaiter(); - channel.presence.enterClient(testClientId1, presenceData, completionWaiter); - completionWaiter.waitFor(); - - // RTP5f: after this point there should be no presence event for client1 - channel.presence.subscribe(new Presence.PresenceListener() { - @Override - public void onPresenceMessage(PresenceMessage message) { - if (message.clientId.equals(testClientId1)) - wrongPresenceEmitted[0] = true; - } - }); - } - - final ArrayList leaveMessages = new ArrayList<>(); - /* Subscribe for message type, test RTP6b */ - channel.presence.subscribe(Action.leave, new Presence.PresenceListener() { - @Override - public void onPresenceMessage(PresenceMessage message) { - leaveMessages.add(message); - } - }); - - /* - * We put testClientId2 presence data into the client library presence map but we - * don't send it to the server - */ - - mockTransport.blockSend(); - channel.presence.enterClient(testClientId2, presenceData); - - ProtocolMessage msg = new ProtocolMessage(); - msg.connectionId = connId; - msg.action = ProtocolMessage.Action.sync; - msg.channel = channelName; - msg.presence = new PresenceMessage[]{ - new PresenceMessage() {{ - action = Action.present; - id = String.format("%s:0:0", connId); - timestamp = System.currentTimeMillis(); - clientId = testClientId2; - connectionId = connId; - data = presenceData; - }} - }; - ably.connection.connectionManager.onMessage(null, msg); - - mockTransport.allowSend(); - - ably.connection.connectionManager.requestState(ConnectionState.suspended); - channelWaiter.waitFor(ChannelState.suspended); - - /* - * When restoring from suspended state server will send sync message erasing - * testClientId2 record from the presence map. Client should re-send presence message - * for testClientId2 and restore its presence data. - */ - - ably.connection.connectionManager.requestState(ConnectionState.connected); - channelWaiter.waitFor(ChannelState.attached); - long reconnectTimestamp = System.currentTimeMillis(); - - try { - Thread.sleep(500); - } catch (InterruptedException e) { - } - - AblyRest ablyRest = new AblyRest(opts); - io.ably.lib.rest.Channel restChannel = ablyRest.channels.get(channelName); - assertEquals("Verify presence data is received by the server", - restChannel.presence.get(null).items().length, i==0 ? 1 : 2); - - /* In both cases we should have one leave message in the leaveMessages */ - assertEquals("Verify exactly one LEAVE message was generated", leaveMessages.size(), 1); - - PresenceMessage leaveMessage = leaveMessages.get(0); - assertEquals("Verify LEAVE message follows specs",leaveMessage.action, Action.leave); - assertEquals("Verify LEAVE message follows specs",leaveMessage.clientId, testClientId2); - assertEquals("Verify LEAVE message follows specs",leaveMessage.data, presenceData); - assertTrue("Verify LEAVE message follows specs", Math.abs(leaveMessage.timestamp-reconnectTimestamp) < 2000); - - /* According to RTP5f there should be no presence event emitted for client1 */ - assertFalse("Verify no presence event emitted on return from suspend on SYNC for client1", - wrongPresenceEmitted[0]); - } - } finally { - if(ably != null) - ably.close(); - } - } - - /** - * Test presence message map behaviour (RTP2 features) - * Tests RTP2a, RTP2b1, RTP2b2, RTP2c, RTP2d, RTP2g, RTP18c, RTP6a features - */ - @Test - public void realtime_presence_map_test() throws AblyException { - AblyRealtime ably = null; - try { - ClientOptions opts = createOptions(testVars.keys[0].keyStr); - ably = new AblyRealtime(opts); - final String channelName = "newness_comparison_" + testParams.name; - Channel channel = ably.channels.get(channelName); - channel.attach(); - ChannelWaiter channelWaiter = new ChannelWaiter(channel); - channelWaiter.waitFor(ChannelState.attached); - - final String wontPass = "Won't pass newness test"; - - Presence presence = channel.presence; - final ArrayList presenceMessages = new ArrayList<>(); - /* Subscribe for all the message types, test RTP6a */ - presence.subscribe(new Presence.PresenceListener() { - @Override - public void onPresenceMessage(PresenceMessage message) { - synchronized (presenceMessages) { - assertNotEquals("Verify wrong message didn't pass the newness test", - message.data, wontPass); - // To exclude leave messages that sometimes sneak in let's collect only enter and update messages - if (message.action == Action.enter || message.action == Action.update) { - presenceMessages.add(message); - } - } - } - }); - - /* Test message newness criteria as described in RTP2b */ - final PresenceMessage[] testData = new PresenceMessage[] { - new PresenceMessage() {{ - clientId = "1"; - action = Action.enter; - connectionId = "1"; - id = "1:0"; - }}, - new PresenceMessage() {{ - clientId = "2"; - action = Action.enter; - connectionId = "2"; - id = "2:1:0"; - }}, - /* Should be newer than previous one */ - new PresenceMessage() {{ - clientId = "2"; - action = Action.update; - connectionId = "2"; - id = "2:2:1"; - timestamp = 1; - }}, - /* Shouldn't pass newness test because of message serial, timestamp doesn't matter in this case */ - new PresenceMessage() {{ - clientId = "2"; - action = Action.update; - connectionId = "2"; - id = "2:1:1"; - timestamp = 2; - data = wontPass; - }}, - /* Shouldn't pass because of message index */ - new PresenceMessage() {{ - clientId = "2"; - action = Action.update; - connectionId = "2"; - id = "2:2:0"; - data = wontPass; - }}, - /* Should pass because id is not in form connId:clientId:index and timestamp is greater */ - new PresenceMessage() {{ - clientId = "2"; - action = Action.update; - connectionId = "2"; - id = "weird_id"; - timestamp = 1000; - }}, - /* Shouldn't pass because of timestamp */ - new PresenceMessage() {{ - clientId = "2"; - action = Action.update; - connectionId = "2"; - id = "2:3:1"; - timestamp = 500; - data = wontPass; - }} - }; - - for (final PresenceMessage msg: testData) { - ProtocolMessage protocolMessage = new ProtocolMessage() {{ - channel = channelName; - action = Action.presence; - presence = new PresenceMessage[]{msg}; - }}; - - ably.connection.connectionManager.onMessage(null, protocolMessage); - } - - int n = 0; - for (PresenceMessage testMsg: testData) { - if (testMsg.data != wontPass) { - PresenceMessage factualMsg = n < presenceMessages.size() ? presenceMessages.get(n++) : null; - assertTrue("Verify message passed newness test", - factualMsg != null && factualMsg.id.equals(testMsg.id)); - assertEquals("Verify message was emitted on the presence object with original action", - factualMsg.action, testMsg.action); - assertEquals("Verify message was added to the presence map and stored with PRESENT action", - presence.get(testMsg.clientId, false)[0].action, Action.present); - } - } - assertEquals("Verify nothing else passed the newness test", n, presenceMessages.size()); - - /* Repeat the process now as a part of SYNC and verify everything is exactly the same */ - final String channel2Name = "sync_newness_comparison_" + testParams.name; - Channel channel2 = ably.channels.get(channel2Name); - channel2.attach(); - new ChannelWaiter(channel2).waitFor(ChannelState.attached); - - /* Send all the presence data in one SYNC message without channelSerial (RTP18c) */ - ProtocolMessage syncMessage = new ProtocolMessage() {{ - channel = channel2Name; - action = Action.sync; - presence = testData.clone(); - }}; - final ArrayList syncPresenceMessages = new ArrayList<>(); - channel2.presence.subscribe(new Presence.PresenceListener() { - @Override - public void onPresenceMessage(PresenceMessage message) { - syncPresenceMessages.add(message); - } - }); - ably.connection.connectionManager.onMessage(null, syncMessage); - - assertEquals("Verify result is the same in case of SYNC", syncPresenceMessages.size(), presenceMessages.size()); - for (int i=0; i100) number of clients so there are several sync messages, disconnect transport - * in the middle and verify channel is re-syncing presence messages after transport reconnect - * - * Tests RTP3 - */ - @Test - public void reattach_resume_broken_sync() { - AblyRealtime clientAbly1 = null; - AblyRealtime clientAbly2 = null; - TestChannel testChannel = new TestChannel(); - int clientCount = 150; /* Should be greater than 100 to break sync into several messages */ - try { - /* subscribe for presence events in the anonymous connection */ - new PresenceWaiter(testChannel.realtimeChannel); - - /* set up a connection with specific clientId */ - ClientOptions client1Opts = new ClientOptions(testVars.keys[0].keyStr); - fillInOptions(client1Opts); - client1Opts.tokenDetails = wildcardToken; - clientAbly1 = new AblyRealtime(client1Opts); - - /* wait until connected */ - (new ConnectionWaiter(clientAbly1.connection)).waitFor(ConnectionState.connected); - assertEquals("Verify connected state reached", clientAbly1.connection.state, ConnectionState.connected); - - /* get channel and attach */ - Channel client1Channel = clientAbly1.channels.get(testChannel.channelName); - client1Channel.attach(); - (new ChannelWaiter(client1Channel)).waitFor(ChannelState.attached); - assertEquals("Verify attached state reached", client1Channel.state, ChannelState.attached); - - /* let client1 enter the channel for multiple clients and wait for the success callback */ - CompletionSet enterComplete = new CompletionSet(); - for(int i = 0; i < clientCount; i++) { - client1Channel.presence.enterClient("client" + i, "Test data (attach_enter_multiple) " + i, enterComplete.add()); - } - enterComplete.waitFor(); - assertTrue("Verify enter callback called on completion", enterComplete.pending.isEmpty()); - assertTrue("Verify no enter errors", enterComplete.errors.isEmpty()); - - /* set up a second connection with different clientId */ - final MockWebsocketFactory mockTransport20 = new MockWebsocketFactory(); - DebugOptions client2Opts = new DebugOptions(testVars.keys[0].keyStr); - fillInOptions(client2Opts); - client2Opts.transportFactory = mockTransport20; - client2Opts.tokenDetails = token2; - client2Opts.clientId = testClientId2; - client2Opts.autoConnect = false; - - mockTransport20.allowSend(); - clientAbly2 = new AblyRealtime(client2Opts); - - /* wait until connected */ - ConnectionWaiter connectionWaiter = new ConnectionWaiter(clientAbly2.connection); - clientAbly2.connection.connect(); - connectionWaiter.waitFor(ConnectionState.connected); - - /* get channel */ - final Channel client2Channel = clientAbly2.channels.get(testChannel.channelName); - final ConnectionManager connectionManager = clientAbly2.connection.connectionManager; - final boolean[] disconnectedTransport = new boolean[]{false}; - final int[] presenceCount = new int[]{0}; - client2Channel.attach(new CompletionListener() { - @Override - public void onSuccess() { - try { - client2Channel.presence.subscribe(new Presence.PresenceListener() { - @Override - public void onPresenceMessage(PresenceMessage message) { - if (!disconnectedTransport[0]) { - mockTransport20.lastCreatedTransport.close(); - connectionManager.onTransportUnavailable(mockTransport20.lastCreatedTransport, new ErrorInfo("Mock", 50000)); - - } - disconnectedTransport[0] = true; - presenceCount[0]++; - } - }); - } - catch (AblyException e) { - } - } - - @Override - public void onError(ErrorInfo reason) { - } - }); - - ChannelWaiter channelWaiter = new ChannelWaiter(client2Channel); - channelWaiter.waitFor(ChannelState.attached); - - /* Wait for reconnect */ - connectionWaiter.waitFor(ConnectionState.connected, 2); - - client2Channel.presence.unsubscribe(); - - /* Verify that channel received sync and all 150 presence messages are received */ - try { - Thread.sleep(500); - assertEquals("Verify number of received presence messages", client2Channel.presence.get(true).length, clientCount); - } catch (InterruptedException e) {} - - } catch(AblyException e) { - e.printStackTrace(); - fail("Unexpected exception running test: " + e.getMessage()); - } finally { - if(clientAbly1 != null) - clientAbly1.close(); - if(clientAbly2 != null) - clientAbly2.close(); - testChannel.dispose(); - } - } - - /** - * Test if presence sync works as it should - * Tests RTP18a, RTP18b, RTP2f - */ - @Test - public void presence_sync() { - AblyRealtime ably = null; - try { - ClientOptions opts = createOptions(testVars.keys[0].keyStr); - ably = new AblyRealtime(opts); - - final String channelName = "presence_sync_test" + testParams.name; - - final Channel channel = ably.channels.get(channelName); - channel.attach(); - ChannelWaiter channelWaiter = new ChannelWaiter(channel); - channelWaiter.waitFor(ChannelState.attached); - - final ArrayList presenceHistory = new ArrayList<>(); - channel.presence.subscribe(new Presence.PresenceListener() { - @Override - public void onPresenceMessage(PresenceMessage message) { - presenceHistory.add(message); - } - }); - - final PresenceMessage[] testPresence1 = new PresenceMessage[] { - /* Will be discarded because we'll start new sync with different channelSerial */ - new PresenceMessage() {{ - clientId = "1"; - action = Action.enter; - connectionId = "1"; - id = "1:0"; - }} - }; - - final PresenceMessage[] testPresence2 = new PresenceMessage[] { - new PresenceMessage() {{ - clientId = "2"; - action = Action.enter; - connectionId = "2"; - id = "2:1:0"; - }}, - /* Enter presence message here is newer than leave in the subsequent message */ - new PresenceMessage() {{ - clientId = "3"; - action = Action.enter; - connectionId = "3"; - id = "3:1:0"; - }} - }; - - final PresenceMessage[] testPresence3 = new PresenceMessage[] { - new PresenceMessage() {{ - clientId = "3"; - action = Action.leave; - connectionId = "3"; - id = "3:0:0"; - }}, - new PresenceMessage() {{ - clientId = "4"; - action = Action.enter; - connectionId = "4"; - id = "4:1:1"; - }}, - new PresenceMessage() {{ - clientId = "4"; - action = Action.leave; - connectionId = "4"; - id = "4:2:2"; - }} - }; - - final boolean[] seenLeaveMessageAsAbsentForClient4 = new boolean[] {false}; - channel.presence.subscribe(Action.leave, new Presence.PresenceListener() { - @Override - public void onPresenceMessage(PresenceMessage message) { - try { - /* - * Do not call it in states other than ATTACHED because of presence.get() side - * effect of attaching channel - */ - if (message.clientId.equals("4") && message.action == Action.leave && channel.state == ChannelState.attached) { - /* - * Client library won't return a presence message if it is stored as ABSENT - * so the result of the presence.get() call should be empty. This is the - * only case when get() called from PresenceListener.onPresenceMessage results - * in an empty answer. - */ - seenLeaveMessageAsAbsentForClient4[0] = channel.presence.get("4", false).length == 0; - } - } catch (AblyException e) {} - } - }); - - ably.connection.connectionManager.onMessage(null, new ProtocolMessage() {{ - action = Action.sync; - channel = channelName; - channelSerial = "1:1"; - presence = testPresence1; - }}); - ably.connection.connectionManager.onMessage(null, new ProtocolMessage() {{ - action = Action.sync; - channel = channelName; - channelSerial = "2:1"; - presence = testPresence2; - }}); - ably.connection.connectionManager.onMessage(null, new ProtocolMessage() {{ - action = Action.sync; - channel = channelName; - channelSerial = "2:"; - presence = testPresence3; - }}); - - assertEquals("Verify incomplete sync was discarded", channel.presence.get("1", false).length, 0); - assertEquals("Verify client with id==2 is in presence map", channel.presence.get("2", false).length, 1); - assertEquals("Verify client with id==3 is in presence map", channel.presence.get("3", false).length, 1); - assertEquals("Verify nothing else is in presence map", channel.presence.get(false).length, 2); - - assertTrue("Verify LEAVE message for client with id==4 was stored as ABSENT", seenLeaveMessageAsAbsentForClient4[0]); - - PresenceMessage[] correctPresenceHistory = new PresenceMessage[] { - /* client 1 enters (will later be discarded) */ - new PresenceMessage(Action.enter, "1"), - /* client 2 enters */ - new PresenceMessage(Action.enter, "2"), - /* client 3 enters and never leaves because of newness comparison for LEAVE fails */ - new PresenceMessage(Action.enter, "3"), - /* client 4 enters and leaves */ - new PresenceMessage(Action.enter, "4"), - new PresenceMessage(Action.leave, "4"), - /* client 1 is eliminated from the presence map because the first portion of SYNC is discarded */ - new PresenceMessage(Action.leave, "1") - }; - - assertEquals("Verify number of presence messages", presenceHistory.size(), correctPresenceHistory.length); - for (int i=0; i sentPresence = new ArrayList<>(); - - /* Allow send but record all the presence messages for later analysis */ - final MockWebsocketFactory mockTransport = new MockWebsocketFactory(); - mockTransport.allowSend(new MockWebsocketFactory.MessageFilter() { - @Override - public boolean matches(ProtocolMessage message) { - if (message.action == ProtocolMessage.Action.presence && message.presence != null) { - synchronized (sentPresence) { - Collections.addAll(sentPresence, message.presence); - sentPresence.notify(); - } - } - return true; - } - }); - - DebugOptions opts = new DebugOptions(testVars.keys[0].keyStr); - fillInOptions(opts); - opts.clientId = testClientId1; - opts.transportFactory = mockTransport; - ably = new AblyRealtime(opts); - - Channel channel = ably.channels.get("protocol_enter_message_format_" + testParams.name); - /* using testClientId1 */ - channel.presence.enter(null, null); - - synchronized (sentPresence) { - while (sentPresence.size() < 1) - sentPresence.wait(); - } - - assertEquals("Verify number of presence messages sent", sentPresence.size(), 1); - assertTrue("Verify presence messages follows spec", - sentPresence.get(0).action == Action.enter && - sentPresence.get(0).clientId == null - ); - - channel.detach(); - new ChannelWaiter(channel).waitFor(ChannelState.detached); - - try { - channel.presence.enter(null, null); - fail("Presence.enter() shouldn't succeed in detached state"); - } catch (AblyException e) { - assertEquals("Verify exception error code", e.errorInfo.code, 91001 /* unable to enter presence channel (invalid channel state) */); - } - - } finally { - if (ably != null) - ably.close(); - } - } - - /** - * Verify protocol messages sent on Presence.enter() follow specs if sent from correct state and - * the call fails if sent from DETACHED state - * - * Tests RTP8c, RTP8g - */ - @Test - public void protocol_enterclient_message_format() throws AblyException, InterruptedException { - AblyRealtime ably = null; - - try { - final ArrayList sentPresence = new ArrayList<>(); - - /* Allow send but record all the presence messages for later analysis */ - final MockWebsocketFactory mockTransport = new MockWebsocketFactory(); - mockTransport.allowSend(new MockWebsocketFactory.MessageFilter() { - @Override - public boolean matches(ProtocolMessage message) { - if (message.action == ProtocolMessage.Action.presence && message.presence != null) { - synchronized (sentPresence) { - Collections.addAll(sentPresence, message.presence); - sentPresence.notify(); - } - } - return true; - } - }); - - DebugOptions opts = new DebugOptions(testVars.keys[0].keyStr); - fillInOptions(opts); - opts.transportFactory = mockTransport; - ably = new AblyRealtime(opts); - - Channel channel = ably.channels.get("protocol_enterclient_message_format_" + testParams.name); - /* using testClientId2 */ - channel.presence.enterClient(testClientId2); - - synchronized (sentPresence) { - while (sentPresence.size() < 1) - sentPresence.wait(); - } - - assertEquals("Verify number of presence messages sent", sentPresence.size(), 1); - assertTrue("Verify presence messages follows spec", - sentPresence.get(0).action == Action.enter && - sentPresence.get(0).clientId.equals(testClientId2) - ); - - channel.detach(); - new ChannelWaiter(channel).waitFor(ChannelState.detached); - - try { - channel.presence.enterClient("testClient3"); - fail("Presence.enterClient() shouldn't succeed in detached state"); - } catch (AblyException e) { - assertEquals("Verify exception error code", e.errorInfo.code, 91001 /* unable to enter presence channel (invalid channel state) */); - } - - } finally { - if (ably != null) - ably.close(); - } - } - - /* - * Verify presence data is received and encoded/decoded correctly - * Tests RTP8e, RTP6a - */ - @Test - public void presence_encoding() throws AblyException, InterruptedException { - AblyRealtime ably1 = null, ably2 = null; - try { - /* Set up two connections: one for entering, one for listening */ - final String channelName = "presence_encoding" + testParams.name; - ClientOptions opts = createOptions(testVars.keys[0].keyStr); - ably1 = new AblyRealtime(opts); - ably2 = new AblyRealtime(opts); - - Channel channel1 = ably1.channels.get(channelName); - Channel channel2 = ably2.channels.get(channelName); - - channel2.attach(); - new ChannelWaiter(channel2).waitFor(ChannelState.attached); - final ArrayList receivedPresenceData = new ArrayList<>(); - channel2.presence.subscribe(new Presence.PresenceListener() { - @Override - public void onPresenceMessage(PresenceMessage message) { - synchronized (receivedPresenceData) { - receivedPresenceData.add(message.data); - receivedPresenceData.notify(); - } - } - }); - - String testStringData = "123"; - byte[] testByteData = new byte[] {1, 2, 3}; - JsonElement testJsonData = new JsonParser().parse("{\"var1\":\"val1\", \"var2\": \"val2\"}"); - - channel1.presence.enterClient("1", testStringData); - channel1.presence.enterClient("2", testByteData); - channel1.presence.enterClient("3", testJsonData); - synchronized (receivedPresenceData) { - while (receivedPresenceData.size() < 3) - receivedPresenceData.wait(); - } - - assertEquals("Verify number of received presence messages", receivedPresenceData.size(), 3); - assertEquals("Verify string data", receivedPresenceData.get(0), testStringData); - assertTrue("Verify byte[] data", - receivedPresenceData.get(1) instanceof byte[] && - Arrays.equals((byte[])receivedPresenceData.get(1), testByteData)); - assertEquals("Verify JSON data", receivedPresenceData.get(2), testJsonData); - - /* use data from ENTER message */ - channel1.presence.leaveClient("1"); - /* use different data */ - channel1.presence.leaveClient("2", "leave"); - - synchronized (receivedPresenceData) { - while (receivedPresenceData.size() < 5) - receivedPresenceData.wait(); - } - - assertEquals("Verify string data for enter message is used in leave message", receivedPresenceData.get(3), testStringData); - assertEquals("Verify overridden leave data", receivedPresenceData.get(4), "leave"); - - } finally { - if (ably1 != null) - ably1.close(); - if (ably2 != null) - ably2.close(); - } - } - - /* - * Test Presence.get() filtering and syncToWait flag - * Tests RTP11b, RTP11c, RTP11d - */ - @Test - public void presence_get() throws AblyException, InterruptedException { - AblyRealtime ably1 = null, ably2 = null; - try { - /* Set up two connections: one for entering, one for listening */ - final String channelName = "presence_get" + testParams.name; - ClientOptions opts = createOptions(testVars.keys[0].keyStr); - ably1 = new AblyRealtime(opts); - opts.autoConnect = false; - ably2 = new AblyRealtime(opts); - - Channel channel1 = ably1.channels.get(channelName); - CompletionWaiter completionWaiter = new CompletionWaiter(); - channel1.presence.enterClient("1", null, completionWaiter); - channel1.presence.enterClient("2", null, completionWaiter); - completionWaiter.waitFor(2); - - Channel channel2 = ably2.channels.get(channelName); - PresenceWaiter waiter2 = new PresenceWaiter(channel2); - - /* - * Wait with waitForSync set to false, should result in 0 members because autoConnect is set to false - * This also tests implicit attach() - */ - PresenceMessage[] presenceMessages1 = channel2.presence.get(false); - assertEquals("Verify number of presence members before SYNC", presenceMessages1.length, 0); - - ably2.connection.connect(); - - /* now that waitForSync is true it should get all the members entered on first connection */ - PresenceMessage[] presenceMessages2 = channel2.presence.get(true); - assertEquals("Verify number of presence members after SYNC", presenceMessages2.length, 2); - - /* enter third member from second connection */ - channel2.presence.enterClient("3", null, completionWaiter); - completionWaiter.waitFor(3); - waiter2.waitFor(3); - - /* filter by clientId */ - PresenceMessage[] presenceMessages3 = channel2.presence.get(new Param(Presence.GET_CLIENTID, "1")); - assertTrue("Verify clientId filter works", - presenceMessages3.length == 1 && presenceMessages3[0].clientId.equals("1")); - - /* filter by connectionId */ - PresenceMessage[] presenceMessages4 = channel2.presence.get(new Param(Presence.GET_CONNECTIONID, ably2.connection.id)); - assertTrue("Verify connectionId filter works", - presenceMessages4.length == 1 && presenceMessages4[0].clientId.equals("3")); - - /* filter by both clientId and connectionId */ - PresenceMessage[] presenceMessages5 = channel2.presence.get( - new Param(Presence.GET_CONNECTIONID, ably1.connection.id), - new Param(Presence.GET_CLIENTID, "2") - ); - PresenceMessage[] presenceMessages6 = channel2.presence.get( - new Param(Presence.GET_CONNECTIONID, ably2.connection.id), - new Param(Presence.GET_CLIENTID, "2") - ); - assertTrue("Verify clientId+connectionId filter works", - presenceMessages5.length == 1 && presenceMessages5[0].clientId.equals("2") && presenceMessages6.length == 0); - - /* go into suspended mode */ - ably2.connection.connectionManager.requestState(ConnectionState.suspended); - new ConnectionWaiter(ably2.connection).waitFor(ConnectionState.suspended); - - /* try with wait set to false, should get all the three members */ - PresenceMessage[] presenceMessages7 = channel2.presence.get(false); - assertEquals("Verify Presence.get() with waitForSync set to false works in SUSPENDED state", presenceMessages7.length, 3); - - /* try with wait set to true, should get exception */ - try { - channel2.presence.get(true); - fail("Presence.get() with waitForSync=true shouldn't succeed in SUSPENDED state"); - } catch (AblyException e) { - assertEquals("Verify correct error code for Presence.get() with waitForSync=true in SUSPENDED state", e.errorInfo.code, 91005); - } - } finally { - if (ably1 != null) - ably1.close(); - if (ably2 != null) - ably2.close(); - } - } - - /** - * Authenticate using wildcard token, initialize AblyRealtime so clientId is not known a priori, - * call enter() without attaching first, start connection - * - * Expect NACK from the server because client is unidentified - * - * Tests RTP8i, RTP8f, partial tests for RTP9e, RTP10e - */ - @Test - public void enter_before_clientid_is_known() throws AblyException { - AblyRealtime ably = null; - try { - ClientOptions restOpts = createOptions(testVars.keys[0].keyStr); - AblyRest ablyForToken = new AblyRest(restOpts); - - /* Initialize connection so clientId is not known before actual connection */ - Auth.TokenParams tokenParams = new Auth.TokenParams(); - Capability capability = new Capability(); - tokenParams.capability = capability.toString(); - tokenParams.clientId = "*"; - - Auth.TokenDetails token = ablyForToken.auth.requestToken(tokenParams, null); - assertNotNull("Expected token value", token.token); - - ClientOptions opts = createOptions(); - opts.defaultTokenParams.clientId = "*"; - opts.token = token.token; - opts.autoConnect = false; - ably = new AblyRealtime(opts); - - /* enter without attaching first */ - Channel channel = ably.channels.get("enter_before_clientid_is_known"+testParams.name); - CompletionWaiter completionWaiter = new CompletionWaiter(); - channel.presence.enter(null, completionWaiter); - - ably.connection.connect(); - - completionWaiter.waitFor(1); - assertFalse("Verify enter() failed", completionWaiter.success); - assertEquals("Verify error code", completionWaiter.error.code, 40012); - - /* Now clientId is known to be "*" and subsequent enter() should fail immediately */ - completionWaiter.reset(); - channel.presence.enter(null, completionWaiter); - completionWaiter.waitFor(1); - assertFalse("Verify enter() failed", completionWaiter.success); - assertEquals("Verify error code", completionWaiter.error.code, 91000); - - /* and so should update() and leave() */ - completionWaiter.reset(); - channel.presence.update(null, completionWaiter); - completionWaiter.waitFor(1); - assertFalse("Verify update() failed", completionWaiter.success); - assertEquals("Verify error code", completionWaiter.error.code, 91000); - - completionWaiter.reset(); - channel.presence.leave(null, completionWaiter); - completionWaiter.waitFor(1); - assertFalse("Verify update() failed", completionWaiter.success); - assertEquals("Verify error code", completionWaiter.error.code, 91000); - - } finally { - if (ably != null) - ably.close(); - } - } - - /** - * To Test PresenceMessage.fromEncoded(JsonObject, ChannelOptions) and PresenceMessage.fromEncoded(String, ChannelOptions) - * Refer Spec TP4 - * @throws AblyException - */ - @Test - public void message_from_encoded_json_object() throws AblyException { - ChannelOptions options = null; - byte[] data = "0123456789".getBytes(); - PresenceMessage encoded = new PresenceMessage(Action.present, "client-123"); - encoded.data = data; - encoded.encode(options); - - PresenceMessage decoded = PresenceMessage.fromEncoded(Serialisation.gson.toJson(encoded), options); - assertEquals(encoded.clientId, decoded.clientId); - assertArrayEquals(data, (byte[]) decoded.data); - - /*Test JSON Data decoding in PresenceMessage.fromEncoded(JsonObject)*/ - JsonObject person = new JsonObject(); - person.addProperty("name", "Amit"); - person.addProperty("country", "Interlaken Ost"); - - PresenceMessage userDetails = new PresenceMessage(Action.absent, "client-123", person); - userDetails.encode(options); - - PresenceMessage decodedMessage1 = PresenceMessage.fromEncoded(Serialisation.gson.toJsonTree(userDetails).getAsJsonObject(), null); - assertEquals(person, decodedMessage1.data); - - /*Test PresenceMessage.fromEncoded(String)*/ - PresenceMessage decodedMessage2 = PresenceMessage.fromEncoded(Serialisation.gson.toJson(userDetails), options); - assertEquals(person, decodedMessage2.data); - - /*Test invalid case.*/ - try { - //We pass invalid PresenceMessage object - PresenceMessage.fromEncoded(person, options); - fail(); - } catch(Exception e) {/*ignore as we are expecting it to fail.*/} - } - - /** - * To test PresenceMessage.fromEncodedArray(JsonArray, ChannelOptions) and PresenceMessage.fromEncodedArray(String, ChannelOptions) - * Refer Spec. TP4 - * @throws AblyException - */ - @Test - public void messages_from_encoded_json_array() throws AblyException { - JsonArray fixtures = null; - MessagesData testMessages = null; - try { - testMessages = (MessagesData) Setup.loadJson(testMessagesEncodingFile, MessagesData.class); - JsonObject jsonObject = (JsonObject) Setup.loadJson(testMessagesEncodingFile, JsonObject.class); - //We use this as-is for decoding purposes. - fixtures = jsonObject.getAsJsonArray("messages"); - } catch(IOException e) { - fail(); - return; - } - PresenceMessage[] decodedMessages = PresenceMessage.fromEncodedArray(fixtures, null); - for(int index = 0; index < decodedMessages.length; index++) { - PresenceMessage testInputMsg = testMessages.messages[index]; - testInputMsg.decode(null); - if(testInputMsg.data instanceof byte[]) { - assertArrayEquals((byte[]) testInputMsg.data, (byte[]) decodedMessages[index].data); - } else { - assertEquals(testInputMsg.data, decodedMessages[index].data); - } - } - /*Test PresenceMessage.fromEncodedArray(String)*/ - String fixturesArray = Serialisation.gson.toJson(fixtures); - PresenceMessage[] decodedMessages2 = PresenceMessage.fromEncodedArray(fixturesArray, null); - for(int index = 0; index < decodedMessages2.length; index++) { - PresenceMessage testInputMsg = testMessages.messages[index]; - if(testInputMsg.data instanceof byte[]) { - assertArrayEquals((byte[]) testInputMsg.data, (byte[]) decodedMessages2[index].data); - } else { - assertEquals(testInputMsg.data, decodedMessages2[index].data); - } - } - } - - static class MessagesData { - public PresenceMessage[] messages; - } + private static final String testMessagesEncodingFile = "ably-common/test-resources/presence-messages-encoding.json"; + private static final String testClientId1 = "testClientId1"; + private static final String testClientId2 = "testClientId2"; + private Auth.TokenDetails token1; + private Auth.TokenDetails token2; + private Auth.TokenDetails wildcardToken; + + private static PresenceMessage contains(PresenceMessage[] messages, String clientId) { + for(PresenceMessage message : messages) + if(clientId.equals(message.clientId)) + return message; + return null; + } + + private PresenceMessage contains(PresenceMessage[] messages, String clientId, PresenceMessage.Action action) { + for(PresenceMessage message : messages) + if(clientId.equals(message.clientId) && action == message.action) + return message; + return null; + } + + private static String random() { + return UUID.randomUUID().toString(); + } + + private class TestChannel { + TestChannel() { + try { + ClientOptions opts = createOptions(testVars.keys[0].keyStr); + rest = new AblyRest(opts); + restChannel = rest.channels.get(channelName); + realtime = new AblyRealtime(opts); + realtimeChannel = realtime.channels.get(channelName); + realtimeChannel.attach(); + (new ChannelWaiter(realtimeChannel)).waitFor(ChannelState.attached); + } catch(AblyException ae) {} + } + + void dispose() { + if(realtime != null) + realtime.close(); + } + + String channelName = random(); + AblyRest rest; + AblyRealtime realtime; + io.ably.lib.rest.Channel restChannel; + io.ably.lib.realtime.Channel realtimeChannel; + } + + @Rule + public Timeout testTimeout = Timeout.seconds(300); + + @Before + public void setUpBefore() throws Exception { + /* create tokens for specific clientIds */ + ClientOptions opts = createOptions(testVars.keys[0].keyStr); + AblyRest rest = new AblyRest(opts); + token1 = rest.auth.requestToken(new TokenParams() {{ clientId = testClientId1; }}, null); + token2 = rest.auth.requestToken(new TokenParams() {{ clientId = testClientId2; }}, null); + wildcardToken = rest.auth.requestToken(new TokenParams() {{ clientId = "*"; }}, null); + } + + /** + * Attach to channel, enter presence channel and await entered event + */ + @Test + public void enter_simple() { + AblyRealtime clientAbly1 = null; + TestChannel testChannel = new TestChannel(); + try { + /* subscribe for presence events in the anonymous connection */ + PresenceWaiter presenceWaiter = new PresenceWaiter(testChannel.realtimeChannel); + /* set up a connection with specific clientId */ + ClientOptions client1Opts = new ClientOptions() {{ + tokenDetails = token1; + clientId = testClientId1; + }}; + fillInOptions(client1Opts); + clientAbly1 = new AblyRealtime(client1Opts); + + /* wait until connected */ + (new ConnectionWaiter(clientAbly1.connection)).waitFor(ConnectionState.connected); + assertEquals("Verify connected state reached", clientAbly1.connection.state, ConnectionState.connected); + + /* get channel and attach */ + Channel client1Channel = clientAbly1.channels.get(testChannel.channelName); + client1Channel.attach(); + (new ChannelWaiter(client1Channel)).waitFor(ChannelState.attached); + assertEquals("Verify attached state reached", client1Channel.state, ChannelState.attached); + + /* let client1 enter the channel and wait for the entered event to be delivered */ + CompletionWaiter enterComplete = new CompletionWaiter(); + String enterString = "Test data (enter_simple)"; + client1Channel.presence.enter(enterString, enterComplete); + presenceWaiter.waitFor(testClientId1, Action.enter); + assertNotNull(presenceWaiter.contains(testClientId1, Action.enter)); + assertEquals(presenceWaiter.receivedMessages.get(0).data, enterString); + + /* verify enter callback called on completion */ + enterComplete.waitFor(); + assertTrue("Verify enter callback called on completion", enterComplete.success); + } catch(AblyException e) { + e.printStackTrace(); + fail("Unexpected exception running test: " + e.getMessage()); + } finally { + if(clientAbly1 != null) + clientAbly1.close(); + if(testChannel != null) + testChannel.dispose(); + } + } + + /** + * Enter presence channel without prior attach and await entered event + */ + @Test + public void enter_before_attach() { + AblyRealtime clientAbly1 = null; + TestChannel testChannel = new TestChannel(); + try { + /* subscribe for presence events in the anonymous connection */ + PresenceWaiter presenceWaiter = new PresenceWaiter(testChannel.realtimeChannel); + /* set up a connection with specific clientId */ + ClientOptions client1Opts = new ClientOptions() {{ + tokenDetails = token1; + clientId = testClientId1; + }}; + fillInOptions(client1Opts); + clientAbly1 = new AblyRealtime(client1Opts); + + /* wait until connected */ + (new ConnectionWaiter(clientAbly1.connection)).waitFor(ConnectionState.connected); + assertEquals("Verify connected state reached", clientAbly1.connection.state, ConnectionState.connected); + + /* get channel */ + Channel client1Channel = clientAbly1.channels.get(testChannel.channelName); + + /* let client1 enter the channel and wait for the entered event to be delivered */ + CompletionWaiter enterComplete = new CompletionWaiter(); + String enterString = "Test data (enter_before_attach)"; + client1Channel.presence.enter(enterString, enterComplete); + presenceWaiter.waitFor(testClientId1, Action.enter); + PresenceMessage expectedPresent = presenceWaiter.contains(testClientId1, clientAbly1.connection.id, Action.enter); + assertNotNull(expectedPresent); + assertEquals(expectedPresent.data, enterString); + + /* verify enter callback called on completion */ + enterComplete.waitFor(); + assertTrue("Verify enter callback called on completion", enterComplete.success); + } catch(AblyException e) { + e.printStackTrace(); + fail("Unexpected exception running test: " + e.getMessage()); + } finally { + if(clientAbly1 != null) + clientAbly1.close(); + if(testChannel != null) + testChannel.dispose(); + } + } + + /** + * Enter presence channel without prior connect and await entered event + */ + @Test + public void enter_before_connect() { + AblyRealtime clientAbly1 = null; + TestChannel testChannel = new TestChannel(); + try { + /* subscribe for presence events in the anonymous connection */ + PresenceWaiter presenceWaiter = new PresenceWaiter(testChannel.realtimeChannel); + /* set up a connection with specific clientId */ + ClientOptions client1Opts = new ClientOptions() {{ + tokenDetails = token1; + clientId = testClientId1; + }}; + fillInOptions(client1Opts); + clientAbly1 = new AblyRealtime(client1Opts); + + /* get channel */ + Channel client1Channel = clientAbly1.channels.get(testChannel.channelName); + + /* let client1 enter the channel and wait for the entered event to be delivered */ + CompletionWaiter enterComplete = new CompletionWaiter(); + String enterString = "Test data (enter_before_connect)"; + client1Channel.presence.enter(enterString, enterComplete); + presenceWaiter.waitFor(testClientId1, Action.enter); + PresenceMessage expectedPresent = presenceWaiter.contains(testClientId1, clientAbly1.connection.id, Action.enter); + assertNotNull(expectedPresent); + assertEquals(expectedPresent.data, enterString); + + /* verify enter callback called on completion */ + enterComplete.waitFor(); + assertTrue("Verify enter callback called on completion", enterComplete.success); + } catch(AblyException e) { + e.printStackTrace(); + fail("Unexpected exception running test: " + e.getMessage()); + } finally { + if(clientAbly1 != null) + clientAbly1.close(); + if(testChannel != null) + testChannel.dispose(); + } + } + + /** + * Enter, then leave, presence channel and await leave event + * Verify that the item is removed from the presence map (RTP2e) + */ + @Test + public void enter_leave_simple() { + AblyRealtime clientAbly1 = null; + TestChannel testChannel = new TestChannel(); + try { + /* subscribe for presence events in the anonymous connection */ + PresenceWaiter presenceWaiter = new PresenceWaiter(testChannel.realtimeChannel); + /* set up a connection with specific clientId */ + ClientOptions client1Opts = new ClientOptions() {{ + tokenDetails = token1; + clientId = testClientId1; + }}; + fillInOptions(client1Opts); + clientAbly1 = new AblyRealtime(client1Opts); + + /* get channel */ + Channel client1Channel = clientAbly1.channels.get(testChannel.channelName); + + /* let client1 enter the channel and wait for the entered event to be delivered */ + CompletionWaiter enterComplete = new CompletionWaiter(); + String enterString = "Test data (enter_before_connect)"; + client1Channel.presence.enter(enterString, enterComplete); + presenceWaiter.waitFor(testClientId1, Action.enter); + assertNotNull(presenceWaiter.contains(testClientId1, Action.enter)); + assertEquals(presenceWaiter.receivedMessages.get(0).data, enterString); + presenceWaiter.reset(); + + /* verify enter callback called on completion */ + enterComplete.waitFor(); + assertTrue("Verify enter callback called on completion", enterComplete.success); + + /* let client1 leave the channel and wait for the leave event to be delivered */ + CompletionWaiter leaveComplete = new CompletionWaiter(); + String leaveString = "Test data (enter_before_connect), leaving"; + client1Channel.presence.leave(leaveString, leaveComplete); + presenceWaiter.waitFor(testClientId1, Action.leave); + PresenceMessage expectedLeft = presenceWaiter.contains(testClientId1, clientAbly1.connection.id, Action.leave); + assertNotNull(expectedLeft); + assertEquals(expectedLeft.data, leaveString); + + /* verify leave callback called on completion */ + leaveComplete.waitFor(); + assertTrue("Verify leave callback called on completion", leaveComplete.success); + + assertEquals("Verify item is removed from the presence map", client1Channel.presence.get(testClientId1, false).length, 0); + + } catch(AblyException e) { + e.printStackTrace(); + fail("Unexpected exception running test: " + e.getMessage()); + } finally { + if(clientAbly1 != null) + clientAbly1.close(); + if(testChannel != null) + testChannel.dispose(); + } + } + + /** + * Enter, then enter again, expecting update event + */ + @Test + public void enter_enter_simple() { + AblyRealtime clientAbly1 = null; + TestChannel testChannel = new TestChannel(); + try { + /* subscribe for presence events in the anonymous connection */ + PresenceWaiter presenceWaiter = new PresenceWaiter(testChannel.realtimeChannel); + /* set up a connection with specific clientId */ + ClientOptions client1Opts = new ClientOptions() {{ + tokenDetails = token1; + clientId = testClientId1; + }}; + fillInOptions(client1Opts); + clientAbly1 = new AblyRealtime(client1Opts); + + /* get channel */ + Channel client1Channel = clientAbly1.channels.get(testChannel.channelName); + + /* let client1 enter the channel and wait for the entered event to be delivered */ + CompletionWaiter enterComplete = new CompletionWaiter(); + String enterString = "Test data (enter_enter_simple)"; + client1Channel.presence.enter(enterString, enterComplete); + presenceWaiter.waitFor(testClientId1, Action.enter); + assertNotNull(presenceWaiter.contains(testClientId1, Action.enter)); + assertEquals(presenceWaiter.receivedMessages.get(0).data, enterString); + presenceWaiter.reset(); + + /* verify enter callback called on completion */ + enterComplete.waitFor(); + assertTrue("Verify enter callback called on completion", enterComplete.success); + + /* let client1 reenter the channel and wait for the update event to be delivered */ + CompletionWaiter reenterComplete = new CompletionWaiter(); + String reenterString = "Test data (enter_enter_simple), reentering"; + client1Channel.presence.enter(reenterString, reenterComplete); + presenceWaiter.waitFor(testClientId1, Action.update); + assertNotNull(presenceWaiter.contains(testClientId1, Action.update)); + assertEquals(presenceWaiter.receivedMessages.get(0).data, reenterString); + + /* verify reenter callback called on completion */ + reenterComplete.waitFor(); + assertTrue("Verify reenter callback called on completion", reenterComplete.success); + + /* let client1 leave the channel and wait for the leave event to be delivered */ + CompletionWaiter leaveComplete = new CompletionWaiter(); + String leaveString = "Test data (enter_enter_simple), leaving"; + client1Channel.presence.leave(leaveString, leaveComplete); + presenceWaiter.waitFor(testClientId1, Action.leave); + PresenceMessage expectedLeft = presenceWaiter.contains(testClientId1, clientAbly1.connection.id, Action.leave); + assertNotNull(expectedLeft); + assertEquals(expectedLeft.data, leaveString); + + /* verify leave callback called on completion */ + leaveComplete.waitFor(); + assertTrue("Verify leave callback called on completion", leaveComplete.success); + } catch(AblyException e) { + e.printStackTrace(); + fail("Unexpected exception running test: " + e.getMessage()); + } finally { + if(clientAbly1 != null) + clientAbly1.close(); + if(testChannel != null) + testChannel.dispose(); + } + } + + /** + * Enter, then update, expecting update event + */ + @Test + public void enter_update_simple() { + AblyRealtime clientAbly1 = null; + TestChannel testChannel = new TestChannel(); + try { + /* subscribe for presence events in the anonymous connection */ + PresenceWaiter presenceWaiter = new PresenceWaiter(testChannel.realtimeChannel); + /* set up a connection with specific clientId */ + ClientOptions client1Opts = new ClientOptions() {{ + tokenDetails = token1; + clientId = testClientId1; + }}; + fillInOptions(client1Opts); + clientAbly1 = new AblyRealtime(client1Opts); + + /* get channel */ + Channel client1Channel = clientAbly1.channels.get(testChannel.channelName); + + /* let client1 enter the channel and wait for the entered event to be delivered */ + CompletionWaiter enterComplete = new CompletionWaiter(); + String enterString = "Test data (enter_update_simple)"; + client1Channel.presence.enter(enterString, enterComplete); + presenceWaiter.waitFor(testClientId1, Action.enter); + assertNotNull(presenceWaiter.contains(testClientId1, Action.enter)); + assertEquals(presenceWaiter.receivedMessages.get(0).data, enterString); + presenceWaiter.reset(); + + /* verify enter callback called on completion */ + enterComplete.waitFor(); + assertTrue("Verify enter callback called on completion", enterComplete.success); + + /* let client1 update the channel and wait for the update event to be delivered */ + CompletionWaiter updateComplete = new CompletionWaiter(); + String reenterString = "Test data (enter_update_simple), updating"; + client1Channel.presence.enter(reenterString, updateComplete); + presenceWaiter.waitFor(testClientId1, Action.update); + assertNotNull(presenceWaiter.contains(testClientId1, Action.update)); + assertEquals(presenceWaiter.receivedMessages.get(0).data, reenterString); + + /* verify reenter callback called on completion */ + updateComplete.waitFor(); + assertTrue("Verify reenter callback called on completion", updateComplete.success); + + /* let client1 leave the channel and wait for the leave event to be delivered */ + CompletionWaiter leaveComplete = new CompletionWaiter(); + String leaveString = "Test data (enter_update_simple), leaving"; + client1Channel.presence.leave(leaveString, leaveComplete); + presenceWaiter.waitFor(testClientId1, Action.leave); + PresenceMessage expectedLeft = presenceWaiter.contains(testClientId1, clientAbly1.connection.id, Action.leave); + assertNotNull(expectedLeft); + assertEquals(expectedLeft.data, leaveString); + + /* verify leave callback called on completion */ + leaveComplete.waitFor(); + assertTrue("Verify leave callback called on completion", leaveComplete.success); + } catch(AblyException e) { + e.printStackTrace(); + fail("Unexpected exception running test: " + e.getMessage()); + } finally { + if(clientAbly1 != null) + clientAbly1.close(); + if(testChannel != null) + testChannel.dispose(); + } + } + + /** + * Enter, then update with null data, expecting previous data to be superseded + */ + @Test + public void enter_update_null() { + AblyRealtime clientAbly1 = null; + TestChannel testChannel = new TestChannel(); + try { + /* subscribe for presence events in the anonymous connection */ + PresenceWaiter presenceWaiter = new PresenceWaiter(testChannel.realtimeChannel); + /* set up a connection with specific clientId */ + ClientOptions client1Opts = new ClientOptions() {{ + tokenDetails = token1; + clientId = testClientId1; + }}; + fillInOptions(client1Opts); + client1Opts.useBinaryProtocol = true; + clientAbly1 = new AblyRealtime(client1Opts); + + /* get channel */ + Channel client1Channel = clientAbly1.channels.get(testChannel.channelName); + + /* let client1 enter the channel and wait for the entered event to be delivered */ + CompletionWaiter enterComplete = new CompletionWaiter(); + String enterString = "Test data (enter_update_null)"; + client1Channel.presence.enter(enterString, enterComplete); + presenceWaiter.waitFor(testClientId1, Action.enter); + assertNotNull(presenceWaiter.contains(testClientId1, Action.enter)); + assertEquals(presenceWaiter.receivedMessages.get(0).data, enterString); + presenceWaiter.reset(); + + /* verify enter callback called on completion */ + enterComplete.waitFor(); + assertTrue("Verify enter callback called on completion", enterComplete.success); + + /* let client1 update the channel and wait for the update event to be delivered */ + CompletionWaiter updateComplete = new CompletionWaiter(); + String updateString = null; + client1Channel.presence.enter(updateString, updateComplete); + presenceWaiter.waitFor(testClientId1, Action.update); + assertNotNull(presenceWaiter.contains(testClientId1, Action.update)); + assertEquals(presenceWaiter.receivedMessages.get(0).data, updateString); + + /* verify reenter callback called on completion */ + updateComplete.waitFor(); + assertTrue("Verify reenter callback called on completion", updateComplete.success); + + /* let client1 leave the channel and wait for the leave event to be delivered */ + CompletionWaiter leaveComplete = new CompletionWaiter(); + String leaveString = "Test data (enter_update_null), leaving"; + client1Channel.presence.leave(leaveString, leaveComplete); + presenceWaiter.waitFor(testClientId1, Action.leave); + PresenceMessage expectedLeft = presenceWaiter.contains(testClientId1, clientAbly1.connection.id, Action.leave); + assertNotNull(expectedLeft); + assertEquals(expectedLeft.data, leaveString); + + /* verify leave callback called on completion */ + leaveComplete.waitFor(); + assertTrue("Verify leave callback called on completion", leaveComplete.success); + } catch(AblyException e) { + e.printStackTrace(); + fail("Unexpected exception running test: " + e.getMessage()); + } finally { + if(clientAbly1 != null) + clientAbly1.close(); + if(testChannel != null) + testChannel.dispose(); + } + } + + /** + * Update without having first entered, expecting enter event + */ + @Test + public void update_noenter() { + AblyRealtime clientAbly1 = null; + TestChannel testChannel = new TestChannel(); + try { + /* subscribe for presence events in the anonymous connection */ + PresenceWaiter presenceWaiter = new PresenceWaiter(testChannel.realtimeChannel); + /* set up a connection with specific clientId */ + ClientOptions client1Opts = new ClientOptions() {{ + tokenDetails = token1; + clientId = testClientId1; + }}; + fillInOptions(client1Opts); + clientAbly1 = new AblyRealtime(client1Opts); + + /* get channel */ + Channel client1Channel = clientAbly1.channels.get(testChannel.channelName); + + /* let client1 enter the channel and wait for the entered event to be delivered */ + CompletionWaiter enterComplete = new CompletionWaiter(); + String updateString = "Test data (update_noenter)"; + client1Channel.presence.update(updateString, enterComplete); + presenceWaiter.waitFor(testClientId1, Action.enter); + assertNotNull(presenceWaiter.contains(testClientId1, Action.enter)); + assertEquals(presenceWaiter.receivedMessages.get(0).data, updateString); + presenceWaiter.reset(); + + /* verify enter callback called on completion */ + enterComplete.waitFor(); + assertTrue("Verify enter callback called on completion", enterComplete.success); + + /* let client1 leave the channel and wait for the leave event to be delivered */ + CompletionWaiter leaveComplete = new CompletionWaiter(); + String leaveString = "Test data (update_noenter), leaving"; + client1Channel.presence.leave(leaveString, leaveComplete); + presenceWaiter.waitFor(testClientId1, Action.leave); + PresenceMessage expectedLeft = presenceWaiter.contains(testClientId1, clientAbly1.connection.id, Action.leave); + assertNotNull(expectedLeft); + assertEquals(expectedLeft.data, leaveString); + + /* verify leave callback called on completion */ + leaveComplete.waitFor(); + assertTrue("Verify leave callback called on completion", leaveComplete.success); + } catch(AblyException e) { + e.printStackTrace(); + fail("Unexpected exception running test: " + e.getMessage()); + } finally { + if(clientAbly1 != null) + clientAbly1.close(); + if(testChannel != null) + testChannel.dispose(); + } + } + + /** + * Enter, then leave (with no data) and await leave event, + * expecting enter data to be in leave event + */ + @Test + public void enter_leave_nodata() { + AblyRealtime clientAbly1 = null; + TestChannel testChannel = new TestChannel(); + try { + /* subscribe for presence events in the anonymous connection */ + PresenceWaiter presenceWaiter = new PresenceWaiter(testChannel.realtimeChannel); + /* set up a connection with specific clientId */ + ClientOptions client1Opts = new ClientOptions() {{ + tokenDetails = token1; + clientId = testClientId1; + }}; + fillInOptions(client1Opts); + clientAbly1 = new AblyRealtime(client1Opts); + + /* get channel */ + Channel client1Channel = clientAbly1.channels.get(testChannel.channelName); + + /* let client1 enter the channel and wait for the entered event to be delivered */ + CompletionWaiter enterComplete = new CompletionWaiter(); + String enterString = "Test data (enter_leave_nodata)"; + client1Channel.presence.enter(enterString, enterComplete); + presenceWaiter.waitFor(testClientId1, Action.enter); + assertNotNull(presenceWaiter.contains(testClientId1, Action.enter)); + assertEquals(presenceWaiter.receivedMessages.get(0).data, enterString); + presenceWaiter.reset(); + + /* verify enter callback called on completion */ + enterComplete.waitFor(); + assertTrue("Verify enter callback called on completion", enterComplete.success); + + /* let client1 leave the channel and wait for the leave event to be delivered */ + CompletionWaiter leaveComplete = new CompletionWaiter(); + client1Channel.presence.leave(leaveComplete); + presenceWaiter.waitFor(testClientId1, Action.leave); + assertNotNull(presenceWaiter.contains(testClientId1, Action.leave)); + assertEquals(presenceWaiter.receivedMessages.get(0).data, enterString); + + /* verify leave callback called on completion */ + leaveComplete.waitFor(); + assertTrue("Verify leave callback called on completion", leaveComplete.success); + + } catch(AblyException e) { + e.printStackTrace(); + } finally { + if(clientAbly1 != null) + clientAbly1.close(); + if(testChannel != null) + testChannel.dispose(); + } + } + + /** + * Attach to channel, enter presence channel and get presence using realtime get() + */ + @Test + public void realtime_get_simple() { + AblyRealtime clientAbly1 = null; + TestChannel testChannel = new TestChannel(); + try { + /* subscribe for presence events in the anonymous connection */ + PresenceWaiter presenceWaiter = new PresenceWaiter(testChannel.realtimeChannel); + + /* set up a connection with specific clientId */ + ClientOptions client1Opts = new ClientOptions() {{ + tokenDetails = token1; + clientId = testClientId1; + }}; + fillInOptions(client1Opts); + clientAbly1 = new AblyRealtime(client1Opts); + + /* wait until connected */ + (new ConnectionWaiter(clientAbly1.connection)).waitFor(ConnectionState.connected); + assertEquals("Verify connected state reached", clientAbly1.connection.state, ConnectionState.connected); + + /* get channel and attach */ + Channel client1Channel = clientAbly1.channels.get(testChannel.channelName); + client1Channel.attach(); + (new ChannelWaiter(client1Channel)).waitFor(ChannelState.attached); + assertEquals("Verify attached state reached", client1Channel.state, ChannelState.attached); + + /* let client1 enter the channel and wait for the success callback */ + CompletionWaiter enterComplete = new CompletionWaiter(); + String enterString = "Test data (get_simple)"; + client1Channel.presence.enter(enterString, enterComplete); + enterComplete.waitFor(); + assertTrue("Verify enter callback called on completion", enterComplete.success); + + /* get presence set and verify client present */ + presenceWaiter.waitFor(testClientId1); + PresenceMessage[] presences = testChannel.realtimeChannel.presence.get(false); + PresenceMessage expectedPresent = contains(presences, testClientId1, Action.present); + assertNotNull("Verify expected client is in presence set", expectedPresent); + assertEquals(expectedPresent.data, enterString); + + } catch(AblyException e) { + e.printStackTrace(); + fail("Unexpected exception running test: " + e.getMessage()); + } finally { + if(clientAbly1 != null) + clientAbly1.close(); + if(testChannel != null) + testChannel.dispose(); + } + } + + /** + * Attach to channel, enter+leave presence channel and get presence with realtime get() + */ + @Test + public void realtime_get_leave() { + AblyRealtime clientAbly1 = null; + TestChannel testChannel = new TestChannel(); + try { + /* subscribe for presence events in the anonymous connection */ + PresenceWaiter presenceWaiter = new PresenceWaiter(testChannel.realtimeChannel); + /* set up a connection with specific clientId */ + ClientOptions client1Opts = new ClientOptions() {{ + tokenDetails = token1; + clientId = testClientId1; + }}; + fillInOptions(client1Opts); + clientAbly1 = new AblyRealtime(client1Opts); + + /* wait until connected */ + (new ConnectionWaiter(clientAbly1.connection)).waitFor(ConnectionState.connected); + assertEquals("Verify connected state reached", clientAbly1.connection.state, ConnectionState.connected); + + /* get channel and attach */ + Channel client1Channel = clientAbly1.channels.get(testChannel.channelName); + client1Channel.attach(); + (new ChannelWaiter(client1Channel)).waitFor(ChannelState.attached); + assertEquals("Verify attached state reached", client1Channel.state, ChannelState.attached); + + /* let client1 enter the channel and wait for the success callback */ + CompletionWaiter enterComplete = new CompletionWaiter(); + client1Channel.presence.enter("Test data (get_leave)", enterComplete); + enterComplete.waitFor(); + assertTrue("Verify enter callback called on completion", enterComplete.success); + + /* let client1 leave the channel; wait for the success callback and event */ + CompletionWaiter leaveComplete = new CompletionWaiter(); + client1Channel.presence.leave(leaveComplete); + leaveComplete.waitFor(); + assertTrue("Verify leave callback called on completion", leaveComplete.success); + presenceWaiter.waitFor(testClientId1, Action.leave); + assertTrue("Verify leave callback called on completion", leaveComplete.success); + + /* get presence set and verify client absent */ + PresenceMessage[] presences = testChannel.realtimeChannel.presence.get(false); + assertNull("Verify expected client is in presence set", contains(presences, testClientId1)); + + } catch(AblyException e) { + e.printStackTrace(); + fail("Unexpected exception running test: " + e.getMessage()); + } finally { + if(clientAbly1 != null) + clientAbly1.close(); + if(testChannel != null) + testChannel.dispose(); + } + } + + /** + * Attach to channel, enter presence channel, then initiate second + * connection, seeing existing member in message subsequent to second attach response + */ + @Test + public void attach_enter_simple() { + AblyRealtime clientAbly1 = null; + AblyRealtime clientAbly2 = null; + TestChannel testChannel = new TestChannel(); + try { + /* subscribe for presence events in the anonymous connection */ + new PresenceWaiter(testChannel.realtimeChannel); + /* set up a connection with specific clientId */ + ClientOptions client1Opts = new ClientOptions() {{ + tokenDetails = token1; + clientId = testClientId1; + }}; + fillInOptions(client1Opts); + clientAbly1 = new AblyRealtime(client1Opts); + + /* wait until connected */ + (new ConnectionWaiter(clientAbly1.connection)).waitFor(ConnectionState.connected); + assertEquals("Verify connected state reached", clientAbly1.connection.state, ConnectionState.connected); + + /* get channel and attach */ + Channel client1Channel = clientAbly1.channels.get(testChannel.channelName); + client1Channel.attach(); + (new ChannelWaiter(client1Channel)).waitFor(ChannelState.attached); + assertEquals("Verify attached state reached", client1Channel.state, ChannelState.attached); + + /* let client1 enter the channel and wait for the success callback */ + CompletionWaiter enterComplete = new CompletionWaiter(); + String enterString = "Test data (attach_enter)"; + client1Channel.presence.enter(enterString, enterComplete); + enterComplete.waitFor(); + assertTrue("Verify enter callback called on completion", enterComplete.success); + + /* set up a second connection with different clientId */ + ClientOptions client2Opts = new ClientOptions() {{ + tokenDetails = token2; + clientId = testClientId2; + }}; + fillInOptions(client2Opts); + clientAbly2 = new AblyRealtime(client2Opts); + + /* wait until connected */ + (new ConnectionWaiter(clientAbly2.connection)).waitFor(ConnectionState.connected); + assertEquals("Verify connected state reached", clientAbly2.connection.state, ConnectionState.connected); + + /* get channel and subscribe to presence */ + Channel client2Channel = clientAbly2.channels.get(testChannel.channelName); + PresenceWaiter client2Waiter = new PresenceWaiter(client2Channel); + client2Waiter.waitFor(testClientId1, Action.present); + + /* get presence set and verify client present */ + PresenceMessage[] presences = client2Channel.presence.get(false); + PresenceMessage expectedPresent = contains(presences, testClientId1, Action.present); + assertNotNull("Verify expected client is in presence set", expectedPresent); + assertEquals(expectedPresent.data, enterString); + + } catch(AblyException e) { + e.printStackTrace(); + fail("Unexpected exception running test: " + e.getMessage()); + } finally { + if(clientAbly1 != null) + clientAbly1.close(); + if(clientAbly2 != null) + clientAbly2.close(); + if(testChannel != null) + testChannel.dispose(); + } + } + + /** + * Attach to channel, enter presence channel with large number of clientIds, + * then initiate second connection, seeing existing members in sync subsequent + * to second attach response + * + * Test RTP4 + */ + @Test + public void attach_enter_multiple() { + AblyRealtime clientAbly1 = null; + AblyRealtime clientAbly2 = null; + TestChannel testChannel = new TestChannel(); + int clientCount = 250; + try { + /* subscribe for presence events in the anonymous connection */ + new PresenceWaiter(testChannel.realtimeChannel); + /* set up a connection with specific clientId */ + ClientOptions client1Opts = new ClientOptions() {{ + tokenDetails = wildcardToken; + }}; + fillInOptions(client1Opts); + clientAbly1 = new AblyRealtime(client1Opts); + + /* wait until connected */ + (new ConnectionWaiter(clientAbly1.connection)).waitFor(ConnectionState.connected); + assertEquals("Verify connected state reached", clientAbly1.connection.state, ConnectionState.connected); + + /* get channel and attach */ + Channel client1Channel = clientAbly1.channels.get(testChannel.channelName); + client1Channel.attach(); + (new ChannelWaiter(client1Channel)).waitFor(ChannelState.attached); + assertEquals("Verify attached state reached", client1Channel.state, ChannelState.attached); + + /* let client1 enter the channel for multiple clients and wait for the success callback */ + CompletionSet enterComplete = new CompletionSet(); + for(int i = 0; i < clientCount; i++) { + client1Channel.presence.enterClient("client" + i, "Test data (attach_enter_multiple) " + i, enterComplete.add()); + } + enterComplete.waitFor(); + assertTrue("Verify enter callback called on completion", enterComplete.pending.isEmpty()); + assertTrue("Verify no enter errors", enterComplete.errors.isEmpty()); + + /* set up a second connection with different clientId */ + ClientOptions client2Opts = new ClientOptions() {{ + tokenDetails = token2; + clientId = testClientId2; + }}; + fillInOptions(client2Opts); + clientAbly2 = new AblyRealtime(client2Opts); + + /* wait until connected */ + (new ConnectionWaiter(clientAbly2.connection)).waitFor(ConnectionState.connected); + assertEquals("Verify connected state reached", clientAbly2.connection.state, ConnectionState.connected); + + /* get channel */ + Channel client2Channel = clientAbly2.channels.get(testChannel.channelName); + client2Channel.attach(); + (new ChannelWaiter(client2Channel)).waitFor(ChannelState.attached); + assertEquals("Verify attached state reached", client2Channel.state, ChannelState.attached); + + /* get presence set and verify client present */ + HashMap memberIndex = new HashMap(); + PresenceMessage[] members = client2Channel.presence.get(true); + assertNotNull("Expected non-null messages", members); + assertEquals("Expected " + clientCount + " messages", members.length, clientCount); + + /* index received messages */ + for(PresenceMessage member: members) + memberIndex.put(member.clientId, member); + + /* verify that all clientIds were received */ + assertEquals("Expected " + clientCount + " members", memberIndex.size(), clientCount); + for(int i = 0; i < clientCount; i++) { + String clientId = "client" + i; + assertTrue("Expected client with id " + clientId, memberIndex.containsKey(clientId)); + } + } catch(AblyException e) { + e.printStackTrace(); + fail("Unexpected exception running test: " + e.getMessage()); + } finally { + if(clientAbly1 != null) + clientAbly1.close(); + if(clientAbly2 != null) + clientAbly2.close(); + testChannel.dispose(); + } + } + + /** + * Attach and enter channel on two connections, seeing + * both members in presence returned by realtime get() */ + @Test + public void realtime_enter_multiple() { + AblyRealtime clientAbly1 = null; + AblyRealtime clientAbly2 = null; + TestChannel testChannel = new TestChannel(); + try { + /* subscribe for presence events in the anonymous connection */ + PresenceWaiter waiter = new PresenceWaiter(testChannel.realtimeChannel); + + /* set up a connection with specific clientId */ + ClientOptions client1Opts = new ClientOptions() {{ + tokenDetails = token1; + clientId = testClientId1; + }}; + fillInOptions(client1Opts); + clientAbly1 = new AblyRealtime(client1Opts); + + /* get channel and attach */ + Channel client1Channel = clientAbly1.channels.get(testChannel.channelName); + CompletionWaiter enter1Complete = new CompletionWaiter(); + String enterString1 = "Test data (enter_multiple, clientId1)"; + client1Channel.presence.enter(enterString1, enter1Complete); + enter1Complete.waitFor(); + assertTrue("Verify enter callback called on completion", enter1Complete.success); + + /* set up a second connection with different clientId */ + ClientOptions client2Opts = new ClientOptions() {{ + tokenDetails = token2; + clientId = testClientId2; + }}; + fillInOptions(client2Opts); + clientAbly2 = new AblyRealtime(client2Opts); + + /* get channel and subscribe to presence */ + Channel client2Channel = clientAbly2.channels.get(testChannel.channelName); + CompletionWaiter enter2Complete = new CompletionWaiter(); + String enterString2 = "Test data (enter_multiple, clientId2)"; + client2Channel.presence.enter(enterString2, enter2Complete); + enter2Complete.waitFor(); + assertTrue("Verify enter callback called on completion", enter2Complete.success); + + /* verify enter events for both clients are received */ + waiter.waitFor(testClientId1, Action.enter); + waiter.waitFor(testClientId2, Action.enter); + + /* get presence set and verify clients present */ + PresenceMessage[] presences = testChannel.realtimeChannel.presence.get(false); + PresenceMessage expectedPresent1 = contains(presences, testClientId1, Action.present); + PresenceMessage expectedPresent2 = contains(presences, testClientId2, Action.present); + assertNotNull("Verify expected clients are in presence set", expectedPresent1); + assertNotNull("Verify expected clients are in presence set", expectedPresent2); + assertEquals(expectedPresent1.data, enterString1); + assertEquals(expectedPresent2.data, enterString2); + + } catch(AblyException e) { + e.printStackTrace(); + fail("Unexpected exception running test: " + e.getMessage()); + } finally { + if(clientAbly1 != null) + clientAbly1.close(); + if(clientAbly2 != null) + clientAbly2.close(); + if(testChannel != null) + testChannel.dispose(); + } + } + + /** + * Attach to channel, enter presence channel and get presence using rest get() + */ + @Test + public void rest_get_simple() { + AblyRealtime clientAbly1 = null; + TestChannel testChannel = new TestChannel(); + try { + /* subscribe for presence events in the anonymous connection */ + new PresenceWaiter(testChannel.realtimeChannel); + /* set up a connection with specific clientId */ + ClientOptions client1Opts = new ClientOptions() {{ + tokenDetails = token1; + clientId = testClientId1; + }}; + fillInOptions(client1Opts); + clientAbly1 = new AblyRealtime(client1Opts); + + /* wait until connected */ + (new ConnectionWaiter(clientAbly1.connection)).waitFor(ConnectionState.connected); + assertEquals("Verify connected state reached", clientAbly1.connection.state, ConnectionState.connected); + + /* get channel and attach */ + Channel client1Channel = clientAbly1.channels.get(testChannel.channelName); + client1Channel.attach(); + (new ChannelWaiter(client1Channel)).waitFor(ChannelState.attached); + assertEquals("Verify attached state reached", client1Channel.state, ChannelState.attached); + + /* let client1 enter the channel and wait for the success callback */ + CompletionWaiter enterComplete = new CompletionWaiter(); + String enterString = "Test data (get_simple)"; + client1Channel.presence.enter(enterString, enterComplete); + enterComplete.waitFor(); + assertTrue("Verify enter callback called on completion", enterComplete.success); + + /* get presence set and verify client present */ + PresenceMessage[] presences = testChannel.restChannel.presence.get(null).items(); + PresenceMessage expectedPresent = contains(presences, testClientId1, Action.present); + assertNotNull("Verify expected client is in presence set", expectedPresent); + assertEquals(expectedPresent.data, enterString); + + } catch(AblyException e) { + e.printStackTrace(); + fail("Unexpected exception running test: " + e.getMessage()); + } finally { + if(clientAbly1 != null) + clientAbly1.close(); + if(testChannel != null) + testChannel.dispose(); + } + } + + /** + * Attach to channel, enter+leave presence channel and get presence with rest get() + */ + @Test + public void rest_get_leave() { + AblyRealtime clientAbly1 = null; + TestChannel testChannel = new TestChannel(); + try { + /* subscribe for presence events in the anonymous connection */ + PresenceWaiter presenceWaiter = new PresenceWaiter(testChannel.realtimeChannel); + /* set up a connection with specific clientId */ + ClientOptions client1Opts = new ClientOptions() {{ + tokenDetails = token1; + clientId = testClientId1; + }}; + fillInOptions(client1Opts); + clientAbly1 = new AblyRealtime(client1Opts); + + /* wait until connected */ + (new ConnectionWaiter(clientAbly1.connection)).waitFor(ConnectionState.connected); + assertEquals("Verify connected state reached", clientAbly1.connection.state, ConnectionState.connected); + + /* get channel and attach */ + Channel client1Channel = clientAbly1.channels.get(testChannel.channelName); + client1Channel.attach(); + (new ChannelWaiter(client1Channel)).waitFor(ChannelState.attached); + assertEquals("Verify attached state reached", client1Channel.state, ChannelState.attached); + + /* let client1 enter the channel and wait for the success callback */ + CompletionWaiter enterComplete = new CompletionWaiter(); + client1Channel.presence.enter("Test data (get_leave)", enterComplete); + enterComplete.waitFor(); + assertTrue("Verify enter callback called on completion", enterComplete.success); + + /* let client1 leave the channel; wait for the success callback and event */ + CompletionWaiter leaveComplete = new CompletionWaiter(); + client1Channel.presence.leave(leaveComplete); + leaveComplete.waitFor(); + assertTrue("Verify leave callback called on completion", leaveComplete.success); + presenceWaiter.waitFor(testClientId1, Action.leave); + assertTrue("Verify leave callback called on completion", leaveComplete.success); + + /* get presence set and verify client absent */ + PresenceMessage[] presences = testChannel.restChannel.presence.get(null).items(); + assertNull("Verify expected client is in presence set", contains(presences, testClientId1)); + + } catch(AblyException e) { + e.printStackTrace(); + fail("Unexpected exception running test: " + e.getMessage()); + } finally { + if(clientAbly1 != null) + clientAbly1.close(); + if(testChannel != null) + testChannel.dispose(); + } + } + + /** + * Attach and enter channel on two connections, seeing + * both members in presence returned by rest get() */ + @Test + public void rest_enter_multiple() { + AblyRealtime clientAbly1 = null; + AblyRealtime clientAbly2 = null; + TestChannel testChannel = new TestChannel(); + try { + /* subscribe for presence events in the anonymous connection */ + new PresenceWaiter(testChannel.realtimeChannel); + /* set up a connection with specific clientId */ + ClientOptions client1Opts = new ClientOptions() {{ + tokenDetails = token1; + clientId = testClientId1; + }}; + fillInOptions(client1Opts); + clientAbly1 = new AblyRealtime(client1Opts); + + /* get channel and attach */ + Channel client1Channel = clientAbly1.channels.get(testChannel.channelName); + CompletionWaiter enter1Complete = new CompletionWaiter(); + String enterString1 = "Test data (enter_multiple, clientId1)"; + client1Channel.presence.enter(enterString1, enter1Complete); + enter1Complete.waitFor(); + assertTrue("Verify enter callback called on completion", enter1Complete.success); + + /* set up a second connection with different clientId */ + ClientOptions client2Opts = new ClientOptions() {{ + tokenDetails = token2; + clientId = testClientId2; + }}; + fillInOptions(client2Opts); + clientAbly2 = new AblyRealtime(client2Opts); + + /* get channel and subscribe to presence */ + Channel client2Channel = clientAbly2.channels.get(testChannel.channelName); + CompletionWaiter enter2Complete = new CompletionWaiter(); + String enterString2 = "Test data (enter_multiple, clientId2)"; + client2Channel.presence.enter(enterString2, enter2Complete); + enter2Complete.waitFor(); + assertTrue("Verify enter callback called on completion", enter2Complete.success); + + /* get presence set and verify client present */ + PresenceMessage[] presences = testChannel.restChannel.presence.get(null).items(); + PresenceMessage expectedPresent1 = contains(presences, testClientId1, Action.present); + PresenceMessage expectedPresent2 = contains(presences, testClientId2, Action.present); + assertNotNull("Verify expected clients are in presence set", expectedPresent1); + assertNotNull("Verify expected clients are in presence set", expectedPresent2); + assertEquals(expectedPresent1.data, enterString1); + assertEquals(expectedPresent2.data, enterString2); + + } catch(AblyException e) { + e.printStackTrace(); + fail("Unexpected exception running test: " + e.getMessage()); + } finally { + if(clientAbly1 != null) + clientAbly1.close(); + if(clientAbly2 != null) + clientAbly2.close(); + if(testChannel != null) + testChannel.dispose(); + } + } + + /** + * Attach and enter channel multiple times on a single connection, + * retrieving members using paginated rest get() */ + @Test + public void rest_paginated_get() { + AblyRealtime clientAbly1 = null; + TestChannel testChannel = new TestChannel(); + int clientCount = 30; + long delay = 100L; + try { + /* subscribe for presence events in the anonymous connection */ + new PresenceWaiter(testChannel.realtimeChannel); + /* set up a connection with specific clientId */ + ClientOptions client1Opts = new ClientOptions() {{ + tokenDetails = wildcardToken; + }}; + fillInOptions(client1Opts); + clientAbly1 = new AblyRealtime(client1Opts); + + /* get channel and attach */ + Channel client1Channel = clientAbly1.channels.get(testChannel.channelName); + + /* enter multiple clients */ + CompletionSet enterComplete = new CompletionSet(); + for(int i = 0; i < clientCount; i++) { + client1Channel.presence.enterClient("client" + i, "Test data (rest_paginated_get) " + i, enterComplete.add()); + try { Thread.sleep(delay); } catch(InterruptedException e){} + } + enterComplete.waitFor(); + assertTrue("Verify enter callback called on completion", enterComplete.errors.isEmpty()); + + /* get the presence for this channel */ + HashMap memberIndex = new HashMap(); + PaginatedResult members = testChannel.restChannel.presence.get(new Param[] { new Param("limit", "10") }); + assertNotNull("Expected non-null messages", members); + assertEquals("Expected 10 messages", members.items().length, 10); + + /* index received messages */ + for(int i = 0; i < 10; i++) { + PresenceMessage member = members.items()[i]; + memberIndex.put(member.clientId, member); + } + + /* get next page */ + members = members.next(); + assertNotNull("Expected non-null messages", members); + assertEquals("Expected 10 messages", members.items().length, 10); + + /* index received messages */ + for(int i = 0; i < 10; i++) { + PresenceMessage member = members.items()[i]; + memberIndex.put(member.clientId, member); + } + + /* get next page */ + members = members.next(); + assertNotNull("Expected non-null messages", members); + assertEquals("Expected 10 messages", members.items().length, 10); + + /* index received messages */ + for(int i = 0; i < 10; i++) { + PresenceMessage member = members.items()[i]; + memberIndex.put(member.clientId, member); + } + + /* verify there is no next page */ + assertFalse("Expected null next page", members.hasNext()); + + /* verify that all clientIds were received */ + assertEquals("Expected " + clientCount + " members", memberIndex.size(), clientCount); + for(int i = 0; i < clientCount; i++) { + String clientId = "client" + i; + assertTrue("Expected client with id " + clientId, memberIndex.containsKey(clientId)); + } + } catch(AblyException e) { + e.printStackTrace(); + fail("Unexpected exception running test: " + e.getMessage()); + } finally { + if(clientAbly1 != null) + clientAbly1.close(); + if(testChannel != null) + testChannel.dispose(); + } + } + + /** + * Attach to channel, enter presence channel, disconnect and await leave event + */ + @Test + public void disconnect_leave() { + AblyRealtime clientAbly1 = null; + TestChannel testChannel = new TestChannel(); + boolean requiresClose = false; + try { + /* subscribe for presence events in the anonymous connection */ + PresenceWaiter presenceWaiter = new PresenceWaiter(testChannel.realtimeChannel); + /* set up a connection with specific clientId */ + ClientOptions client1Opts = new ClientOptions() {{ + tokenDetails = token1; + clientId = testClientId1; + }}; + fillInOptions(client1Opts); + clientAbly1 = new AblyRealtime(client1Opts); + requiresClose = true; + + /* get channel */ + Channel client1Channel = clientAbly1.channels.get(testChannel.channelName); + + /* let client1 enter the channel and wait for the entered event to be delivered */ + CompletionWaiter enterComplete = new CompletionWaiter(); + String enterString = "Test data (disconnect_leave)"; + client1Channel.presence.enter(enterString, enterComplete); + presenceWaiter.waitFor(testClientId1, Action.enter); + PresenceMessage expectedPresent = presenceWaiter.contains(testClientId1, Action.enter); + assertNotNull(expectedPresent); + assertEquals(expectedPresent.data, enterString); + + /* verify enter callback called on completion */ + enterComplete.waitFor(); + assertTrue("Verify enter callback called on completion", enterComplete.success); + + /* close client1 connection and wait for the leave event to be delivered */ + clientAbly1.close(); + requiresClose = false; + presenceWaiter.waitFor(testClientId1, Action.leave); + PresenceMessage expectedLeft = presenceWaiter.contains(testClientId1, Action.leave); + assertNotNull(expectedLeft); + /* verify leave message contains data that was published with enter */ + assertEquals(expectedLeft.data, enterString); + + } catch(AblyException e) { + e.printStackTrace(); + fail("Unexpected exception running test: " + e.getMessage()); + } finally { + if(requiresClose) + clientAbly1.close(); + if(testChannel != null) + testChannel.dispose(); + } + } + + /** + *

+ * Validates channel removes all subscribers, + * when {@code Channel#unsubscribe()} with no argument gets called. + *

+ * + * Tests RTP7a + * + * @throws AblyException + */ + @Test + public void realtime_presence_unsubscribe_all() throws AblyException { + /* Ably instance that will emit presence events */ + AblyRealtime ably1 = null; + /* Ably instance that will receive presence events */ + AblyRealtime ably2 = null; + + String channelName = "test.presence.unsubscribe.all" + System.currentTimeMillis(); + + try { + ClientOptions option1 = createOptions(testVars.keys[0].keyStr); + option1.clientId = "emitter client"; + ClientOptions option2 = createOptions(testVars.keys[0].keyStr); + option2.clientId = "receiver client"; + + ably1 = new AblyRealtime(option1); + ably2 = new AblyRealtime(option2); + + Channel channel1 = ably1.channels.get(channelName); + channel1.attach(); + (new ChannelWaiter(channel1)).waitFor(ChannelState.attached); + + Channel channel2 = ably2.channels.get(channelName); + channel2.attach(); + (new ChannelWaiter(channel2)).waitFor(ChannelState.attached); + + ArrayList receivedMessageStack = new ArrayList<>(); + Presence.PresenceListener listener = new Presence.PresenceListener() { + List messageStack; + + @Override + public void onPresenceMessage(PresenceMessage message) { + messageStack.add(message); + } + + public Presence.PresenceListener setMessageStack(List messageStack) { + this.messageStack = messageStack; + return this; + } + }.setMessageStack(receivedMessageStack); + + /* Subscribe using various alternatives of {@code Presence#subscribe()} */ + channel2.presence.subscribe(listener); + channel2.presence.subscribe(Action.present, listener); + channel2.presence.subscribe(EnumSet.of(Action.update, Action.leave), listener); + + /* Unsubscribe */ + channel2.presence.unsubscribe(); + + /* Start emitting channel with ably client 1 (emitter) */ + channel1.presence.enter("Hello, #2!", null); + channel1.presence.update("Lorem ipsum", null); + channel1.presence.update("Dolor sit!", null); + channel1.presence.leave(null); + + /* Wait until receiver client (ably2) observes {@code Action.leave} + * is emitted from emitter client (ably1) + */ + Helpers.PresenceWaiter leavePresenceWaiter = new Helpers.PresenceWaiter(channel2); + leavePresenceWaiter.waitFor(ably1.options.clientId, Action.leave); + + /* Validate that we didn't received anything + */ + assertThat(receivedMessageStack, is(emptyCollectionOf(PresenceMessage.class))); + } finally { + if (ably1 != null) ably1.close(); + if (ably2 != null) ably2.close(); + } + } + + /** + *

+ * Validates channel removes a subscriber, + * when {@code Channel#unsubscribe()} gets called with a listener. + *

+ * + * @throws AblyException + */ + @Test + public void realtime_presence_unsubscribe_single() throws AblyException { + /* Ably instance that will emit presence events */ + AblyRealtime ably1 = null; + /* Ably instance that will receive presence events */ + AblyRealtime ably2 = null; + + String channelName = "test.presence.unsubscribe.single" + System.currentTimeMillis(); + + try { + ClientOptions option1 = createOptions(testVars.keys[0].keyStr); + option1.clientId = "emitter client"; + ClientOptions option2 = createOptions(testVars.keys[0].keyStr); + option2.clientId = "receiver client"; + + ably1 = new AblyRealtime(option1); + ably2 = new AblyRealtime(option2); + + Channel channel1 = ably1.channels.get(channelName); + channel1.attach(); + (new ChannelWaiter(channel1)).waitFor(ChannelState.attached); + + Channel channel2 = ably2.channels.get(channelName); + channel2.attach(); + (new ChannelWaiter(channel2)).waitFor(ChannelState.attached); + + ArrayList receivedMessageStack = new ArrayList<>(); + Presence.PresenceListener listener = new Presence.PresenceListener() { + List messageStack; + + @Override + public void onPresenceMessage(PresenceMessage message) { + messageStack.add(message); + } + + public Presence.PresenceListener setMessageStack(List messageStack) { + this.messageStack = messageStack; + return this; + } + }.setMessageStack(receivedMessageStack); + + /* Subscribe using various alternatives of {@code Presence#subscribe()} */ + channel2.presence.subscribe(listener); + channel2.presence.subscribe(Action.present, listener); + channel2.presence.subscribe(EnumSet.of(Action.update, Action.leave), listener); + + /* Unsubscribe */ + channel2.presence.unsubscribe(listener); + + /* Start emitting channel with ably client 1 (emitter) */ + channel1.presence.enter("Hello, #2!", null); + channel1.presence.update("Lorem ipsum", null); + channel1.presence.update("Dolor sit!", null); + channel1.presence.leave(null); + + /* Wait until receiver client (ably2) observes {@code Action.leave} + * is emitted from emitter client (ably1) + */ + Helpers.PresenceWaiter leavePresenceWaiter = new Helpers.PresenceWaiter(channel2); + leavePresenceWaiter.waitFor(ably1.options.clientId, Action.leave); + + /* Validate that we didn't received anything + */ + assertThat(receivedMessageStack, is(emptyCollectionOf(PresenceMessage.class))); + } finally { + if (ably1 != null) ably1.close(); + if (ably2 != null) ably2.close(); + } + } + + /** + *

+ * Validates a client can observe presence messages of other client, + * when they entered to the same channel and observing client subscribed + * to multiple actions. + *

+ * + * @throws AblyException + */ + @Test + public void realtime_presence_subscribe_all() throws AblyException { + /* Ably instance that will emit presence events */ + AblyRealtime ably1 = null; + /* Ably instance that will receive presence events */ + AblyRealtime ably2 = null; + + String channelName = "test.presence.subscribe.all" + System.currentTimeMillis(); + + try { + ClientOptions option1 = createOptions(testVars.keys[0].keyStr); + option1.clientId = "emitter client"; + ClientOptions option2 = createOptions(testVars.keys[0].keyStr); + option2.clientId = "receiver client"; + + ably1 = new AblyRealtime(option1); + ably2 = new AblyRealtime(option2); + + Channel channel1 = ably1.channels.get(channelName); + channel1.attach(); + (new ChannelWaiter(channel1)).waitFor(ChannelState.attached); + + Channel channel2 = ably2.channels.get(channelName); + channel2.attach(); + (new ChannelWaiter(channel2)).waitFor(ChannelState.attached); + + ArrayList receivedMessageStack = new ArrayList<>(); + channel2.presence.subscribe(new Presence.PresenceListener() { + List messageStack; + + @Override + public void onPresenceMessage(PresenceMessage message) { + messageStack.add(message); + } + + public Presence.PresenceListener setMessageStack(List messageStack) { + this.messageStack = messageStack; + return this; + } + }.setMessageStack(receivedMessageStack)); + + /* Start emitting channel with ably client 1 (emitter) */ + channel1.presence.enter("Hello, #2!", null); + channel1.presence.update("Lorem ipsum", null); + channel1.presence.update("Dolor sit!", null); + channel1.presence.leave(null); + + /* Wait until receiver client (ably2) observes {@code Action.leave} + * is emitted from emitter client (ably1) + */ + Helpers.PresenceWaiter leavePresenceWaiter = new Helpers.PresenceWaiter(channel2); + leavePresenceWaiter.waitFor(ably1.options.clientId, Action.leave); + + /* Validate that, + * - we received all actions + */ + assertThat(receivedMessageStack.size(), is(equalTo(4))); + for (PresenceMessage message : receivedMessageStack) { + assertThat(message.action, isOneOf(Action.enter, Action.update, Action.leave)); + } + } finally { + if (ably1 != null) ably1.close(); + if (ably2 != null) ably2.close(); + } + } + + /** + *

+ * Validates a client can observe presence messages of other client, + * when they entered to the same channel and observing client subscribed + * to multiple actions. + *

+ * + * @throws AblyException + */ + @Test + public void realtime_presence_subscribe_multiple() throws AblyException { + /* Ably instance that will emit presence events */ + AblyRealtime ably1 = null; + /* Ably instance that will receive presence events */ + AblyRealtime ably2 = null; + + String channelName = "test.presence.subscribe.multiple" + System.currentTimeMillis(); + EnumSet actions = EnumSet.of(Action.update, Action.leave); + + try { + ClientOptions option1 = createOptions(testVars.keys[0].keyStr); + option1.clientId = "emitter client"; + ClientOptions option2 = createOptions(testVars.keys[0].keyStr); + option2.clientId = "receiver client"; + + ably1 = new AblyRealtime(option1); + ably2 = new AblyRealtime(option2); + + Channel channel1 = ably1.channels.get(channelName); + channel1.attach(); + (new ChannelWaiter(channel1)).waitFor(ChannelState.attached); + + Channel channel2 = ably2.channels.get(channelName); + channel2.attach(); + (new ChannelWaiter(channel2)).waitFor(ChannelState.attached); + + final ArrayList receivedMessageStack = new ArrayList<>(); + channel2.presence.subscribe(actions, new Presence.PresenceListener() { + @Override + public void onPresenceMessage(PresenceMessage message) { + synchronized (receivedMessageStack) { + receivedMessageStack.add(message); + receivedMessageStack.notify(); + } + } + }); + + /* Start emitting channel with ably client 1 (emitter) */ + channel1.presence.enter("Hello, #2!", null); + channel1.presence.update("Lorem ipsum", null); + channel1.presence.update("Dolor sit!", null); + channel1.presence.leave(null); + + /* Wait until receiver client (ably2) observes {@code Action.leave} + * is emitted from emitter client (ably1) + */ + try { + synchronized (receivedMessageStack) { + while (receivedMessageStack.size() == 0 || + !receivedMessageStack.get(receivedMessageStack.size()-1).clientId.equals(ably1.options.clientId) || + receivedMessageStack.get(receivedMessageStack.size()-1).action != Action.leave) + receivedMessageStack.wait(); + } + } catch(InterruptedException e) {} + + /* Validate that, + * - we received specific actions + */ + assertThat(receivedMessageStack.size(), is(equalTo(3))); + for (PresenceMessage message : receivedMessageStack) { + assertTrue(actions.contains(message.action)); + } + } finally { + if (ably1 != null) ably1.close(); + if (ably2 != null) ably2.close(); + } + } + + /** + *

+ * Validates a client can observe presence messages of other client, + * when they entered to the same channel and observing client subscribed + * to a single action. + *

+ * + * @throws AblyException + */ + @Test + public void realtime_presence_subscribe_single() throws AblyException { + /* Ably instance that will emit presence events */ + AblyRealtime ably1 = null; + /* Ably instance that will receive presence events */ + AblyRealtime ably2 = null; + + String channelName = "test.presence.subscribe.single." + System.currentTimeMillis(); + PresenceMessage.Action action = Action.enter; + + try { + ClientOptions option1 = createOptions(testVars.keys[0].keyStr); + option1.clientId = "emitter client"; + ClientOptions option2 = createOptions(testVars.keys[0].keyStr); + option2.clientId = "receiver client"; + + ably1 = new AblyRealtime(option1); + ably2 = new AblyRealtime(option2); + + Channel channel1 = ably1.channels.get(channelName); + channel1.attach(); + (new ChannelWaiter(channel1)).waitFor(ChannelState.attached); + + Channel channel2 = ably2.channels.get(channelName); + channel2.attach(); + (new ChannelWaiter(channel2)).waitFor(ChannelState.attached); + + ArrayList receivedMessageStack = new ArrayList<>(); + channel2.presence.subscribe(action, new Presence.PresenceListener() { + List messageStack; + + @Override + public void onPresenceMessage(PresenceMessage message) { + messageStack.add(message); + } + + public Presence.PresenceListener setMessageStack(List messageStack) { + this.messageStack = messageStack; + return this; + } + }.setMessageStack(receivedMessageStack)); + + Helpers.PresenceWaiter waiter = new Helpers.PresenceWaiter(channel2); + + /* Start emitting presence with ably client 1 (emitter) */ + channel1.presence.enter("Hello, #2!", null); + channel1.presence.updatePresence(new PresenceMessage(Action.update, ably1.options.clientId), null); + channel1.presence.update("Lorem Ipsum", null); + channel1.presence.leave(null); + + /* Wait until receiver client (ably2) observes {@code Action.leave} + * is emitted from emitter client (ably1) + */ + waiter.waitFor(ably1.options.clientId, Action.leave); + + /* Validate that, + * - we received specific actions + */ + assertThat(receivedMessageStack, is(not(empty()))); + for (PresenceMessage message : receivedMessageStack) { + assertThat(message.action, is(equalTo(action))); + } + } finally { + if (ably1 != null) ably1.close(); + if (ably2 != null) ably2.close(); + } + } + + /** + *

+ * Validate {@code Presence#subscribe(...)} will result in the listener not being + * registered and an error being indicated, when the channel moves to the FAILED + * state before the operation succeeds + *

+ *

+ * Spec: RTP6c + *

+ * + * @throws AblyException + */ + @Test + public void realtime_presence_attach_implicit_subscribe_fail() throws AblyException { + AblyRealtime ably = null; + try { + ClientOptions optsForToken = createOptions(testVars.keys[0].keyStr); + final AblyRest ablyForToken = new AblyRest(optsForToken); + final String channelName = "realtime_presence_attach_implicit_subscribe_fail" + testParams.name; + + /* get first token */ + Auth.TokenParams tokenParams = new Auth.TokenParams(); + Capability capability = new Capability(); + capability.addResource("otherchannel", "publish"); + tokenParams.capability = capability.toString(); + tokenParams.clientId = testClientId1; + + Auth.TokenDetails token = ablyForToken.auth.requestToken(tokenParams, null); + + /* get second token */ + Auth.TokenParams tokenParams2 = new Auth.TokenParams(); + Capability capability2 = new Capability(); + capability2.addResource(channelName, "publish"); + capability2.addOperation(channelName, "presence"); + capability2.addOperation(channelName, "subscribe"); + tokenParams2.capability = capability2.toString(); + tokenParams2.clientId = testClientId1; + + final Auth.TokenDetails token2 = ablyForToken.auth.requestToken(tokenParams2, null); + assertNotNull("Expected token value", token2.token); + + ClientOptions opts = createOptions(testVars.keys[0].keyStr); + opts.autoConnect = false; + opts.tokenDetails = token; + opts.clientId = testClientId1; + ably = new AblyRealtime(opts); + + final ArrayList presenceMessages = new ArrayList<>(); + Presence.PresenceListener listener = new Presence.PresenceListener() { + @Override + public void onPresenceMessage(PresenceMessage message) { + synchronized (presenceMessages) { + presenceMessages.add(message); + presenceMessages.notify(); + } + } + }; + + /* create a channel and subscribe, implicitly initiate attach */ + CompletionWaiter completionWaiter = new CompletionWaiter(); + final Channel channel = ably.channels.get(channelName); + channel.presence.subscribe(listener, completionWaiter); + + ably.connection.connect(); + + completionWaiter.waitFor(1); + assertFalse("Verify subscribe failed", completionWaiter.success); + assertEquals("Verify subscribe failure error status", completionWaiter.error.statusCode, 401); + assertEquals("Verify failed state reached", channel.state, ChannelState.failed); + + try { + channel.presence.subscribe(new PresenceWaiter(channel)); + fail("Presence.subscribe() shouldn't succeed"); + } catch (AblyException e) { + assertEquals("Verify failure error code", e.errorInfo.code, 90001); + } + + /* Change token to allow channel subscription so we can enter client and verify listener was set despite the failure */ + final boolean[] authUpdated = new boolean[]{false}; + ably.connection.on(ConnectionEvent.update, new ConnectionStateListener() { + @Override + public void onConnectionStateChanged(ConnectionStateChange state) { + synchronized (authUpdated) { + authUpdated[0] = true; + authUpdated.notify(); + } + } + }); + + + ably.auth.authorize(null, new Auth.AuthOptions() {{ + tokenDetails = token2; + }}); + + try { + synchronized (authUpdated) { + while (!authUpdated[0]) + authUpdated.wait(); + } + } catch (InterruptedException e) {} + + channel.attach(); + new ChannelWaiter(channel).waitFor(ChannelState.attached); + + /* Now to ensure listener was set despite the error we enter a client */ + channel.presence.enter(null, null); + try { + synchronized (presenceMessages) { + while (presenceMessages.size() == 0) + presenceMessages.wait(); + } + } catch (InterruptedException e) {} + + assertTrue("Verify listener was set despite channel attach failure", + presenceMessages.size() == 1 && + presenceMessages.get(0).action == Action.enter && presenceMessages.get(0).clientId.equals(testClientId1)); + + } finally { + if(ably != null) + ably.close(); + } + } + + /** + *

+ * Validate {@code Presence#enter(...)} will result in the listener not being + * registered and an error being indicated, when the channel moves to the + * FAILED state before the operation succeeds + *

+ *

+ * Spec: RTP8d + *

+ * + * @throws AblyException + */ + @Test + public void realtime_presence_attach_implicit_enter_fail() throws AblyException { + AblyRealtime ably = null; + try { + ClientOptions opts = createOptions(testVars.keys[1].keyStr); + opts.clientId = "theClient"; + ably = new AblyRealtime(opts); + + /* wait until connected */ + new ConnectionWaiter(ably.connection).waitFor(ConnectionState.connected); + assertEquals("Verify connected state reached", ably.connection.state, ConnectionState.connected); + + /* create a channel and subscribe */ + final Channel channel = ably.channels.get("enter_fail_" + testParams.name); + CompletionWaiter completionWaiter = new CompletionWaiter(); + channel.presence.enter("Lorem Ipsum", completionWaiter); + assertEquals("Verify attaching state reached", channel.state, ChannelState.attaching); + + ErrorInfo errorInfo = completionWaiter.waitFor(); + + new ChannelWaiter(channel).waitFor(ChannelState.failed); + assertEquals("Verify failed state reached", channel.state, ChannelState.failed); + assertEquals("Verify reason code gives correct failure reason", errorInfo.statusCode, 401); + } finally { + if(ably != null) + ably.close(); + } + } + + /** + *

+ * Validate {@code Presence#get(...)} will result in an error, when the channel + * moves to the FAILED state before the operation succeeds + *

+ *

+ * Spec: RTP11b + *

+ * + * @throws AblyException + */ + @Test + public void realtime_presence_attach_implicit_get_fail() throws AblyException { + AblyRealtime ably = null; + try { + ClientOptions opts = createOptions(testVars.keys[1].keyStr); + ably = new AblyRealtime(opts); + + /* wait until connected */ + new ConnectionWaiter(ably.connection).waitFor(ConnectionState.connected); + assertEquals("Verify connected state reached", ably.connection.state, ConnectionState.connected); + + /* create a channel and subscribe */ + final Channel channel = ably.channels.get("get_fail"); + channel.presence.get(false); + assertEquals("Verify attaching state reached", channel.state, ChannelState.attaching); + + ErrorInfo fail = new ChannelWaiter(channel).waitFor(ChannelState.failed); + assertEquals("Verify failed state reached", channel.state, ChannelState.failed); + assertEquals("Verify reason code gives correct failure reason", fail.statusCode, 401); + } finally { + if(ably != null) + ably.close(); + } + } + + /** + *

+ * Validate {@code Presence#enterClient(...)} will result in the listener not being + * registered and an error being indicated, when the channel moves to the FAILED + * state before the operation succeeds + *

+ *

+ * Spec: RTP15e + *

+ * + * @throws AblyException + */ + @Test + public void realtime_presence_attach_implicit_enterclient_fail() throws AblyException { + AblyRealtime ably = null; + try { + ClientOptions opts = createOptions(testVars.keys[1].keyStr); + ably = new AblyRealtime(opts); + + /* wait until connected */ + new ConnectionWaiter(ably.connection).waitFor(ConnectionState.connected); + assertEquals("Verify connected state reached", ably.connection.state, ConnectionState.connected); + + /* create a channel and subscribe */ + final Channel channel = ably.channels.get("enterclient_fail_" + testParams.name); + CompletionWaiter completionWaiter = new CompletionWaiter(); + channel.presence.enterClient("theClient", "Lorem Ipsum", completionWaiter); + assertEquals("Verify attaching state reached", channel.state, ChannelState.attaching); + + ErrorInfo errorInfo = completionWaiter.waitFor(); + + new ChannelWaiter(channel).waitFor(ChannelState.failed); + assertEquals("Verify failed state reached", channel.state, ChannelState.failed); + assertEquals("Verify reason code gives correct failure reason", errorInfo.statusCode, 401); + } finally { + if(ably != null) + ably.close(); + } + } + + /** + *

+ * Validate {@code Presence#updateClient(...)} will result in the listener not being + * registered and an error being indicated, when the channel is in or moves to the + * FAILED state before the operation succeeds + *

+ *

+ * Spec: RTP15e + *

+ * + * @throws AblyException + */ + @Test + public void realtime_presence_attach_implicit_updateclient_fail() throws AblyException { + AblyRealtime ably = null; + try { + ClientOptions opts = createOptions(testVars.keys[1].keyStr); + ably = new AblyRealtime(opts); + + /* wait until connected */ + new ConnectionWaiter(ably.connection).waitFor(ConnectionState.connected); + assertEquals("Verify connected state reached", ably.connection.state, ConnectionState.connected); + + /* create a channel and subscribe */ + final Channel channel = ably.channels.get("updateclient_fail_" + testParams.name); + CompletionWaiter completionWaiter = new CompletionWaiter(); + channel.presence.updateClient("theClient", "Lorem Ipsum", completionWaiter); + assertEquals("Verify attaching state reached", channel.state, ChannelState.attaching); + + ErrorInfo errorInfo = completionWaiter.waitFor(); + + new ChannelWaiter(channel).waitFor(ChannelState.failed); + assertEquals("Verify failed state reached", channel.state, ChannelState.failed); + assertEquals("Verify reason code gives correct failure reason", errorInfo.statusCode, 401); + } finally { + if(ably != null) + ably.close(); + } + } + + /** + *

+ * Validate {@code Presence#leaveClient(...)} will result in the listener not being + * registered and an error being indicated, when the channel is in or moves to the + * FAILED state before the operation succeeds + *

+ *

+ * Spec: RTP15e + *

+ * + * @throws AblyException + */ + @Test + public void realtime_presence_attach_implicit_leaveclient_fail() throws AblyException { + AblyRealtime ably = null; + try { + ClientOptions opts = createOptions(testVars.keys[1].keyStr); + ably = new AblyRealtime(opts); + + /* wait until connected */ + new ConnectionWaiter(ably.connection).waitFor(ConnectionState.connected); + assertEquals("Verify connected state reached", ably.connection.state, ConnectionState.connected); + + /* create a channel and subscribe */ + final Channel channel = ably.channels.get("leaveclient_fail+" + testParams.name); + CompletionWaiter completionWaiter = new CompletionWaiter(); + channel.presence.leaveClient("theClient", "Lorem Ipsum", completionWaiter); + assertEquals("Verify attaching state reached", channel.state, ChannelState.attaching); + completionWaiter.waitFor(); + + ErrorInfo errorInfo = completionWaiter.waitFor(); + + new ChannelWaiter(channel).waitFor(ChannelState.failed); + assertEquals("Verify failed state reached", channel.state, ChannelState.failed); + assertEquals("Verify reason code gives correct failure reason", errorInfo.statusCode, 401); + } finally { + if(ably != null) + ably.close(); + } + } + + /** + *

+ * Validate {@code Presence#get(...)} throws an exception, when the channel + * is in the FAILED state + *

+ * + * @throws AblyException + */ + @Test + public void realtime_presence_get_throws_when_channel_failed() throws AblyException { + AblyRealtime ably = null; + try { + ClientOptions opts = createOptions(testVars.keys[1].keyStr); + ably = new AblyRealtime(opts); + + /* wait until connected */ + new ConnectionWaiter(ably.connection).waitFor(ConnectionState.connected); + assertEquals("Verify connected state reached", ably.connection.state, ConnectionState.connected); + + /* create a channel and subscribe */ + final Channel channel = ably.channels.get("get_fail"); + channel.attach(); + new ChannelWaiter(channel).waitFor(ChannelState.failed); + + try { + channel.presence.get(false); + fail("Presence#get(...) should throw an exception when channel is in failed state"); + } catch(AblyException e) { + assertThat(e.errorInfo.code, is(equalTo(90001))); + assertThat(e.errorInfo.message, is(equalTo("channel operation failed (invalid channel state)"))); + } + } finally { + if(ably != null) + ably.close(); + } + } + + /** + * Test if after reattach when returning from suspended mode client re-enters the channel with the same data + * @throws AblyException + * + * Tests RTP17, RTP19, RTP19a, RTP5f, RTP6b + */ + @Test + public void realtime_presence_suspended_reenter() throws AblyException { + AblyRealtime ably = null; + try { + MockWebsocketFactory mockTransport = new MockWebsocketFactory(); + DebugOptions opts = new DebugOptions(testVars.keys[0].keyStr); + fillInOptions(opts); + opts.transportFactory = mockTransport; + + for (int i=0; i<2; i++) { + final String channelName = "presence_suspended_reenter" + testParams.name + String.valueOf(i); + + mockTransport.allowSend(); + + ably = new AblyRealtime(opts); + + ConnectionWaiter connectionWaiter = new ConnectionWaiter(ably.connection); + connectionWaiter.waitFor(ConnectionState.connected); + + final Channel channel = ably.channels.get(channelName); + channel.attach(); + ChannelWaiter channelWaiter = new ChannelWaiter(channel); + + channelWaiter.waitFor(ChannelState.attached); + + final String presenceData = "PRESENCE_DATA"; + final String connId = ably.connection.id; + + /* + * On the first run to test RTP19a we don't enter client1 so the server on + * return from suspend sees no presence data and sends ATTACHED without HAS_PRESENCE + * The client then should remove all the members from the presence map and then + * re-enter client2. On the second loop run we enter client1 and receive ATTACHED with + * HAS_PRESENCE + */ + final boolean[] wrongPresenceEmitted = new boolean[] {false}; + if (i == 1) { + CompletionWaiter completionWaiter = new CompletionWaiter(); + channel.presence.enterClient(testClientId1, presenceData, completionWaiter); + completionWaiter.waitFor(); + + // RTP5f: after this point there should be no presence event for client1 + channel.presence.subscribe(new Presence.PresenceListener() { + @Override + public void onPresenceMessage(PresenceMessage message) { + if (message.clientId.equals(testClientId1)) + wrongPresenceEmitted[0] = true; + } + }); + } + + final ArrayList leaveMessages = new ArrayList<>(); + /* Subscribe for message type, test RTP6b */ + channel.presence.subscribe(Action.leave, new Presence.PresenceListener() { + @Override + public void onPresenceMessage(PresenceMessage message) { + leaveMessages.add(message); + } + }); + + /* + * We put testClientId2 presence data into the client library presence map but we + * don't send it to the server + */ + + mockTransport.blockSend(); + channel.presence.enterClient(testClientId2, presenceData); + + ProtocolMessage msg = new ProtocolMessage(); + msg.connectionId = connId; + msg.action = ProtocolMessage.Action.sync; + msg.channel = channelName; + msg.presence = new PresenceMessage[]{ + new PresenceMessage() {{ + action = Action.present; + id = String.format("%s:0:0", connId); + timestamp = System.currentTimeMillis(); + clientId = testClientId2; + connectionId = connId; + data = presenceData; + }} + }; + ably.connection.connectionManager.onMessage(null, msg); + + mockTransport.allowSend(); + + ably.connection.connectionManager.requestState(ConnectionState.suspended); + channelWaiter.waitFor(ChannelState.suspended); + + /* + * When restoring from suspended state server will send sync message erasing + * testClientId2 record from the presence map. Client should re-send presence message + * for testClientId2 and restore its presence data. + */ + + ably.connection.connectionManager.requestState(ConnectionState.connected); + channelWaiter.waitFor(ChannelState.attached); + long reconnectTimestamp = System.currentTimeMillis(); + + try { + Thread.sleep(500); + } catch (InterruptedException e) { + } + + AblyRest ablyRest = new AblyRest(opts); + io.ably.lib.rest.Channel restChannel = ablyRest.channels.get(channelName); + assertEquals("Verify presence data is received by the server", + restChannel.presence.get(null).items().length, i==0 ? 1 : 2); + + /* In both cases we should have one leave message in the leaveMessages */ + assertEquals("Verify exactly one LEAVE message was generated", leaveMessages.size(), 1); + + PresenceMessage leaveMessage = leaveMessages.get(0); + assertEquals("Verify LEAVE message follows specs",leaveMessage.action, Action.leave); + assertEquals("Verify LEAVE message follows specs",leaveMessage.clientId, testClientId2); + assertEquals("Verify LEAVE message follows specs",leaveMessage.data, presenceData); + assertTrue("Verify LEAVE message follows specs", Math.abs(leaveMessage.timestamp-reconnectTimestamp) < 2000); + + /* According to RTP5f there should be no presence event emitted for client1 */ + assertFalse("Verify no presence event emitted on return from suspend on SYNC for client1", + wrongPresenceEmitted[0]); + } + } finally { + if(ably != null) + ably.close(); + } + } + + /** + * Test presence message map behaviour (RTP2 features) + * Tests RTP2a, RTP2b1, RTP2b2, RTP2c, RTP2d, RTP2g, RTP18c, RTP6a features + */ + @Test + public void realtime_presence_map_test() throws AblyException { + AblyRealtime ably = null; + try { + ClientOptions opts = createOptions(testVars.keys[0].keyStr); + ably = new AblyRealtime(opts); + final String channelName = "newness_comparison_" + testParams.name; + Channel channel = ably.channels.get(channelName); + channel.attach(); + ChannelWaiter channelWaiter = new ChannelWaiter(channel); + channelWaiter.waitFor(ChannelState.attached); + + final String wontPass = "Won't pass newness test"; + + Presence presence = channel.presence; + final ArrayList presenceMessages = new ArrayList<>(); + /* Subscribe for all the message types, test RTP6a */ + presence.subscribe(new Presence.PresenceListener() { + @Override + public void onPresenceMessage(PresenceMessage message) { + synchronized (presenceMessages) { + assertNotEquals("Verify wrong message didn't pass the newness test", + message.data, wontPass); + // To exclude leave messages that sometimes sneak in let's collect only enter and update messages + if (message.action == Action.enter || message.action == Action.update) { + presenceMessages.add(message); + } + } + } + }); + + /* Test message newness criteria as described in RTP2b */ + final PresenceMessage[] testData = new PresenceMessage[] { + new PresenceMessage() {{ + clientId = "1"; + action = Action.enter; + connectionId = "1"; + id = "1:0"; + }}, + new PresenceMessage() {{ + clientId = "2"; + action = Action.enter; + connectionId = "2"; + id = "2:1:0"; + }}, + /* Should be newer than previous one */ + new PresenceMessage() {{ + clientId = "2"; + action = Action.update; + connectionId = "2"; + id = "2:2:1"; + timestamp = 1; + }}, + /* Shouldn't pass newness test because of message serial, timestamp doesn't matter in this case */ + new PresenceMessage() {{ + clientId = "2"; + action = Action.update; + connectionId = "2"; + id = "2:1:1"; + timestamp = 2; + data = wontPass; + }}, + /* Shouldn't pass because of message index */ + new PresenceMessage() {{ + clientId = "2"; + action = Action.update; + connectionId = "2"; + id = "2:2:0"; + data = wontPass; + }}, + /* Should pass because id is not in form connId:clientId:index and timestamp is greater */ + new PresenceMessage() {{ + clientId = "2"; + action = Action.update; + connectionId = "2"; + id = "weird_id"; + timestamp = 1000; + }}, + /* Shouldn't pass because of timestamp */ + new PresenceMessage() {{ + clientId = "2"; + action = Action.update; + connectionId = "2"; + id = "2:3:1"; + timestamp = 500; + data = wontPass; + }} + }; + + for (final PresenceMessage msg: testData) { + ProtocolMessage protocolMessage = new ProtocolMessage() {{ + channel = channelName; + action = Action.presence; + presence = new PresenceMessage[]{msg}; + }}; + + ably.connection.connectionManager.onMessage(null, protocolMessage); + } + + int n = 0; + for (PresenceMessage testMsg: testData) { + if (testMsg.data != wontPass) { + PresenceMessage factualMsg = n < presenceMessages.size() ? presenceMessages.get(n++) : null; + assertTrue("Verify message passed newness test", + factualMsg != null && factualMsg.id.equals(testMsg.id)); + assertEquals("Verify message was emitted on the presence object with original action", + factualMsg.action, testMsg.action); + assertEquals("Verify message was added to the presence map and stored with PRESENT action", + presence.get(testMsg.clientId, false)[0].action, Action.present); + } + } + assertEquals("Verify nothing else passed the newness test", n, presenceMessages.size()); + + /* Repeat the process now as a part of SYNC and verify everything is exactly the same */ + final String channel2Name = "sync_newness_comparison_" + testParams.name; + Channel channel2 = ably.channels.get(channel2Name); + channel2.attach(); + new ChannelWaiter(channel2).waitFor(ChannelState.attached); + + /* Send all the presence data in one SYNC message without channelSerial (RTP18c) */ + ProtocolMessage syncMessage = new ProtocolMessage() {{ + channel = channel2Name; + action = Action.sync; + presence = testData.clone(); + }}; + final ArrayList syncPresenceMessages = new ArrayList<>(); + channel2.presence.subscribe(new Presence.PresenceListener() { + @Override + public void onPresenceMessage(PresenceMessage message) { + syncPresenceMessages.add(message); + } + }); + ably.connection.connectionManager.onMessage(null, syncMessage); + + assertEquals("Verify result is the same in case of SYNC", syncPresenceMessages.size(), presenceMessages.size()); + for (int i=0; i100) number of clients so there are several sync messages, disconnect transport + * in the middle and verify channel is re-syncing presence messages after transport reconnect + * + * Tests RTP3 + */ + @Test + public void reattach_resume_broken_sync() { + AblyRealtime clientAbly1 = null; + AblyRealtime clientAbly2 = null; + TestChannel testChannel = new TestChannel(); + int clientCount = 150; /* Should be greater than 100 to break sync into several messages */ + try { + /* subscribe for presence events in the anonymous connection */ + new PresenceWaiter(testChannel.realtimeChannel); + + /* set up a connection with specific clientId */ + ClientOptions client1Opts = new ClientOptions(testVars.keys[0].keyStr); + fillInOptions(client1Opts); + client1Opts.tokenDetails = wildcardToken; + clientAbly1 = new AblyRealtime(client1Opts); + + /* wait until connected */ + (new ConnectionWaiter(clientAbly1.connection)).waitFor(ConnectionState.connected); + assertEquals("Verify connected state reached", clientAbly1.connection.state, ConnectionState.connected); + + /* get channel and attach */ + Channel client1Channel = clientAbly1.channels.get(testChannel.channelName); + client1Channel.attach(); + (new ChannelWaiter(client1Channel)).waitFor(ChannelState.attached); + assertEquals("Verify attached state reached", client1Channel.state, ChannelState.attached); + + /* let client1 enter the channel for multiple clients and wait for the success callback */ + CompletionSet enterComplete = new CompletionSet(); + for(int i = 0; i < clientCount; i++) { + client1Channel.presence.enterClient("client" + i, "Test data (attach_enter_multiple) " + i, enterComplete.add()); + } + enterComplete.waitFor(); + assertTrue("Verify enter callback called on completion", enterComplete.pending.isEmpty()); + assertTrue("Verify no enter errors", enterComplete.errors.isEmpty()); + + /* set up a second connection with different clientId */ + final MockWebsocketFactory mockTransport20 = new MockWebsocketFactory(); + DebugOptions client2Opts = new DebugOptions(testVars.keys[0].keyStr); + fillInOptions(client2Opts); + client2Opts.transportFactory = mockTransport20; + client2Opts.tokenDetails = token2; + client2Opts.clientId = testClientId2; + client2Opts.autoConnect = false; + + mockTransport20.allowSend(); + clientAbly2 = new AblyRealtime(client2Opts); + + /* wait until connected */ + ConnectionWaiter connectionWaiter = new ConnectionWaiter(clientAbly2.connection); + clientAbly2.connection.connect(); + connectionWaiter.waitFor(ConnectionState.connected); + + /* get channel */ + final Channel client2Channel = clientAbly2.channels.get(testChannel.channelName); + final ConnectionManager connectionManager = clientAbly2.connection.connectionManager; + final boolean[] disconnectedTransport = new boolean[]{false}; + final int[] presenceCount = new int[]{0}; + client2Channel.attach(new CompletionListener() { + @Override + public void onSuccess() { + try { + client2Channel.presence.subscribe(new Presence.PresenceListener() { + @Override + public void onPresenceMessage(PresenceMessage message) { + if (!disconnectedTransport[0]) { + mockTransport20.lastCreatedTransport.close(); + connectionManager.onTransportUnavailable(mockTransport20.lastCreatedTransport, new ErrorInfo("Mock", 50000)); + + } + disconnectedTransport[0] = true; + presenceCount[0]++; + } + }); + } + catch (AblyException e) { + } + } + + @Override + public void onError(ErrorInfo reason) { + } + }); + + ChannelWaiter channelWaiter = new ChannelWaiter(client2Channel); + channelWaiter.waitFor(ChannelState.attached); + + /* Wait for reconnect */ + connectionWaiter.waitFor(ConnectionState.connected, 2); + + client2Channel.presence.unsubscribe(); + + /* Verify that channel received sync and all 150 presence messages are received */ + try { + Thread.sleep(500); + assertEquals("Verify number of received presence messages", client2Channel.presence.get(true).length, clientCount); + } catch (InterruptedException e) {} + + } catch(AblyException e) { + e.printStackTrace(); + fail("Unexpected exception running test: " + e.getMessage()); + } finally { + if(clientAbly1 != null) + clientAbly1.close(); + if(clientAbly2 != null) + clientAbly2.close(); + testChannel.dispose(); + } + } + + /** + * Test if presence sync works as it should + * Tests RTP18a, RTP18b, RTP2f + */ + @Test + public void presence_sync() { + AblyRealtime ably = null; + try { + ClientOptions opts = createOptions(testVars.keys[0].keyStr); + ably = new AblyRealtime(opts); + + final String channelName = "presence_sync_test" + testParams.name; + + final Channel channel = ably.channels.get(channelName); + channel.attach(); + ChannelWaiter channelWaiter = new ChannelWaiter(channel); + channelWaiter.waitFor(ChannelState.attached); + + final ArrayList presenceHistory = new ArrayList<>(); + channel.presence.subscribe(new Presence.PresenceListener() { + @Override + public void onPresenceMessage(PresenceMessage message) { + presenceHistory.add(message); + } + }); + + final PresenceMessage[] testPresence1 = new PresenceMessage[] { + /* Will be discarded because we'll start new sync with different channelSerial */ + new PresenceMessage() {{ + clientId = "1"; + action = Action.enter; + connectionId = "1"; + id = "1:0"; + }} + }; + + final PresenceMessage[] testPresence2 = new PresenceMessage[] { + new PresenceMessage() {{ + clientId = "2"; + action = Action.enter; + connectionId = "2"; + id = "2:1:0"; + }}, + /* Enter presence message here is newer than leave in the subsequent message */ + new PresenceMessage() {{ + clientId = "3"; + action = Action.enter; + connectionId = "3"; + id = "3:1:0"; + }} + }; + + final PresenceMessage[] testPresence3 = new PresenceMessage[] { + new PresenceMessage() {{ + clientId = "3"; + action = Action.leave; + connectionId = "3"; + id = "3:0:0"; + }}, + new PresenceMessage() {{ + clientId = "4"; + action = Action.enter; + connectionId = "4"; + id = "4:1:1"; + }}, + new PresenceMessage() {{ + clientId = "4"; + action = Action.leave; + connectionId = "4"; + id = "4:2:2"; + }} + }; + + final boolean[] seenLeaveMessageAsAbsentForClient4 = new boolean[] {false}; + channel.presence.subscribe(Action.leave, new Presence.PresenceListener() { + @Override + public void onPresenceMessage(PresenceMessage message) { + try { + /* + * Do not call it in states other than ATTACHED because of presence.get() side + * effect of attaching channel + */ + if (message.clientId.equals("4") && message.action == Action.leave && channel.state == ChannelState.attached) { + /* + * Client library won't return a presence message if it is stored as ABSENT + * so the result of the presence.get() call should be empty. This is the + * only case when get() called from PresenceListener.onPresenceMessage results + * in an empty answer. + */ + seenLeaveMessageAsAbsentForClient4[0] = channel.presence.get("4", false).length == 0; + } + } catch (AblyException e) {} + } + }); + + ably.connection.connectionManager.onMessage(null, new ProtocolMessage() {{ + action = Action.sync; + channel = channelName; + channelSerial = "1:1"; + presence = testPresence1; + }}); + ably.connection.connectionManager.onMessage(null, new ProtocolMessage() {{ + action = Action.sync; + channel = channelName; + channelSerial = "2:1"; + presence = testPresence2; + }}); + ably.connection.connectionManager.onMessage(null, new ProtocolMessage() {{ + action = Action.sync; + channel = channelName; + channelSerial = "2:"; + presence = testPresence3; + }}); + + assertEquals("Verify incomplete sync was discarded", channel.presence.get("1", false).length, 0); + assertEquals("Verify client with id==2 is in presence map", channel.presence.get("2", false).length, 1); + assertEquals("Verify client with id==3 is in presence map", channel.presence.get("3", false).length, 1); + assertEquals("Verify nothing else is in presence map", channel.presence.get(false).length, 2); + + assertTrue("Verify LEAVE message for client with id==4 was stored as ABSENT", seenLeaveMessageAsAbsentForClient4[0]); + + PresenceMessage[] correctPresenceHistory = new PresenceMessage[] { + /* client 1 enters (will later be discarded) */ + new PresenceMessage(Action.enter, "1"), + /* client 2 enters */ + new PresenceMessage(Action.enter, "2"), + /* client 3 enters and never leaves because of newness comparison for LEAVE fails */ + new PresenceMessage(Action.enter, "3"), + /* client 4 enters and leaves */ + new PresenceMessage(Action.enter, "4"), + new PresenceMessage(Action.leave, "4"), + /* client 1 is eliminated from the presence map because the first portion of SYNC is discarded */ + new PresenceMessage(Action.leave, "1") + }; + + assertEquals("Verify number of presence messages", presenceHistory.size(), correctPresenceHistory.length); + for (int i=0; i sentPresence = new ArrayList<>(); + + /* Allow send but record all the presence messages for later analysis */ + final MockWebsocketFactory mockTransport = new MockWebsocketFactory(); + mockTransport.allowSend(new MockWebsocketFactory.MessageFilter() { + @Override + public boolean matches(ProtocolMessage message) { + if (message.action == ProtocolMessage.Action.presence && message.presence != null) { + synchronized (sentPresence) { + Collections.addAll(sentPresence, message.presence); + sentPresence.notify(); + } + } + return true; + } + }); + + DebugOptions opts = new DebugOptions(testVars.keys[0].keyStr); + fillInOptions(opts); + opts.clientId = testClientId1; + opts.transportFactory = mockTransport; + ably = new AblyRealtime(opts); + + Channel channel = ably.channels.get("protocol_enter_message_format_" + testParams.name); + /* using testClientId1 */ + channel.presence.enter(null, null); + + synchronized (sentPresence) { + while (sentPresence.size() < 1) + sentPresence.wait(); + } + + assertEquals("Verify number of presence messages sent", sentPresence.size(), 1); + assertTrue("Verify presence messages follows spec", + sentPresence.get(0).action == Action.enter && + sentPresence.get(0).clientId == null + ); + + channel.detach(); + new ChannelWaiter(channel).waitFor(ChannelState.detached); + + try { + channel.presence.enter(null, null); + fail("Presence.enter() shouldn't succeed in detached state"); + } catch (AblyException e) { + assertEquals("Verify exception error code", e.errorInfo.code, 91001 /* unable to enter presence channel (invalid channel state) */); + } + + } finally { + if (ably != null) + ably.close(); + } + } + + /** + * Verify protocol messages sent on Presence.enter() follow specs if sent from correct state and + * the call fails if sent from DETACHED state + * + * Tests RTP8c, RTP8g + */ + @Test + public void protocol_enterclient_message_format() throws AblyException, InterruptedException { + AblyRealtime ably = null; + + try { + final ArrayList sentPresence = new ArrayList<>(); + + /* Allow send but record all the presence messages for later analysis */ + final MockWebsocketFactory mockTransport = new MockWebsocketFactory(); + mockTransport.allowSend(new MockWebsocketFactory.MessageFilter() { + @Override + public boolean matches(ProtocolMessage message) { + if (message.action == ProtocolMessage.Action.presence && message.presence != null) { + synchronized (sentPresence) { + Collections.addAll(sentPresence, message.presence); + sentPresence.notify(); + } + } + return true; + } + }); + + DebugOptions opts = new DebugOptions(testVars.keys[0].keyStr); + fillInOptions(opts); + opts.transportFactory = mockTransport; + ably = new AblyRealtime(opts); + + Channel channel = ably.channels.get("protocol_enterclient_message_format_" + testParams.name); + /* using testClientId2 */ + channel.presence.enterClient(testClientId2); + + synchronized (sentPresence) { + while (sentPresence.size() < 1) + sentPresence.wait(); + } + + assertEquals("Verify number of presence messages sent", sentPresence.size(), 1); + assertTrue("Verify presence messages follows spec", + sentPresence.get(0).action == Action.enter && + sentPresence.get(0).clientId.equals(testClientId2) + ); + + channel.detach(); + new ChannelWaiter(channel).waitFor(ChannelState.detached); + + try { + channel.presence.enterClient("testClient3"); + fail("Presence.enterClient() shouldn't succeed in detached state"); + } catch (AblyException e) { + assertEquals("Verify exception error code", e.errorInfo.code, 91001 /* unable to enter presence channel (invalid channel state) */); + } + + } finally { + if (ably != null) + ably.close(); + } + } + + /* + * Verify presence data is received and encoded/decoded correctly + * Tests RTP8e, RTP6a + */ + @Test + public void presence_encoding() throws AblyException, InterruptedException { + AblyRealtime ably1 = null, ably2 = null; + try { + /* Set up two connections: one for entering, one for listening */ + final String channelName = "presence_encoding" + testParams.name; + ClientOptions opts = createOptions(testVars.keys[0].keyStr); + ably1 = new AblyRealtime(opts); + ably2 = new AblyRealtime(opts); + + Channel channel1 = ably1.channels.get(channelName); + Channel channel2 = ably2.channels.get(channelName); + + channel2.attach(); + new ChannelWaiter(channel2).waitFor(ChannelState.attached); + final ArrayList receivedPresenceData = new ArrayList<>(); + channel2.presence.subscribe(new Presence.PresenceListener() { + @Override + public void onPresenceMessage(PresenceMessage message) { + synchronized (receivedPresenceData) { + receivedPresenceData.add(message.data); + receivedPresenceData.notify(); + } + } + }); + + String testStringData = "123"; + byte[] testByteData = new byte[] {1, 2, 3}; + JsonElement testJsonData = new JsonParser().parse("{\"var1\":\"val1\", \"var2\": \"val2\"}"); + + channel1.presence.enterClient("1", testStringData); + channel1.presence.enterClient("2", testByteData); + channel1.presence.enterClient("3", testJsonData); + synchronized (receivedPresenceData) { + while (receivedPresenceData.size() < 3) + receivedPresenceData.wait(); + } + + assertEquals("Verify number of received presence messages", receivedPresenceData.size(), 3); + assertEquals("Verify string data", receivedPresenceData.get(0), testStringData); + assertTrue("Verify byte[] data", + receivedPresenceData.get(1) instanceof byte[] && + Arrays.equals((byte[])receivedPresenceData.get(1), testByteData)); + assertEquals("Verify JSON data", receivedPresenceData.get(2), testJsonData); + + /* use data from ENTER message */ + channel1.presence.leaveClient("1"); + /* use different data */ + channel1.presence.leaveClient("2", "leave"); + + synchronized (receivedPresenceData) { + while (receivedPresenceData.size() < 5) + receivedPresenceData.wait(); + } + + assertEquals("Verify string data for enter message is used in leave message", receivedPresenceData.get(3), testStringData); + assertEquals("Verify overridden leave data", receivedPresenceData.get(4), "leave"); + + } finally { + if (ably1 != null) + ably1.close(); + if (ably2 != null) + ably2.close(); + } + } + + /* + * Test Presence.get() filtering and syncToWait flag + * Tests RTP11b, RTP11c, RTP11d + */ + @Test + public void presence_get() throws AblyException, InterruptedException { + AblyRealtime ably1 = null, ably2 = null; + try { + /* Set up two connections: one for entering, one for listening */ + final String channelName = "presence_get" + testParams.name; + ClientOptions opts = createOptions(testVars.keys[0].keyStr); + ably1 = new AblyRealtime(opts); + opts.autoConnect = false; + ably2 = new AblyRealtime(opts); + + Channel channel1 = ably1.channels.get(channelName); + CompletionWaiter completionWaiter = new CompletionWaiter(); + channel1.presence.enterClient("1", null, completionWaiter); + channel1.presence.enterClient("2", null, completionWaiter); + completionWaiter.waitFor(2); + + Channel channel2 = ably2.channels.get(channelName); + PresenceWaiter waiter2 = new PresenceWaiter(channel2); + + /* + * Wait with waitForSync set to false, should result in 0 members because autoConnect is set to false + * This also tests implicit attach() + */ + PresenceMessage[] presenceMessages1 = channel2.presence.get(false); + assertEquals("Verify number of presence members before SYNC", presenceMessages1.length, 0); + + ably2.connection.connect(); + + /* now that waitForSync is true it should get all the members entered on first connection */ + PresenceMessage[] presenceMessages2 = channel2.presence.get(true); + assertEquals("Verify number of presence members after SYNC", presenceMessages2.length, 2); + + /* enter third member from second connection */ + channel2.presence.enterClient("3", null, completionWaiter); + completionWaiter.waitFor(3); + waiter2.waitFor(3); + + /* filter by clientId */ + PresenceMessage[] presenceMessages3 = channel2.presence.get(new Param(Presence.GET_CLIENTID, "1")); + assertTrue("Verify clientId filter works", + presenceMessages3.length == 1 && presenceMessages3[0].clientId.equals("1")); + + /* filter by connectionId */ + PresenceMessage[] presenceMessages4 = channel2.presence.get(new Param(Presence.GET_CONNECTIONID, ably2.connection.id)); + assertTrue("Verify connectionId filter works", + presenceMessages4.length == 1 && presenceMessages4[0].clientId.equals("3")); + + /* filter by both clientId and connectionId */ + PresenceMessage[] presenceMessages5 = channel2.presence.get( + new Param(Presence.GET_CONNECTIONID, ably1.connection.id), + new Param(Presence.GET_CLIENTID, "2") + ); + PresenceMessage[] presenceMessages6 = channel2.presence.get( + new Param(Presence.GET_CONNECTIONID, ably2.connection.id), + new Param(Presence.GET_CLIENTID, "2") + ); + assertTrue("Verify clientId+connectionId filter works", + presenceMessages5.length == 1 && presenceMessages5[0].clientId.equals("2") && presenceMessages6.length == 0); + + /* go into suspended mode */ + ably2.connection.connectionManager.requestState(ConnectionState.suspended); + new ConnectionWaiter(ably2.connection).waitFor(ConnectionState.suspended); + + /* try with wait set to false, should get all the three members */ + PresenceMessage[] presenceMessages7 = channel2.presence.get(false); + assertEquals("Verify Presence.get() with waitForSync set to false works in SUSPENDED state", presenceMessages7.length, 3); + + /* try with wait set to true, should get exception */ + try { + channel2.presence.get(true); + fail("Presence.get() with waitForSync=true shouldn't succeed in SUSPENDED state"); + } catch (AblyException e) { + assertEquals("Verify correct error code for Presence.get() with waitForSync=true in SUSPENDED state", e.errorInfo.code, 91005); + } + } finally { + if (ably1 != null) + ably1.close(); + if (ably2 != null) + ably2.close(); + } + } + + /** + * Authenticate using wildcard token, initialize AblyRealtime so clientId is not known a priori, + * call enter() without attaching first, start connection + * + * Expect NACK from the server because client is unidentified + * + * Tests RTP8i, RTP8f, partial tests for RTP9e, RTP10e + */ + @Test + public void enter_before_clientid_is_known() throws AblyException { + AblyRealtime ably = null; + try { + ClientOptions restOpts = createOptions(testVars.keys[0].keyStr); + AblyRest ablyForToken = new AblyRest(restOpts); + + /* Initialize connection so clientId is not known before actual connection */ + Auth.TokenParams tokenParams = new Auth.TokenParams(); + Capability capability = new Capability(); + tokenParams.capability = capability.toString(); + tokenParams.clientId = "*"; + + Auth.TokenDetails token = ablyForToken.auth.requestToken(tokenParams, null); + assertNotNull("Expected token value", token.token); + + ClientOptions opts = createOptions(); + opts.defaultTokenParams.clientId = "*"; + opts.token = token.token; + opts.autoConnect = false; + ably = new AblyRealtime(opts); + + /* enter without attaching first */ + Channel channel = ably.channels.get("enter_before_clientid_is_known"+testParams.name); + CompletionWaiter completionWaiter = new CompletionWaiter(); + channel.presence.enter(null, completionWaiter); + + ably.connection.connect(); + + completionWaiter.waitFor(1); + assertFalse("Verify enter() failed", completionWaiter.success); + assertEquals("Verify error code", completionWaiter.error.code, 40012); + + /* Now clientId is known to be "*" and subsequent enter() should fail immediately */ + completionWaiter.reset(); + channel.presence.enter(null, completionWaiter); + completionWaiter.waitFor(1); + assertFalse("Verify enter() failed", completionWaiter.success); + assertEquals("Verify error code", completionWaiter.error.code, 91000); + + /* and so should update() and leave() */ + completionWaiter.reset(); + channel.presence.update(null, completionWaiter); + completionWaiter.waitFor(1); + assertFalse("Verify update() failed", completionWaiter.success); + assertEquals("Verify error code", completionWaiter.error.code, 91000); + + completionWaiter.reset(); + channel.presence.leave(null, completionWaiter); + completionWaiter.waitFor(1); + assertFalse("Verify update() failed", completionWaiter.success); + assertEquals("Verify error code", completionWaiter.error.code, 91000); + + } finally { + if (ably != null) + ably.close(); + } + } + + /** + * To Test PresenceMessage.fromEncoded(JsonObject, ChannelOptions) and PresenceMessage.fromEncoded(String, ChannelOptions) + * Refer Spec TP4 + * @throws AblyException + */ + @Test + public void message_from_encoded_json_object() throws AblyException { + ChannelOptions options = null; + byte[] data = "0123456789".getBytes(); + PresenceMessage encoded = new PresenceMessage(Action.present, "client-123"); + encoded.data = data; + encoded.encode(options); + + PresenceMessage decoded = PresenceMessage.fromEncoded(Serialisation.gson.toJson(encoded), options); + assertEquals(encoded.clientId, decoded.clientId); + assertArrayEquals(data, (byte[]) decoded.data); + + /*Test JSON Data decoding in PresenceMessage.fromEncoded(JsonObject)*/ + JsonObject person = new JsonObject(); + person.addProperty("name", "Amit"); + person.addProperty("country", "Interlaken Ost"); + + PresenceMessage userDetails = new PresenceMessage(Action.absent, "client-123", person); + userDetails.encode(options); + + PresenceMessage decodedMessage1 = PresenceMessage.fromEncoded(Serialisation.gson.toJsonTree(userDetails).getAsJsonObject(), null); + assertEquals(person, decodedMessage1.data); + + /*Test PresenceMessage.fromEncoded(String)*/ + PresenceMessage decodedMessage2 = PresenceMessage.fromEncoded(Serialisation.gson.toJson(userDetails), options); + assertEquals(person, decodedMessage2.data); + + /*Test invalid case.*/ + try { + //We pass invalid PresenceMessage object + PresenceMessage.fromEncoded(person, options); + fail(); + } catch(Exception e) {/*ignore as we are expecting it to fail.*/} + } + + /** + * To test PresenceMessage.fromEncodedArray(JsonArray, ChannelOptions) and PresenceMessage.fromEncodedArray(String, ChannelOptions) + * Refer Spec. TP4 + * @throws AblyException + */ + @Test + public void messages_from_encoded_json_array() throws AblyException { + JsonArray fixtures = null; + MessagesData testMessages = null; + try { + testMessages = (MessagesData) Setup.loadJson(testMessagesEncodingFile, MessagesData.class); + JsonObject jsonObject = (JsonObject) Setup.loadJson(testMessagesEncodingFile, JsonObject.class); + //We use this as-is for decoding purposes. + fixtures = jsonObject.getAsJsonArray("messages"); + } catch(IOException e) { + fail(); + return; + } + PresenceMessage[] decodedMessages = PresenceMessage.fromEncodedArray(fixtures, null); + for(int index = 0; index < decodedMessages.length; index++) { + PresenceMessage testInputMsg = testMessages.messages[index]; + testInputMsg.decode(null); + if(testInputMsg.data instanceof byte[]) { + assertArrayEquals((byte[]) testInputMsg.data, (byte[]) decodedMessages[index].data); + } else { + assertEquals(testInputMsg.data, decodedMessages[index].data); + } + } + /*Test PresenceMessage.fromEncodedArray(String)*/ + String fixturesArray = Serialisation.gson.toJson(fixtures); + PresenceMessage[] decodedMessages2 = PresenceMessage.fromEncodedArray(fixturesArray, null); + for(int index = 0; index < decodedMessages2.length; index++) { + PresenceMessage testInputMsg = testMessages.messages[index]; + if(testInputMsg.data instanceof byte[]) { + assertArrayEquals((byte[]) testInputMsg.data, (byte[]) decodedMessages2[index].data); + } else { + assertEquals(testInputMsg.data, decodedMessages2[index].data); + } + } + } + + static class MessagesData { + public PresenceMessage[] messages; + } } diff --git a/lib/src/test/java/io/ably/lib/test/realtime/RealtimeReauthTest.java b/lib/src/test/java/io/ably/lib/test/realtime/RealtimeReauthTest.java index 388846084..349e89e36 100644 --- a/lib/src/test/java/io/ably/lib/test/realtime/RealtimeReauthTest.java +++ b/lib/src/test/java/io/ably/lib/test/realtime/RealtimeReauthTest.java @@ -38,468 +38,468 @@ */ public class RealtimeReauthTest extends ParameterizedTest { - @Rule - public Timeout testTimeout = Timeout.seconds(90); - - /** - * RTC8a: In-place reauthorization on a connected connection. - * RTC8a1: A test should exist that performs an upgrade of - * capabilities without any loss of continuity or connectivity - * during the upgrade process. - */ - @Test - public void reauth_tokenDetails() { - String wrongChannel = "wrongchannel"; - String rightChannel = "rightchannel"; - String testClientId = "testClientId"; - - try { - /* init ably for token */ - ClientOptions optsForToken = createOptions(testVars.keys[0].keyStr); - final AblyRest ablyForToken = new AblyRest(optsForToken); - - /* get first token */ - Auth.TokenParams tokenParams = new Auth.TokenParams(); - Capability capability = new Capability(); - capability.addResource(wrongChannel, "*"); - tokenParams.capability = capability.toString(); - tokenParams.clientId = testClientId; - - Auth.TokenDetails firstToken = ablyForToken.auth.requestToken(tokenParams, null); - assertNotNull("Expected token value", firstToken.token); - - /* create ably realtime with tokenDetails and clientId */ - ClientOptions opts = createOptions(); - opts.clientId = testClientId; - opts.tokenDetails = firstToken; - AblyRealtime ablyRealtime = new AblyRealtime(opts); - - /* wait for connected state */ - Helpers.ConnectionWaiter connectionWaiter = new Helpers.ConnectionWaiter(ablyRealtime.connection); - connectionWaiter.waitFor(ConnectionState.connected); - assertEquals("Verify connected state is reached", ConnectionState.connected, ablyRealtime.connection.state); - - /* create a channel and check can't attach */ - Channel channel = ablyRealtime.channels.get(rightChannel); - Helpers.CompletionWaiter waiter = new Helpers.CompletionWaiter(); - channel.attach(waiter); - ErrorInfo error = waiter.waitFor(); - assertNotNull("Expected error", error); - assertEquals("Verify error code 40160 (channel is denied access)", error.code, 40160); - - /* get second token */ - tokenParams = new Auth.TokenParams(); - capability = new Capability(); - capability.addResource(wrongChannel, "*"); - capability.addResource(rightChannel, "*"); - tokenParams.capability = capability.toString(); - tokenParams.clientId = testClientId; - - Auth.TokenDetails secondToken = ablyForToken.auth.requestToken(tokenParams, null); - assertNotNull("Expected token value", secondToken.token); - - /* reauthorize */ - connectionWaiter.reset(); - Auth.AuthOptions authOptions = new Auth.AuthOptions(); - authOptions.key = testVars.keys[0].keyStr; - authOptions.tokenDetails = secondToken; - Auth.TokenDetails reauthTokenDetails = ablyRealtime.auth.authorize(null, authOptions); - assertNotNull("Expected token value", reauthTokenDetails.token); - - /* re-attach to the channel */ - waiter = new Helpers.CompletionWaiter(); - channel.attach(waiter); - - /* verify onSuccess callback gets called */ - waiter.waitFor(); - assertThat(waiter.success, is(true)); - /* Verify that the connection never disconnected (0.9 in-place authorization) */ - assertTrue("Expected in-place authorization", connectionWaiter.getCount(ConnectionState.connecting) == 0); - - ablyRealtime.close(); - } catch (AblyException e) { - e.printStackTrace(); - fail(); - } - } - - /** - /* RTC8a1: Another test should exist where the capabilities are - * downgraded resulting in Ably sending an ERROR ProtocolMessage - * with a channel property, causing the channel to enter the FAILED - * state. That test must assert that the channel becomes failed - * soon after the token update and the reason is included in the - * channel state change event. - */ - @Test - public void reauth_downgrade() { - String wrongChannel = "wrongchannel"; - String rightChannel = "rightchannel"; - String testClientId = "testClientId"; - - try { - /* init ably for token */ - ClientOptions optsForToken = createOptions(testVars.keys[0].keyStr); - final AblyRest ablyForToken = new AblyRest(optsForToken); - - /* get first (good) token */ - Auth.TokenParams tokenParams = new Auth.TokenParams(); - Capability capability = new Capability(); - capability.addResource(wrongChannel, "*"); - capability.addResource(rightChannel, "*"); - tokenParams.capability = capability.toString(); - tokenParams.clientId = testClientId; - Auth.TokenDetails firstToken = ablyForToken.auth.requestToken(tokenParams, null); - assertNotNull("Expected token value", firstToken.token); - - /* create ably realtime with tokenDetails and clientId */ - ClientOptions opts = createOptions(); - opts.clientId = testClientId; - opts.tokenDetails = firstToken; - AblyRealtime ablyRealtime = new AblyRealtime(opts); - - /* wait for connected state */ - Helpers.ConnectionWaiter connectionWaiter = new Helpers.ConnectionWaiter(ablyRealtime.connection); - connectionWaiter.waitFor(ConnectionState.connected); - assertEquals("Verify connected state is reached", ConnectionState.connected, ablyRealtime.connection.state); - - /* create a channel and check attached */ - Channel channel = ablyRealtime.channels.get(rightChannel); - Helpers.ChannelWaiter waiter = new Helpers.ChannelWaiter(channel); - channel.attach(); - /* verify onSuccess callback gets called */ - waiter.waitFor(ChannelState.attached); - assertEquals("Verify attached state reached", channel.state, ChannelState.attached); - - /* get second (bad) token */ - tokenParams = new Auth.TokenParams(); - capability = new Capability(); - capability.addResource(wrongChannel, "*"); - tokenParams.capability = capability.toString(); - tokenParams.clientId = testClientId; - Auth.TokenDetails secondToken = ablyForToken.auth.requestToken(tokenParams, null); - assertNotNull("Expected token value", secondToken.token); - - /* reauthorize */ - connectionWaiter.reset(); - Auth.AuthOptions authOptions = new Auth.AuthOptions(); - authOptions.tokenDetails = secondToken; - Auth.TokenDetails reauthTokenDetails = ablyRealtime.auth.authorize(null, authOptions); - assertNotNull("Expected token value", reauthTokenDetails.token); - - /* Check that the channel moves to failed state within 2s, and that - * we get the expected error code. */ - long before = System.currentTimeMillis(); - ErrorInfo err = waiter.waitFor(ChannelState.failed); - assertEquals("Verify failed state reached", channel.state, ChannelState.failed); - assertEquals("Verify error code", err.code, 40160); - assertTrue("Expected channel to fail quickly", System.currentTimeMillis() - before < 2000); - - ablyRealtime.close(); - } catch (AblyException e) { - e.printStackTrace(); - fail(); - } - } - - /** - /* RTC8a2: If the authentication token change fails, then Ably will send an - * ERROR ProtocolMessage triggering the connection to transition to the - * FAILED state. A test should exist for a token change that fails (such as - * sending a new token with an incompatible clientId) - */ - @Test - public void reauth_fail() { - String rightChannel = "rightchannel"; - String testClientId = "testClientId"; - String badClientId = "badClientId"; - - try { - /* init ably for token */ - ClientOptions optsForToken = createOptions(testVars.keys[0].keyStr); - final AblyRest ablyForToken = new AblyRest(optsForToken); - - /* get first (good) token */ - Auth.TokenParams tokenParams = new Auth.TokenParams(); - Capability capability = new Capability(); - capability.addResource(rightChannel, "*"); - tokenParams.capability = capability.toString(); - tokenParams.clientId = testClientId; - Auth.TokenDetails firstToken = ablyForToken.auth.requestToken(tokenParams, null); - assertNotNull("Expected token value", firstToken.token); - - /* create ably realtime with tokenDetails and clientId */ - ClientOptions opts = createOptions(); - opts.clientId = testClientId; - opts.tokenDetails = firstToken; - AblyRealtime ablyRealtime = new AblyRealtime(opts); - - /* wait for connected state */ - Helpers.ConnectionWaiter connectionWaiter = new Helpers.ConnectionWaiter(ablyRealtime.connection); - connectionWaiter.waitFor(ConnectionState.connected); - assertEquals("Verify connected state is reached", ConnectionState.connected, ablyRealtime.connection.state); - - /* create a channel and check attached */ - Channel channel = ablyRealtime.channels.get(rightChannel); - Helpers.ChannelWaiter waiter = new Helpers.ChannelWaiter(channel); - channel.attach(); - /* verify onSuccess callback gets called */ - waiter.waitFor(ChannelState.attached); - assertEquals("Verify attached state reached", channel.state, ChannelState.attached); - - /* get second (bad) token */ - tokenParams.clientId = badClientId; - Auth.TokenDetails secondToken = ablyForToken.auth.requestToken(tokenParams, null); - /* revert client id in token details, otherwise it will be blocked by the client library */ - secondToken.clientId = testClientId; - assertNotNull("Expected token value", secondToken.token); - - /* Reauthorize. We expect this to throw an exception from the - * mismatched client id and end up in failed state. */ - connectionWaiter.reset(); - Auth.AuthOptions authOptions = new Auth.AuthOptions(); - authOptions.tokenDetails = secondToken; - try { - Auth.TokenDetails reauthTokenDetails = ablyRealtime.auth.authorize(null, authOptions); - assertFalse("Expecting exception", true); - System.out.println("Authorize failed to throw an exception"); - } catch (AblyException e) { - assertEquals("Expecting failed", ConnectionState.failed, ablyRealtime.connection.state); - System.out.println("Got failed connection"); - } - - /** - * RTC8c: If the connection is in the DISCONNECTED, SUSPENDED, FAILED, or - * CLOSED state when auth#authorize is called, after obtaining a token the - * library should move to the CONNECTING state and initiate a connection - * attempt using the new token, and RTC8b1 applies. - */ - - /* Reauthorize with good token. We expect this to connect. */ - connectionWaiter.reset(); - authOptions = new Auth.AuthOptions(); - authOptions.tokenDetails = firstToken; - Auth.TokenDetails reauthTokenDetails = ablyRealtime.auth.authorize(null, authOptions); - assertNotNull("Expected token value", reauthTokenDetails.token); - connectionWaiter.waitFor(ConnectionState.connected); - assertEquals("Verify connected state is reached", ConnectionState.connected, ablyRealtime.connection.state); - - ablyRealtime.close(); - } catch (AblyException e) { - e.printStackTrace(); - fail(); - } - } - - /** - * RSA4c - * If authorize fails we should get the event for the failure - */ - @Test - public void reauth_failure_test() { - String testClientId = "testClientId"; - - try { - final ArrayList stateChangeHistory = new ArrayList<>(); - - /* init ably for token */ - ClientOptions optsForToken = createOptions(testVars.keys[0].keyStr); - final AblyRest ablyForToken = new AblyRest(optsForToken); - - /* get first token */ - Auth.TokenParams tokenParams = new Auth.TokenParams(); - tokenParams.clientId = testClientId; - Auth.TokenDetails firstToken = ablyForToken.auth.requestToken(tokenParams, null); - assertNotNull("Expected token value", firstToken.token); - - /* create ably realtime with tokenDetails and clientId */ - ClientOptions opts = createOptions(); - opts.clientId = testClientId; - opts.tokenDetails = firstToken; - AblyRealtime ablyRealtime = new AblyRealtime(opts); - - ablyRealtime.connection.on(new ConnectionStateListener() { - @Override - public void onConnectionStateChanged(ConnectionStateChange state) { - synchronized (stateChangeHistory) { - stateChangeHistory.add(state); - stateChangeHistory.notify(); - } - } - }); - - /* wait for connected state */ - synchronized (stateChangeHistory) { - while (ablyRealtime.connection.state != ConnectionState.connected) { - try { stateChangeHistory.wait(); } catch (InterruptedException e) {} - } - } - - assertEquals("Verify connected state is reached", ConnectionState.connected, ablyRealtime.connection.state); - - int stateChangeCount = stateChangeHistory.size(); - - /* fail getting the second token */ - /* reauthorize and fail */ - Auth.AuthOptions authOptions = new Auth.AuthOptions(); - authOptions.key = testVars.keys[0].keyStr; - authOptions.authUrl = "https://nonexistent-domain-abcdef.com"; - try { - ablyRealtime.auth.authorize(null, authOptions); - // should not succeed - fail(); - } - catch (AblyException e) { - // nothing - } - - /* wait for new entries in state change history */ - synchronized (stateChangeHistory) { - while (stateChangeHistory.size() <= stateChangeCount) { - try { stateChangeHistory.wait(); } catch (InterruptedException e) {} - } - - /* should stay in connected state, errorInfo should indicate authentication non-fatal error */ - ConnectionStateListener.ConnectionStateChange lastChange = stateChangeHistory.get(stateChangeHistory.size()-1); - assertEquals("Verify connection stayed in connected state", lastChange.current, ConnectionState.connected); - assertEquals("Verify authentication failure error code", lastChange.reason.code, 80019); - } - - ablyRealtime.close(); - } catch (AblyException e) { - e.printStackTrace(); - fail(); - } - } - - /** - * Verify that the server issues reauth message 30 seconds before token expiration time, authCallback is - * called to obtain new token and in-place re-authorization takes place with connection staying in connected - * state. Also tests if UPDATE event is delivered on the connection - * - * Test for RTN4h, RTC8a1, RTN24 features - */ - @Test - public void reauth_token_expire_inplace_reauth() { - try { - ClientOptions optsForToken = createOptions(testVars.keys[0].keyStr); - final AblyRest ablyForToken = new AblyRest(optsForToken); - /* Server will send reauth message 30 seconds before token expiration time i.e. in 4 seconds */ - TokenDetails tokenDetails = ablyForToken.auth.requestToken(new TokenParams() {{ ttl = 34000L; }}, null); - assertNotNull("Expected token value", tokenDetails.token); - - final boolean[] flags = new boolean[] { - false, /* authCallback is called */ - false, /* state other than connected is reached */ - false /* update event was delivered */ - }; - - /* create Ably realtime instance without key */ - ClientOptions opts = createOptions(); - opts.tokenDetails = tokenDetails; - opts.authCallback = new TokenCallback() { - /* implement callback, using Ably instance with key */ - @Override - public Object getTokenRequest(TokenParams params) throws AblyException { - synchronized (flags) { - flags[0] = true; - } - return ablyForToken.auth.requestToken(params, null); - } - }; - AblyRealtime ably = new AblyRealtime(opts); - - /* Test UPDATE event delivery */ - ably.connection.on(ConnectionEvent.update, new ConnectionStateListener() { - @Override - public void onConnectionStateChanged(ConnectionStateChange state) { - flags[2] = true; - } - }); - ably.connection.on(new ConnectionStateListener() { - @Override - public void onConnectionStateChanged(ConnectionStateChange state) { - if (state.previous == ConnectionState.connected && state.current != ConnectionState.connected) { - synchronized (flags) { - flags[1] = true; - flags.notify(); - } - } - } - }); - - synchronized (flags) { - try { - flags.wait(8000); - } catch (InterruptedException e) {} - } - - assertTrue("Verify token generation was called", flags[0]); - assertFalse("Verify connection didn't leave connected state", flags[1]); - assertTrue("Verify UPDATE event was delivered", flags[2]); - - ably.close(); - } catch (AblyException e) { - e.printStackTrace(); - fail("init0: Unexpected exception instantiating library"); - } - } - - /** - * Verify that the server issues reauth message 30 seconds before token expiration time of a token = - * derived from a locally-supplied key, authCallback is called to obtain new token and in-place - * re-authorization takes place with connection staying in connected - * state. Also tests if UPDATE event is delivered on the connection - * - * Test for RTN4h, RTC8a1, RTN24 features - */ - @Test - public void reauth_key_expire_inplace_reauth() { - try { - final boolean[] flags = new boolean[] { - false, /* state other than connected is reached */ - false /* update event was delivered */ - }; - - /* create Ably realtime instance with key */ - ClientOptions opts = createOptions(testVars.keys[0].keyStr); - opts.clientId = "testClientId"; - opts.useTokenAuth = true; - opts.defaultTokenParams.ttl = 34000L; - AblyRealtime ably = new AblyRealtime(opts); - - /* Test UPDATE event delivery */ - ably.connection.on(ConnectionEvent.update, new ConnectionStateListener() { - @Override - public void onConnectionStateChanged(ConnectionStateChange state) { - flags[1] = true; - } - }); - ably.connection.on(new ConnectionStateListener() { - @Override - public void onConnectionStateChanged(ConnectionStateChange state) { - if (state.previous == ConnectionState.connected && state.current != ConnectionState.connected) { - synchronized (flags) { - flags[0] = true; - flags.notify(); - } - } - } - }); - - synchronized (flags) { - try { - flags.wait(8000); - } catch (InterruptedException e) {} - } - - assertFalse("Verify connection didn't leave connected state", flags[0]); - assertTrue("Verify UPDATE event was delivered", flags[1]); - - ably.close(); - } catch (AblyException e) { - e.printStackTrace(); - fail("init0: Unexpected exception instantiating library"); - } - } + @Rule + public Timeout testTimeout = Timeout.seconds(90); + + /** + * RTC8a: In-place reauthorization on a connected connection. + * RTC8a1: A test should exist that performs an upgrade of + * capabilities without any loss of continuity or connectivity + * during the upgrade process. + */ + @Test + public void reauth_tokenDetails() { + String wrongChannel = "wrongchannel"; + String rightChannel = "rightchannel"; + String testClientId = "testClientId"; + + try { + /* init ably for token */ + ClientOptions optsForToken = createOptions(testVars.keys[0].keyStr); + final AblyRest ablyForToken = new AblyRest(optsForToken); + + /* get first token */ + Auth.TokenParams tokenParams = new Auth.TokenParams(); + Capability capability = new Capability(); + capability.addResource(wrongChannel, "*"); + tokenParams.capability = capability.toString(); + tokenParams.clientId = testClientId; + + Auth.TokenDetails firstToken = ablyForToken.auth.requestToken(tokenParams, null); + assertNotNull("Expected token value", firstToken.token); + + /* create ably realtime with tokenDetails and clientId */ + ClientOptions opts = createOptions(); + opts.clientId = testClientId; + opts.tokenDetails = firstToken; + AblyRealtime ablyRealtime = new AblyRealtime(opts); + + /* wait for connected state */ + Helpers.ConnectionWaiter connectionWaiter = new Helpers.ConnectionWaiter(ablyRealtime.connection); + connectionWaiter.waitFor(ConnectionState.connected); + assertEquals("Verify connected state is reached", ConnectionState.connected, ablyRealtime.connection.state); + + /* create a channel and check can't attach */ + Channel channel = ablyRealtime.channels.get(rightChannel); + Helpers.CompletionWaiter waiter = new Helpers.CompletionWaiter(); + channel.attach(waiter); + ErrorInfo error = waiter.waitFor(); + assertNotNull("Expected error", error); + assertEquals("Verify error code 40160 (channel is denied access)", error.code, 40160); + + /* get second token */ + tokenParams = new Auth.TokenParams(); + capability = new Capability(); + capability.addResource(wrongChannel, "*"); + capability.addResource(rightChannel, "*"); + tokenParams.capability = capability.toString(); + tokenParams.clientId = testClientId; + + Auth.TokenDetails secondToken = ablyForToken.auth.requestToken(tokenParams, null); + assertNotNull("Expected token value", secondToken.token); + + /* reauthorize */ + connectionWaiter.reset(); + Auth.AuthOptions authOptions = new Auth.AuthOptions(); + authOptions.key = testVars.keys[0].keyStr; + authOptions.tokenDetails = secondToken; + Auth.TokenDetails reauthTokenDetails = ablyRealtime.auth.authorize(null, authOptions); + assertNotNull("Expected token value", reauthTokenDetails.token); + + /* re-attach to the channel */ + waiter = new Helpers.CompletionWaiter(); + channel.attach(waiter); + + /* verify onSuccess callback gets called */ + waiter.waitFor(); + assertThat(waiter.success, is(true)); + /* Verify that the connection never disconnected (0.9 in-place authorization) */ + assertTrue("Expected in-place authorization", connectionWaiter.getCount(ConnectionState.connecting) == 0); + + ablyRealtime.close(); + } catch (AblyException e) { + e.printStackTrace(); + fail(); + } + } + + /** + /* RTC8a1: Another test should exist where the capabilities are + * downgraded resulting in Ably sending an ERROR ProtocolMessage + * with a channel property, causing the channel to enter the FAILED + * state. That test must assert that the channel becomes failed + * soon after the token update and the reason is included in the + * channel state change event. + */ + @Test + public void reauth_downgrade() { + String wrongChannel = "wrongchannel"; + String rightChannel = "rightchannel"; + String testClientId = "testClientId"; + + try { + /* init ably for token */ + ClientOptions optsForToken = createOptions(testVars.keys[0].keyStr); + final AblyRest ablyForToken = new AblyRest(optsForToken); + + /* get first (good) token */ + Auth.TokenParams tokenParams = new Auth.TokenParams(); + Capability capability = new Capability(); + capability.addResource(wrongChannel, "*"); + capability.addResource(rightChannel, "*"); + tokenParams.capability = capability.toString(); + tokenParams.clientId = testClientId; + Auth.TokenDetails firstToken = ablyForToken.auth.requestToken(tokenParams, null); + assertNotNull("Expected token value", firstToken.token); + + /* create ably realtime with tokenDetails and clientId */ + ClientOptions opts = createOptions(); + opts.clientId = testClientId; + opts.tokenDetails = firstToken; + AblyRealtime ablyRealtime = new AblyRealtime(opts); + + /* wait for connected state */ + Helpers.ConnectionWaiter connectionWaiter = new Helpers.ConnectionWaiter(ablyRealtime.connection); + connectionWaiter.waitFor(ConnectionState.connected); + assertEquals("Verify connected state is reached", ConnectionState.connected, ablyRealtime.connection.state); + + /* create a channel and check attached */ + Channel channel = ablyRealtime.channels.get(rightChannel); + Helpers.ChannelWaiter waiter = new Helpers.ChannelWaiter(channel); + channel.attach(); + /* verify onSuccess callback gets called */ + waiter.waitFor(ChannelState.attached); + assertEquals("Verify attached state reached", channel.state, ChannelState.attached); + + /* get second (bad) token */ + tokenParams = new Auth.TokenParams(); + capability = new Capability(); + capability.addResource(wrongChannel, "*"); + tokenParams.capability = capability.toString(); + tokenParams.clientId = testClientId; + Auth.TokenDetails secondToken = ablyForToken.auth.requestToken(tokenParams, null); + assertNotNull("Expected token value", secondToken.token); + + /* reauthorize */ + connectionWaiter.reset(); + Auth.AuthOptions authOptions = new Auth.AuthOptions(); + authOptions.tokenDetails = secondToken; + Auth.TokenDetails reauthTokenDetails = ablyRealtime.auth.authorize(null, authOptions); + assertNotNull("Expected token value", reauthTokenDetails.token); + + /* Check that the channel moves to failed state within 2s, and that + * we get the expected error code. */ + long before = System.currentTimeMillis(); + ErrorInfo err = waiter.waitFor(ChannelState.failed); + assertEquals("Verify failed state reached", channel.state, ChannelState.failed); + assertEquals("Verify error code", err.code, 40160); + assertTrue("Expected channel to fail quickly", System.currentTimeMillis() - before < 2000); + + ablyRealtime.close(); + } catch (AblyException e) { + e.printStackTrace(); + fail(); + } + } + + /** + /* RTC8a2: If the authentication token change fails, then Ably will send an + * ERROR ProtocolMessage triggering the connection to transition to the + * FAILED state. A test should exist for a token change that fails (such as + * sending a new token with an incompatible clientId) + */ + @Test + public void reauth_fail() { + String rightChannel = "rightchannel"; + String testClientId = "testClientId"; + String badClientId = "badClientId"; + + try { + /* init ably for token */ + ClientOptions optsForToken = createOptions(testVars.keys[0].keyStr); + final AblyRest ablyForToken = new AblyRest(optsForToken); + + /* get first (good) token */ + Auth.TokenParams tokenParams = new Auth.TokenParams(); + Capability capability = new Capability(); + capability.addResource(rightChannel, "*"); + tokenParams.capability = capability.toString(); + tokenParams.clientId = testClientId; + Auth.TokenDetails firstToken = ablyForToken.auth.requestToken(tokenParams, null); + assertNotNull("Expected token value", firstToken.token); + + /* create ably realtime with tokenDetails and clientId */ + ClientOptions opts = createOptions(); + opts.clientId = testClientId; + opts.tokenDetails = firstToken; + AblyRealtime ablyRealtime = new AblyRealtime(opts); + + /* wait for connected state */ + Helpers.ConnectionWaiter connectionWaiter = new Helpers.ConnectionWaiter(ablyRealtime.connection); + connectionWaiter.waitFor(ConnectionState.connected); + assertEquals("Verify connected state is reached", ConnectionState.connected, ablyRealtime.connection.state); + + /* create a channel and check attached */ + Channel channel = ablyRealtime.channels.get(rightChannel); + Helpers.ChannelWaiter waiter = new Helpers.ChannelWaiter(channel); + channel.attach(); + /* verify onSuccess callback gets called */ + waiter.waitFor(ChannelState.attached); + assertEquals("Verify attached state reached", channel.state, ChannelState.attached); + + /* get second (bad) token */ + tokenParams.clientId = badClientId; + Auth.TokenDetails secondToken = ablyForToken.auth.requestToken(tokenParams, null); + /* revert client id in token details, otherwise it will be blocked by the client library */ + secondToken.clientId = testClientId; + assertNotNull("Expected token value", secondToken.token); + + /* Reauthorize. We expect this to throw an exception from the + * mismatched client id and end up in failed state. */ + connectionWaiter.reset(); + Auth.AuthOptions authOptions = new Auth.AuthOptions(); + authOptions.tokenDetails = secondToken; + try { + Auth.TokenDetails reauthTokenDetails = ablyRealtime.auth.authorize(null, authOptions); + assertFalse("Expecting exception", true); + System.out.println("Authorize failed to throw an exception"); + } catch (AblyException e) { + assertEquals("Expecting failed", ConnectionState.failed, ablyRealtime.connection.state); + System.out.println("Got failed connection"); + } + + /** + * RTC8c: If the connection is in the DISCONNECTED, SUSPENDED, FAILED, or + * CLOSED state when auth#authorize is called, after obtaining a token the + * library should move to the CONNECTING state and initiate a connection + * attempt using the new token, and RTC8b1 applies. + */ + + /* Reauthorize with good token. We expect this to connect. */ + connectionWaiter.reset(); + authOptions = new Auth.AuthOptions(); + authOptions.tokenDetails = firstToken; + Auth.TokenDetails reauthTokenDetails = ablyRealtime.auth.authorize(null, authOptions); + assertNotNull("Expected token value", reauthTokenDetails.token); + connectionWaiter.waitFor(ConnectionState.connected); + assertEquals("Verify connected state is reached", ConnectionState.connected, ablyRealtime.connection.state); + + ablyRealtime.close(); + } catch (AblyException e) { + e.printStackTrace(); + fail(); + } + } + + /** + * RSA4c + * If authorize fails we should get the event for the failure + */ + @Test + public void reauth_failure_test() { + String testClientId = "testClientId"; + + try { + final ArrayList stateChangeHistory = new ArrayList<>(); + + /* init ably for token */ + ClientOptions optsForToken = createOptions(testVars.keys[0].keyStr); + final AblyRest ablyForToken = new AblyRest(optsForToken); + + /* get first token */ + Auth.TokenParams tokenParams = new Auth.TokenParams(); + tokenParams.clientId = testClientId; + Auth.TokenDetails firstToken = ablyForToken.auth.requestToken(tokenParams, null); + assertNotNull("Expected token value", firstToken.token); + + /* create ably realtime with tokenDetails and clientId */ + ClientOptions opts = createOptions(); + opts.clientId = testClientId; + opts.tokenDetails = firstToken; + AblyRealtime ablyRealtime = new AblyRealtime(opts); + + ablyRealtime.connection.on(new ConnectionStateListener() { + @Override + public void onConnectionStateChanged(ConnectionStateChange state) { + synchronized (stateChangeHistory) { + stateChangeHistory.add(state); + stateChangeHistory.notify(); + } + } + }); + + /* wait for connected state */ + synchronized (stateChangeHistory) { + while (ablyRealtime.connection.state != ConnectionState.connected) { + try { stateChangeHistory.wait(); } catch (InterruptedException e) {} + } + } + + assertEquals("Verify connected state is reached", ConnectionState.connected, ablyRealtime.connection.state); + + int stateChangeCount = stateChangeHistory.size(); + + /* fail getting the second token */ + /* reauthorize and fail */ + Auth.AuthOptions authOptions = new Auth.AuthOptions(); + authOptions.key = testVars.keys[0].keyStr; + authOptions.authUrl = "https://nonexistent-domain-abcdef.com"; + try { + ablyRealtime.auth.authorize(null, authOptions); + // should not succeed + fail(); + } + catch (AblyException e) { + // nothing + } + + /* wait for new entries in state change history */ + synchronized (stateChangeHistory) { + while (stateChangeHistory.size() <= stateChangeCount) { + try { stateChangeHistory.wait(); } catch (InterruptedException e) {} + } + + /* should stay in connected state, errorInfo should indicate authentication non-fatal error */ + ConnectionStateListener.ConnectionStateChange lastChange = stateChangeHistory.get(stateChangeHistory.size()-1); + assertEquals("Verify connection stayed in connected state", lastChange.current, ConnectionState.connected); + assertEquals("Verify authentication failure error code", lastChange.reason.code, 80019); + } + + ablyRealtime.close(); + } catch (AblyException e) { + e.printStackTrace(); + fail(); + } + } + + /** + * Verify that the server issues reauth message 30 seconds before token expiration time, authCallback is + * called to obtain new token and in-place re-authorization takes place with connection staying in connected + * state. Also tests if UPDATE event is delivered on the connection + * + * Test for RTN4h, RTC8a1, RTN24 features + */ + @Test + public void reauth_token_expire_inplace_reauth() { + try { + ClientOptions optsForToken = createOptions(testVars.keys[0].keyStr); + final AblyRest ablyForToken = new AblyRest(optsForToken); + /* Server will send reauth message 30 seconds before token expiration time i.e. in 4 seconds */ + TokenDetails tokenDetails = ablyForToken.auth.requestToken(new TokenParams() {{ ttl = 34000L; }}, null); + assertNotNull("Expected token value", tokenDetails.token); + + final boolean[] flags = new boolean[] { + false, /* authCallback is called */ + false, /* state other than connected is reached */ + false /* update event was delivered */ + }; + + /* create Ably realtime instance without key */ + ClientOptions opts = createOptions(); + opts.tokenDetails = tokenDetails; + opts.authCallback = new TokenCallback() { + /* implement callback, using Ably instance with key */ + @Override + public Object getTokenRequest(TokenParams params) throws AblyException { + synchronized (flags) { + flags[0] = true; + } + return ablyForToken.auth.requestToken(params, null); + } + }; + AblyRealtime ably = new AblyRealtime(opts); + + /* Test UPDATE event delivery */ + ably.connection.on(ConnectionEvent.update, new ConnectionStateListener() { + @Override + public void onConnectionStateChanged(ConnectionStateChange state) { + flags[2] = true; + } + }); + ably.connection.on(new ConnectionStateListener() { + @Override + public void onConnectionStateChanged(ConnectionStateChange state) { + if (state.previous == ConnectionState.connected && state.current != ConnectionState.connected) { + synchronized (flags) { + flags[1] = true; + flags.notify(); + } + } + } + }); + + synchronized (flags) { + try { + flags.wait(8000); + } catch (InterruptedException e) {} + } + + assertTrue("Verify token generation was called", flags[0]); + assertFalse("Verify connection didn't leave connected state", flags[1]); + assertTrue("Verify UPDATE event was delivered", flags[2]); + + ably.close(); + } catch (AblyException e) { + e.printStackTrace(); + fail("init0: Unexpected exception instantiating library"); + } + } + + /** + * Verify that the server issues reauth message 30 seconds before token expiration time of a token = + * derived from a locally-supplied key, authCallback is called to obtain new token and in-place + * re-authorization takes place with connection staying in connected + * state. Also tests if UPDATE event is delivered on the connection + * + * Test for RTN4h, RTC8a1, RTN24 features + */ + @Test + public void reauth_key_expire_inplace_reauth() { + try { + final boolean[] flags = new boolean[] { + false, /* state other than connected is reached */ + false /* update event was delivered */ + }; + + /* create Ably realtime instance with key */ + ClientOptions opts = createOptions(testVars.keys[0].keyStr); + opts.clientId = "testClientId"; + opts.useTokenAuth = true; + opts.defaultTokenParams.ttl = 34000L; + AblyRealtime ably = new AblyRealtime(opts); + + /* Test UPDATE event delivery */ + ably.connection.on(ConnectionEvent.update, new ConnectionStateListener() { + @Override + public void onConnectionStateChanged(ConnectionStateChange state) { + flags[1] = true; + } + }); + ably.connection.on(new ConnectionStateListener() { + @Override + public void onConnectionStateChanged(ConnectionStateChange state) { + if (state.previous == ConnectionState.connected && state.current != ConnectionState.connected) { + synchronized (flags) { + flags[0] = true; + flags.notify(); + } + } + } + }); + + synchronized (flags) { + try { + flags.wait(8000); + } catch (InterruptedException e) {} + } + + assertFalse("Verify connection didn't leave connected state", flags[0]); + assertTrue("Verify UPDATE event was delivered", flags[1]); + + ably.close(); + } catch (AblyException e) { + e.printStackTrace(); + fail("init0: Unexpected exception instantiating library"); + } + } } diff --git a/lib/src/test/java/io/ably/lib/test/realtime/RealtimeRecoverTest.java b/lib/src/test/java/io/ably/lib/test/realtime/RealtimeRecoverTest.java index ad881c8b2..c8a97c478 100644 --- a/lib/src/test/java/io/ably/lib/test/realtime/RealtimeRecoverTest.java +++ b/lib/src/test/java/io/ably/lib/test/realtime/RealtimeRecoverTest.java @@ -28,393 +28,393 @@ public class RealtimeRecoverTest extends ParameterizedTest { - private static final String TAG = RealtimeRecoverTest.class.getName(); - - /** - * Connect to the service using two library instances to set - * up separate send and recv connections. - * Send on one connection while the other is disconnected; - * Open a third connection to inherit from the disconnected connection - * and explicitly wait for the connection before re-attaching the channel. - * verify that the messages sent whilst disconnected are delivered - * on recover - * Spec: RTN16a,RTN16b - */ - @Test - public void recover_disconnected() { - AblyRealtime ablyRx = null, ablyTx = null, ablyRxRecover = null; - String channelName = "recover_disconnected"; - int messageCount = 5; - long delay = 200; - try { - ClientOptions opts = createOptions(testVars.keys[0].keyStr); - ablyRx = new AblyRealtime(opts); - ablyTx = new AblyRealtime(opts); - - /* create and attach channel to send on */ - final Channel channelTx = ablyTx.channels.get(channelName); - channelTx.attach(); - (new ChannelWaiter(channelTx)).waitFor(ChannelState.attached); - assertEquals("Verify attached state reached for tx", channelTx.state, ChannelState.attached); - - /* create and attach channel to recv on */ - final Channel channelRx = ablyRx.channels.get(channelName); - channelRx.attach(); - (new ChannelWaiter(channelRx)).waitFor(ChannelState.attached); - assertEquals("Verify attached state reached for rx", channelRx.state, ChannelState.attached); - - /* subscribe */ - MessageWaiter messageWaiter = new MessageWaiter(channelRx); - - /* publish first messages to the channel */ - CompletionSet msgComplete1 = new CompletionSet(); - for(int i = 0; i < messageCount; i++) { - channelTx.publish("test_event", "Test message (recover_disconnected) " + i, msgComplete1.add()); - try { Thread.sleep(delay); } catch(InterruptedException e){} - } - - /* wait for the publish callback to be called */ - ErrorInfo[] errors = msgComplete1.waitFor(); - assertTrue("Verify success from all message callbacks", errors.length == 0); - - /* wait for the subscription callback to be called */ - messageWaiter.waitFor(messageCount); - assertEquals("Verify message subscriptions all called", messageWaiter.receivedMessages.size(), messageCount); - - /* disconnect the rx connection, without closing; - * NOTE this depends on knowledge of the internal structure - * of the library, to simulate a dropped transport without - * causing the connection itself to be disposed */ - String recoverConnectionKey = ablyRx.connection.recoveryKey; - ablyRx.connection.connectionManager.requestState(ConnectionState.failed); - - /* wait */ - try { Thread.sleep(2000L); } catch(InterruptedException e) {} - - /* publish next messages to the channel */ - CompletionSet msgComplete2 = new CompletionSet(); - for(int i = 0; i < messageCount; i++) { - channelTx.publish("test_event", "Test message (recover_disconnected) " + i, msgComplete2.add()); - try { Thread.sleep(delay); } catch(InterruptedException e){} - } - - /* wait for the publish callback to be called */ - errors = msgComplete2.waitFor(); - assertTrue("Verify success from all message callbacks", errors.length == 0); - - /* establish a new rx connection with recover string, and wait for connection */ - ClientOptions recoverOpts = createOptions(testVars.keys[0].keyStr); - recoverOpts.recover = recoverConnectionKey; - ablyRxRecover = new AblyRealtime(recoverOpts); - (new ConnectionWaiter(ablyRxRecover.connection)).waitFor(ConnectionState.connected); - - /* subscribe to channel */ - final Channel channelRxRecover = ablyRxRecover.channels.get(channelName); - MessageWaiter messageWaiterRecover = new MessageWaiter(channelRxRecover); - - /* wait for the subscription callback to be called */ - messageWaiterRecover.waitFor(messageCount); - assertEquals("Verify message subscriptions all called after recovery", messageWaiterRecover.receivedMessages.size(), messageCount); - - } catch (AblyException e) { - e.printStackTrace(); - fail("init0: Unexpected exception instantiating library"); - } finally { - if(ablyTx != null) - ablyTx.close(); - if(ablyRx != null) - ablyRx.close(); - if(ablyRxRecover != null) - ablyRxRecover.close(); - } - } - - /** - * Connect to the service using two library instances to set - * up separate send and recv connections. - * Send on one connection while the other is disconnected; - * Open a third connection to inherit from the disconnected connection - * without an explicit wait for connection. - * verify that the messages sent whilst disconnected are delivered - * on recover - * Spec: RTN16a,RTN16b - */ - @Test - public void recover_implicit_connect() { - AblyRealtime ablyRx = null, ablyTx = null, ablyRxRecover = null; - String channelName = "recover_implicit_connect"; - int messageCount = 5; - long delay = 200; - try { - ClientOptions opts = createOptions(testVars.keys[0].keyStr); - ablyRx = new AblyRealtime(opts); - ablyTx = new AblyRealtime(opts); - - /* create and attach channel to send on */ - final Channel channelTx = ablyTx.channels.get(channelName); - channelTx.attach(); - (new ChannelWaiter(channelTx)).waitFor(ChannelState.attached); - assertEquals("Verify attached state reached for tx", channelTx.state, ChannelState.attached); - - /* create and attach channel to recv on */ - final Channel channelRx = ablyRx.channels.get(channelName); - channelRx.attach(); - (new ChannelWaiter(channelRx)).waitFor(ChannelState.attached); - assertEquals("Verify attached state reached for rx", channelRx.state, ChannelState.attached); - - /* subscribe */ - MessageWaiter messageWaiter = new MessageWaiter(channelRx); - - /* publish first messages to the channel */ - CompletionSet msgComplete1 = new CompletionSet(); - for(int i = 0; i < messageCount; i++) { - channelTx.publish("test_event", "Test message (recover_implicit_connect) " + i, msgComplete1.add()); - try { Thread.sleep(delay); } catch(InterruptedException e){} - } - - /* wait for the publish callback to be called */ - ErrorInfo[] errors = msgComplete1.waitFor(); - assertTrue("Verify success from all message callbacks", errors.length == 0); - - /* wait for the subscription callback to be called */ - messageWaiter.waitFor(messageCount); - assertEquals("Verify message subscriptions all called", messageWaiter.receivedMessages.size(), messageCount); - - /* disconnect the rx connection, without closing; - * NOTE this depends on knowledge of the internal structure - * of the library, to simulate a dropped transport without - * causing the connection itself to be disposed */ - String recoverConnectionKey = ablyRx.connection.recoveryKey; - ablyRx.connection.connectionManager.requestState(ConnectionState.failed); - - /* wait */ - try { Thread.sleep(2000L); } catch(InterruptedException e) {} - - /* publish next messages to the channel */ - CompletionSet msgComplete2 = new CompletionSet(); - for(int i = 0; i < messageCount; i++) { - channelTx.publish("test_event", "Test message (recover_implicit_connect) " + i, msgComplete2.add()); - try { Thread.sleep(delay); } catch(InterruptedException e){} - } - - /* wait for the publish callback to be called */ - errors = msgComplete2.waitFor(); - assertTrue("Verify success from all message callbacks", errors.length == 0); - - /* establish a new rx connection with recover string, and wait for connection */ - ClientOptions recoverOpts = createOptions(testVars.keys[0].keyStr); - recoverOpts.recover = recoverConnectionKey; - ablyRxRecover = new AblyRealtime(recoverOpts); - - /* subscribe to channel */ - final Channel channelRxRecover = ablyRxRecover.channels.get(channelName); - MessageWaiter messageWaiterRecover = new MessageWaiter(channelRxRecover); - - /* wait for the subscription callback to be called */ - messageWaiterRecover.waitFor(messageCount); - assertEquals("Verify message subscriptions all called after recovery", messageWaiterRecover.receivedMessages.size(), messageCount); - - } catch (AblyException e) { - e.printStackTrace(); - fail("init0: Unexpected exception instantiating library"); - } finally { - if(ablyTx != null) - ablyTx.close(); - if(ablyRx != null) - ablyRx.close(); - if(ablyRxRecover != null) - ablyRxRecover.close(); - } - } - - /** - * Connect to the service using two library instances to set - * up separate send and recv connections. - * Disconnect+suspend and then reconnect the send connection; verify that - * each subsequent publish causes a CompletionListener call. - */ - @Test - public void recover_verify_publish() { - AblyRealtime ablyRx = null, ablyTx = null; - String channelName = "recover_verify_publish"; - int messageCount = 5; - long delay = 200; - try { - ClientOptions opts = createOptions(testVars.keys[0].keyStr); - ablyRx = new AblyRealtime(opts); - ablyTx = new AblyRealtime(opts); - - /* create and attach channel to send on */ - final Channel channelTx = ablyTx.channels.get(channelName); - channelTx.attach(); - (new ChannelWaiter(channelTx)).waitFor(ChannelState.attached); - assertEquals("Verify attached state reached for tx", channelTx.state, ChannelState.attached); - - /* create and attach channel to recv on */ - final Channel channelRx = ablyRx.channels.get(channelName); - channelRx.attach(); - (new ChannelWaiter(channelRx)).waitFor(ChannelState.attached); - assertEquals("Verify attached state reached for rx", channelRx.state, ChannelState.attached); - - /* subscribe */ - MessageWaiter messageWaiter = new MessageWaiter(channelRx); - - /* publish first messages to the channel */ - CompletionSet msgComplete1 = new CompletionSet(); - for(int i = 0; i < messageCount; i++) { - channelTx.publish("test_event", "Test message (resume_simple) " + i, msgComplete1.add()); - try { Thread.sleep(delay); } catch(InterruptedException e){} - } - - /* wait for the publish callback to be called */ - ErrorInfo[] errors = msgComplete1.waitFor(); - assertTrue("Verify success from all message callbacks", errors.length == 0); - - /* wait for the subscription callback to be called */ - messageWaiter.waitFor(messageCount); - assertEquals("Verify message subscriptions all called", messageWaiter.receivedMessages.size(), messageCount); - messageWaiter.reset(); - - /* suspend the tx connection, without closing; - * NOTE this depends on knowledge of the internal structure - * of the library, to simulate a dropped transport without - * causing the connection itself to be disposed */ - System.out.println("*** about to suspend tx connection"); - ablyTx.connection.connectionManager.requestState(ConnectionState.suspended); - - /* wait */ - try { Thread.sleep(2000L); } catch(InterruptedException e) {} - - /* reconnect the tx connection */ - System.out.println("*** about to reconnect tx connection"); - ablyTx.connection.connect(); - (new ConnectionWaiter(ablyTx.connection)).waitFor(ConnectionState.connected); - - /* need to manually attach the tx channel as connection was suspended */ - System.out.println("*** tx connection now connected. About to recover channel"); - channelTx.attach(); - (new ChannelWaiter(channelTx)).waitFor(ChannelState.attached); - assertEquals("Verify attached state reached for tx again", channelTx.state, ChannelState.attached); - System.out.println("*** tx channel now attached. About to publish"); - - /* publish further messages to the channel */ - CompletionSet msgComplete2 = new CompletionSet(); - for(int i = 0; i < messageCount; i++) { - channelTx.publish("test_event", "Test message (resume_simple) " + i, msgComplete2.add()); - try { Thread.sleep(delay); } catch(InterruptedException e){} - } - - /* wait for the publish callback to be called. This never finishes if - * https://github.com/ably/ably-java/issues/170 - * is not fixed. */ - System.out.println("*** published. About to wait for callbacks"); - errors = msgComplete2.waitFor(); - System.out.println("*** done"); - assertTrue("Verify success from all message callbacks", errors.length == 0); - - /* wait for the subscription callback to be called */ - messageWaiter.waitFor(messageCount); - assertEquals("Verify message subscriptions all called after reconnection", messageWaiter.receivedMessages.size(), messageCount); - - } catch (AblyException e) { - e.printStackTrace(); - fail("init0: Unexpected exception instantiating library"); - } finally { - if(ablyTx != null) - ablyTx.close(); - if(ablyRx != null) - ablyRx.close(); - } - } - - /** - * Test if ConnectionManager behaves correctly after exception thrown from ITransport.send() - */ - @Test - public void recover_transport_send_exception() { - AblyRealtime ably = null; - final String channelName = "recover_transport_send_exception"; - try { - MockWebsocketFactory mockTransport = new MockWebsocketFactory(); - mockTransport.throwOnSend = false; - mockTransport.exceptionsThrown = 0; - - DebugOptions opts = new DebugOptions(testVars.keys[0].keyStr); - fillInOptions(opts); - opts.transportFactory = mockTransport; - - opts.autoConnect = false; - ably = new AblyRealtime(opts); - ConnectionWaiter connWaiter = new ConnectionWaiter(ably.connection); - ably.connection.connect(); - connWaiter.waitFor(ConnectionState.connected); - - Channel channel = ably.channels.get(channelName); - channel.attach(); - new ChannelWaiter(channel).waitFor(ChannelState.attached); - - ably.connection.connectionManager.requestState(ConnectionState.disconnected); - connWaiter.waitFor(ConnectionState.connecting, 2); - - /* Start throwing exceptions in the send() */ - mockTransport.throwOnSend = true; - for (int i=0; i<5; i++) { - try { - channel.publish("name", "data"); - } catch (Throwable e) { - } - } - - try { - Thread.sleep(1000); - } catch (InterruptedException e) {} - - /* Stop exceptions */ - mockTransport.throwOnSend = false; - channel.publish("name2", "data2"); - - try { - Thread.sleep(500); - } catch (InterruptedException e) {} - - /* It is hard to predict how many exception would be thrown but it shouldn't be hundreds */ - assertThat("", mockTransport.exceptionsThrown, is(lessThan(10))); - - } catch (AblyException e) { - e.printStackTrace(); - fail("Unexpected exception"); - } finally { - if (ably != null) - ably.close(); - } - } - - public static class MockWebsocketFactory implements ITransport.Factory { - boolean throwOnSend = false; - int exceptionsThrown = 0; - - /* - * Special transport class that allows throwing exceptions from send() - */ - private class MockWebsocketTransport extends WebSocketTransport { - private MockWebsocketTransport(TransportParams transportParams, ConnectionManager connectionManager) { - super(transportParams, connectionManager); - } - - @Override - public void send(ProtocolMessage msg) throws AblyException { - if (throwOnSend) { - exceptionsThrown++; - throw AblyException.fromErrorInfo(new ErrorInfo("TestException", 40000)); - } else { - super.send(msg); - } - } - } - - @Override - public ITransport getTransport(ITransport.TransportParams transportParams, ConnectionManager connectionManager) { - return new MockWebsocketTransport(transportParams, connectionManager); - } - } + private static final String TAG = RealtimeRecoverTest.class.getName(); + + /** + * Connect to the service using two library instances to set + * up separate send and recv connections. + * Send on one connection while the other is disconnected; + * Open a third connection to inherit from the disconnected connection + * and explicitly wait for the connection before re-attaching the channel. + * verify that the messages sent whilst disconnected are delivered + * on recover + * Spec: RTN16a,RTN16b + */ + @Test + public void recover_disconnected() { + AblyRealtime ablyRx = null, ablyTx = null, ablyRxRecover = null; + String channelName = "recover_disconnected"; + int messageCount = 5; + long delay = 200; + try { + ClientOptions opts = createOptions(testVars.keys[0].keyStr); + ablyRx = new AblyRealtime(opts); + ablyTx = new AblyRealtime(opts); + + /* create and attach channel to send on */ + final Channel channelTx = ablyTx.channels.get(channelName); + channelTx.attach(); + (new ChannelWaiter(channelTx)).waitFor(ChannelState.attached); + assertEquals("Verify attached state reached for tx", channelTx.state, ChannelState.attached); + + /* create and attach channel to recv on */ + final Channel channelRx = ablyRx.channels.get(channelName); + channelRx.attach(); + (new ChannelWaiter(channelRx)).waitFor(ChannelState.attached); + assertEquals("Verify attached state reached for rx", channelRx.state, ChannelState.attached); + + /* subscribe */ + MessageWaiter messageWaiter = new MessageWaiter(channelRx); + + /* publish first messages to the channel */ + CompletionSet msgComplete1 = new CompletionSet(); + for(int i = 0; i < messageCount; i++) { + channelTx.publish("test_event", "Test message (recover_disconnected) " + i, msgComplete1.add()); + try { Thread.sleep(delay); } catch(InterruptedException e){} + } + + /* wait for the publish callback to be called */ + ErrorInfo[] errors = msgComplete1.waitFor(); + assertTrue("Verify success from all message callbacks", errors.length == 0); + + /* wait for the subscription callback to be called */ + messageWaiter.waitFor(messageCount); + assertEquals("Verify message subscriptions all called", messageWaiter.receivedMessages.size(), messageCount); + + /* disconnect the rx connection, without closing; + * NOTE this depends on knowledge of the internal structure + * of the library, to simulate a dropped transport without + * causing the connection itself to be disposed */ + String recoverConnectionKey = ablyRx.connection.recoveryKey; + ablyRx.connection.connectionManager.requestState(ConnectionState.failed); + + /* wait */ + try { Thread.sleep(2000L); } catch(InterruptedException e) {} + + /* publish next messages to the channel */ + CompletionSet msgComplete2 = new CompletionSet(); + for(int i = 0; i < messageCount; i++) { + channelTx.publish("test_event", "Test message (recover_disconnected) " + i, msgComplete2.add()); + try { Thread.sleep(delay); } catch(InterruptedException e){} + } + + /* wait for the publish callback to be called */ + errors = msgComplete2.waitFor(); + assertTrue("Verify success from all message callbacks", errors.length == 0); + + /* establish a new rx connection with recover string, and wait for connection */ + ClientOptions recoverOpts = createOptions(testVars.keys[0].keyStr); + recoverOpts.recover = recoverConnectionKey; + ablyRxRecover = new AblyRealtime(recoverOpts); + (new ConnectionWaiter(ablyRxRecover.connection)).waitFor(ConnectionState.connected); + + /* subscribe to channel */ + final Channel channelRxRecover = ablyRxRecover.channels.get(channelName); + MessageWaiter messageWaiterRecover = new MessageWaiter(channelRxRecover); + + /* wait for the subscription callback to be called */ + messageWaiterRecover.waitFor(messageCount); + assertEquals("Verify message subscriptions all called after recovery", messageWaiterRecover.receivedMessages.size(), messageCount); + + } catch (AblyException e) { + e.printStackTrace(); + fail("init0: Unexpected exception instantiating library"); + } finally { + if(ablyTx != null) + ablyTx.close(); + if(ablyRx != null) + ablyRx.close(); + if(ablyRxRecover != null) + ablyRxRecover.close(); + } + } + + /** + * Connect to the service using two library instances to set + * up separate send and recv connections. + * Send on one connection while the other is disconnected; + * Open a third connection to inherit from the disconnected connection + * without an explicit wait for connection. + * verify that the messages sent whilst disconnected are delivered + * on recover + * Spec: RTN16a,RTN16b + */ + @Test + public void recover_implicit_connect() { + AblyRealtime ablyRx = null, ablyTx = null, ablyRxRecover = null; + String channelName = "recover_implicit_connect"; + int messageCount = 5; + long delay = 200; + try { + ClientOptions opts = createOptions(testVars.keys[0].keyStr); + ablyRx = new AblyRealtime(opts); + ablyTx = new AblyRealtime(opts); + + /* create and attach channel to send on */ + final Channel channelTx = ablyTx.channels.get(channelName); + channelTx.attach(); + (new ChannelWaiter(channelTx)).waitFor(ChannelState.attached); + assertEquals("Verify attached state reached for tx", channelTx.state, ChannelState.attached); + + /* create and attach channel to recv on */ + final Channel channelRx = ablyRx.channels.get(channelName); + channelRx.attach(); + (new ChannelWaiter(channelRx)).waitFor(ChannelState.attached); + assertEquals("Verify attached state reached for rx", channelRx.state, ChannelState.attached); + + /* subscribe */ + MessageWaiter messageWaiter = new MessageWaiter(channelRx); + + /* publish first messages to the channel */ + CompletionSet msgComplete1 = new CompletionSet(); + for(int i = 0; i < messageCount; i++) { + channelTx.publish("test_event", "Test message (recover_implicit_connect) " + i, msgComplete1.add()); + try { Thread.sleep(delay); } catch(InterruptedException e){} + } + + /* wait for the publish callback to be called */ + ErrorInfo[] errors = msgComplete1.waitFor(); + assertTrue("Verify success from all message callbacks", errors.length == 0); + + /* wait for the subscription callback to be called */ + messageWaiter.waitFor(messageCount); + assertEquals("Verify message subscriptions all called", messageWaiter.receivedMessages.size(), messageCount); + + /* disconnect the rx connection, without closing; + * NOTE this depends on knowledge of the internal structure + * of the library, to simulate a dropped transport without + * causing the connection itself to be disposed */ + String recoverConnectionKey = ablyRx.connection.recoveryKey; + ablyRx.connection.connectionManager.requestState(ConnectionState.failed); + + /* wait */ + try { Thread.sleep(2000L); } catch(InterruptedException e) {} + + /* publish next messages to the channel */ + CompletionSet msgComplete2 = new CompletionSet(); + for(int i = 0; i < messageCount; i++) { + channelTx.publish("test_event", "Test message (recover_implicit_connect) " + i, msgComplete2.add()); + try { Thread.sleep(delay); } catch(InterruptedException e){} + } + + /* wait for the publish callback to be called */ + errors = msgComplete2.waitFor(); + assertTrue("Verify success from all message callbacks", errors.length == 0); + + /* establish a new rx connection with recover string, and wait for connection */ + ClientOptions recoverOpts = createOptions(testVars.keys[0].keyStr); + recoverOpts.recover = recoverConnectionKey; + ablyRxRecover = new AblyRealtime(recoverOpts); + + /* subscribe to channel */ + final Channel channelRxRecover = ablyRxRecover.channels.get(channelName); + MessageWaiter messageWaiterRecover = new MessageWaiter(channelRxRecover); + + /* wait for the subscription callback to be called */ + messageWaiterRecover.waitFor(messageCount); + assertEquals("Verify message subscriptions all called after recovery", messageWaiterRecover.receivedMessages.size(), messageCount); + + } catch (AblyException e) { + e.printStackTrace(); + fail("init0: Unexpected exception instantiating library"); + } finally { + if(ablyTx != null) + ablyTx.close(); + if(ablyRx != null) + ablyRx.close(); + if(ablyRxRecover != null) + ablyRxRecover.close(); + } + } + + /** + * Connect to the service using two library instances to set + * up separate send and recv connections. + * Disconnect+suspend and then reconnect the send connection; verify that + * each subsequent publish causes a CompletionListener call. + */ + @Test + public void recover_verify_publish() { + AblyRealtime ablyRx = null, ablyTx = null; + String channelName = "recover_verify_publish"; + int messageCount = 5; + long delay = 200; + try { + ClientOptions opts = createOptions(testVars.keys[0].keyStr); + ablyRx = new AblyRealtime(opts); + ablyTx = new AblyRealtime(opts); + + /* create and attach channel to send on */ + final Channel channelTx = ablyTx.channels.get(channelName); + channelTx.attach(); + (new ChannelWaiter(channelTx)).waitFor(ChannelState.attached); + assertEquals("Verify attached state reached for tx", channelTx.state, ChannelState.attached); + + /* create and attach channel to recv on */ + final Channel channelRx = ablyRx.channels.get(channelName); + channelRx.attach(); + (new ChannelWaiter(channelRx)).waitFor(ChannelState.attached); + assertEquals("Verify attached state reached for rx", channelRx.state, ChannelState.attached); + + /* subscribe */ + MessageWaiter messageWaiter = new MessageWaiter(channelRx); + + /* publish first messages to the channel */ + CompletionSet msgComplete1 = new CompletionSet(); + for(int i = 0; i < messageCount; i++) { + channelTx.publish("test_event", "Test message (resume_simple) " + i, msgComplete1.add()); + try { Thread.sleep(delay); } catch(InterruptedException e){} + } + + /* wait for the publish callback to be called */ + ErrorInfo[] errors = msgComplete1.waitFor(); + assertTrue("Verify success from all message callbacks", errors.length == 0); + + /* wait for the subscription callback to be called */ + messageWaiter.waitFor(messageCount); + assertEquals("Verify message subscriptions all called", messageWaiter.receivedMessages.size(), messageCount); + messageWaiter.reset(); + + /* suspend the tx connection, without closing; + * NOTE this depends on knowledge of the internal structure + * of the library, to simulate a dropped transport without + * causing the connection itself to be disposed */ + System.out.println("*** about to suspend tx connection"); + ablyTx.connection.connectionManager.requestState(ConnectionState.suspended); + + /* wait */ + try { Thread.sleep(2000L); } catch(InterruptedException e) {} + + /* reconnect the tx connection */ + System.out.println("*** about to reconnect tx connection"); + ablyTx.connection.connect(); + (new ConnectionWaiter(ablyTx.connection)).waitFor(ConnectionState.connected); + + /* need to manually attach the tx channel as connection was suspended */ + System.out.println("*** tx connection now connected. About to recover channel"); + channelTx.attach(); + (new ChannelWaiter(channelTx)).waitFor(ChannelState.attached); + assertEquals("Verify attached state reached for tx again", channelTx.state, ChannelState.attached); + System.out.println("*** tx channel now attached. About to publish"); + + /* publish further messages to the channel */ + CompletionSet msgComplete2 = new CompletionSet(); + for(int i = 0; i < messageCount; i++) { + channelTx.publish("test_event", "Test message (resume_simple) " + i, msgComplete2.add()); + try { Thread.sleep(delay); } catch(InterruptedException e){} + } + + /* wait for the publish callback to be called. This never finishes if + * https://github.com/ably/ably-java/issues/170 + * is not fixed. */ + System.out.println("*** published. About to wait for callbacks"); + errors = msgComplete2.waitFor(); + System.out.println("*** done"); + assertTrue("Verify success from all message callbacks", errors.length == 0); + + /* wait for the subscription callback to be called */ + messageWaiter.waitFor(messageCount); + assertEquals("Verify message subscriptions all called after reconnection", messageWaiter.receivedMessages.size(), messageCount); + + } catch (AblyException e) { + e.printStackTrace(); + fail("init0: Unexpected exception instantiating library"); + } finally { + if(ablyTx != null) + ablyTx.close(); + if(ablyRx != null) + ablyRx.close(); + } + } + + /** + * Test if ConnectionManager behaves correctly after exception thrown from ITransport.send() + */ + @Test + public void recover_transport_send_exception() { + AblyRealtime ably = null; + final String channelName = "recover_transport_send_exception"; + try { + MockWebsocketFactory mockTransport = new MockWebsocketFactory(); + mockTransport.throwOnSend = false; + mockTransport.exceptionsThrown = 0; + + DebugOptions opts = new DebugOptions(testVars.keys[0].keyStr); + fillInOptions(opts); + opts.transportFactory = mockTransport; + + opts.autoConnect = false; + ably = new AblyRealtime(opts); + ConnectionWaiter connWaiter = new ConnectionWaiter(ably.connection); + ably.connection.connect(); + connWaiter.waitFor(ConnectionState.connected); + + Channel channel = ably.channels.get(channelName); + channel.attach(); + new ChannelWaiter(channel).waitFor(ChannelState.attached); + + ably.connection.connectionManager.requestState(ConnectionState.disconnected); + connWaiter.waitFor(ConnectionState.connecting, 2); + + /* Start throwing exceptions in the send() */ + mockTransport.throwOnSend = true; + for (int i=0; i<5; i++) { + try { + channel.publish("name", "data"); + } catch (Throwable e) { + } + } + + try { + Thread.sleep(1000); + } catch (InterruptedException e) {} + + /* Stop exceptions */ + mockTransport.throwOnSend = false; + channel.publish("name2", "data2"); + + try { + Thread.sleep(500); + } catch (InterruptedException e) {} + + /* It is hard to predict how many exception would be thrown but it shouldn't be hundreds */ + assertThat("", mockTransport.exceptionsThrown, is(lessThan(10))); + + } catch (AblyException e) { + e.printStackTrace(); + fail("Unexpected exception"); + } finally { + if (ably != null) + ably.close(); + } + } + + public static class MockWebsocketFactory implements ITransport.Factory { + boolean throwOnSend = false; + int exceptionsThrown = 0; + + /* + * Special transport class that allows throwing exceptions from send() + */ + private class MockWebsocketTransport extends WebSocketTransport { + private MockWebsocketTransport(TransportParams transportParams, ConnectionManager connectionManager) { + super(transportParams, connectionManager); + } + + @Override + public void send(ProtocolMessage msg) throws AblyException { + if (throwOnSend) { + exceptionsThrown++; + throw AblyException.fromErrorInfo(new ErrorInfo("TestException", 40000)); + } else { + super.send(msg); + } + } + } + + @Override + public ITransport getTransport(ITransport.TransportParams transportParams, ConnectionManager connectionManager) { + return new MockWebsocketTransport(transportParams, connectionManager); + } + } } diff --git a/lib/src/test/java/io/ably/lib/test/realtime/RealtimeResumeTest.java b/lib/src/test/java/io/ably/lib/test/realtime/RealtimeResumeTest.java index 0a41cefa8..731987a89 100644 --- a/lib/src/test/java/io/ably/lib/test/realtime/RealtimeResumeTest.java +++ b/lib/src/test/java/io/ably/lib/test/realtime/RealtimeResumeTest.java @@ -27,717 +27,717 @@ public class RealtimeResumeTest extends ParameterizedTest { - private static final String TAG = RealtimeResumeTest.class.getName(); - - @Rule - public Timeout testTimeout = Timeout.seconds(60); - - /** - * Connect to the service and attach a channel. - * Don't publish any messages; disconnect and then reconnect; verify that - * the channel is still attached. - */ - @Test - public void resume_none() { - AblyRealtime ably = null; - String channelName = "resume_none"; - try { - ClientOptions opts = createOptions(testVars.keys[0].keyStr); - ably = new AblyRealtime(opts); - - /* create and attach channel */ - final Channel channel = ably.channels.get(channelName); - System.out.println("Attaching"); - channel.attach(); - (new ChannelWaiter(channel)).waitFor(ChannelState.attached); - assertEquals("Verify attached state reached", channel.state, ChannelState.attached); - - /* disconnect the connection, without closing, - /* suppressing automatic retries by the connection manager */ - System.out.println("Simulating dropped transport"); - try { - Method method = ably.connection.connectionManager.getClass().getDeclaredMethod("disconnectAndSuppressRetries"); - method.setAccessible(true); - method.invoke(ably.connection.connectionManager); - } catch (NoSuchMethodException|IllegalAccessException| InvocationTargetException e) { - fail("Unexpected exception in suppressing retries"); - } - - /* reconnect the rx connection */ - ably.connection.connect(); - System.out.println("Waiting for reconnection"); - ConnectionWaiter connectionWaiter = new ConnectionWaiter(ably.connection); - connectionWaiter.waitFor(ConnectionState.connected); - assertEquals("Verify connected state is reached", ConnectionState.connected, ably.connection.state); - - /* wait */ - System.out.println("Got reconnection; waiting 2s"); - try { Thread.sleep(2000L); } catch(InterruptedException e) {} - - /* Check the channel is still attached. */ - assertEquals("Verify channel still attached", channel.state, ChannelState.attached); - - } catch (AblyException e) { - e.printStackTrace(); - fail("init0: Unexpected exception instantiating library"); - } finally { - if(ably != null) { - ably.close(); - } - } - } - - /** - * Connect to the service using two library instances to set - * up separate send and recv connections. - * Disconnect and then reconnect one connection; verify that - * the connection continues to receive messages on attached - * channels after reconnection. - */ - @Test - public void resume_simple() { - AblyRealtime ablyTx = null; - AblyRealtime ablyRx = null; - String channelName = "resume_simple"; - int messageCount = 5; - long delay = 200; - try { - ClientOptions opts = createOptions(testVars.keys[0].keyStr); - ablyRx = new AblyRealtime(opts); - ablyTx = new AblyRealtime(opts); - - /* create and attach channel to send on */ - final Channel channelTx = ablyTx.channels.get(channelName); - channelTx.attach(); - (new ChannelWaiter(channelTx)).waitFor(ChannelState.attached); - assertEquals("Verify attached state reached for tx", channelTx.state, ChannelState.attached); - - /* create and attach channel to recv on */ - final Channel channelRx = ablyRx.channels.get(channelName); - channelRx.attach(); - (new ChannelWaiter(channelRx)).waitFor(ChannelState.attached); - assertEquals("Verify attached state reached for rx", channelRx.state, ChannelState.attached); - - /* subscribe */ - MessageWaiter messageWaiter = new MessageWaiter(channelRx); - - /* publish first messages to the channel */ - CompletionSet msgComplete1 = new CompletionSet(); - for(int i = 0; i < messageCount; i++) { - channelTx.publish("test_event", "Test message (resume_simple) " + i, msgComplete1.add()); - try { Thread.sleep(delay); } catch(InterruptedException e){} - } - - /* wait for the publish callback to be called */ - ErrorInfo[] errors = msgComplete1.waitFor(); - assertTrue("Verify success from all message callbacks", errors.length == 0); - - /* wait for the subscription callback to be called */ - messageWaiter.waitFor(messageCount); - assertEquals("Verify message subscriptions all called", messageWaiter.receivedMessages.size(), messageCount); - messageWaiter.reset(); - - /* disconnect the rx connection, without closing; - * NOTE this depends on knowledge of the internal structure - * of the library, to simulate a dropped transport without - * causing the connection itself to be disposed */ - ablyRx.connection.connectionManager.requestState(ConnectionState.disconnected); - - /* wait */ - try { Thread.sleep(2000L); } catch(InterruptedException e) {} - - /* reconnect the rx connection */ - ablyRx.connection.connect(); - - /* publish further messages to the channel */ - CompletionSet msgComplete2 = new CompletionSet(); - for(int i = 0; i < messageCount; i++) { - channelTx.publish("test_event", "Test message (resume_simple) " + i, msgComplete2.add()); - try { Thread.sleep(delay); } catch(InterruptedException e){} - } - - /* wait for the publish callback to be called */ - errors = msgComplete2.waitFor(); - assertTrue("Verify success from all message callbacks", errors.length == 0); - - /* wait for the subscription callback to be called */ - messageWaiter.waitFor(messageCount); - assertEquals("Verify message subscriptions all called after reconnection", messageWaiter.receivedMessages.size(), messageCount); - - } catch (AblyException e) { - e.printStackTrace(); - fail("init0: Unexpected exception instantiating library"); - } finally { - if(ablyTx != null) { - ablyTx.close(); - } - if(ablyRx != null) { - ablyRx.close(); - } - } - } - - /** - * Connect to the service using two library instances to set - * up separate send and recv connections. - * Send on one connection while the other is disconnected; - * verify that the messages sent whilst disconnected are delivered - * on resume - */ - @Test - public void resume_disconnected() { - AblyRealtime ablyTx = null; - AblyRealtime ablyRx = null; - String channelName = "resume_disconnected"; - int messageCount = 5; - long delay = 200; - try { - ClientOptions opts = createOptions(testVars.keys[0].keyStr); - ablyRx = new AblyRealtime(opts); - ablyTx = new AblyRealtime(opts); - - /* create and attach channel to send on */ - final Channel channelTx = ablyTx.channels.get(channelName); - channelTx.attach(); - (new ChannelWaiter(channelTx)).waitFor(ChannelState.attached); - assertEquals("Verify attached state reached for tx", channelTx.state, ChannelState.attached); - - /* create and attach channel to recv on */ - final Channel channelRx = ablyRx.channels.get(channelName); - channelRx.attach(); - (new ChannelWaiter(channelRx)).waitFor(ChannelState.attached); - assertEquals("Verify attached state reached for rx", channelRx.state, ChannelState.attached); - - /* subscribe */ - MessageWaiter messageWaiter = new MessageWaiter(channelRx); - - /* publish first messages to the channel */ - CompletionSet msgComplete1 = new CompletionSet(); - for(int i = 0; i < messageCount; i++) { - channelTx.publish("test_event", "Test message (resume_disconnected) " + i, msgComplete1.add()); - try { Thread.sleep(delay); } catch(InterruptedException e){} - } - - /* wait for the publish callback to be called */ - ErrorInfo[] errors = msgComplete1.waitFor(); - assertTrue("Verify success from all message callbacks", errors.length == 0); - - /* wait for the subscription callback to be called */ - messageWaiter.waitFor(messageCount); - assertEquals("Verify message subscriptions all called", messageWaiter.receivedMessages.size(), messageCount); - messageWaiter.reset(); - - /* disconnect the rx connection, without closing; - * NOTE this depends on knowledge of the internal structure - * of the library, to simulate a dropped transport without - * causing the connection itself to be disposed */ - ablyRx.connection.connectionManager.requestState(ConnectionState.disconnected); - - /* wait */ - try { Thread.sleep(2000L); } catch(InterruptedException e) {} - - /* publish next messages to the channel */ - CompletionSet msgComplete2 = new CompletionSet(); - for(int i = 0; i < messageCount; i++) { - channelTx.publish("test_event", "Test message (resume_disconnected) " + i, msgComplete2.add()); - try { Thread.sleep(delay); } catch(InterruptedException e){} - } - - /* wait for the publish callback to be called */ - errors = msgComplete2.waitFor(); - assertTrue("Verify success from all message callbacks", errors.length == 0); - - /* reconnect the rx connection, and expect the messages to be delivered */ - ablyRx.connection.connect(); - /* wait for the subscription callback to be called */ - messageWaiter.waitFor(messageCount); - assertEquals("Verify message subscriptions all called after reconnection", messageWaiter.receivedMessages.size(), messageCount); - - } catch (AblyException e) { - e.printStackTrace(); - fail("init0: Unexpected exception instantiating library"); - } finally { - if(ablyTx != null) { - ablyTx.close(); - } - if(ablyRx != null) { - ablyRx.close(); - } - } - } - - /** - * Verify resume behaviour with multiple channels - */ - @Test - public void resume_multiple_channel() { - AblyRealtime ablyTx = null; - AblyRealtime ablyRx = null; - String channelName = "resume_multiple_channel"; - int messageCount = 5; - long delay = 200; - try { - ClientOptions opts = createOptions(testVars.keys[0].keyStr); - ablyRx = new AblyRealtime(opts); - ablyTx = new AblyRealtime(opts); - - /* create and attach channels to send on */ - final Channel channelTx1 = ablyTx.channels.get(channelName + "_1"); - channelTx1.attach(); - (new ChannelWaiter(channelTx1)).waitFor(ChannelState.attached); - assertEquals("Verify attached state reached for tx", channelTx1.state, ChannelState.attached); - final Channel channelTx2 = ablyTx.channels.get(channelName + "_2"); - channelTx2.attach(); - (new ChannelWaiter(channelTx2)).waitFor(ChannelState.attached); - assertEquals("Verify attached state reached for tx", channelTx2.state, ChannelState.attached); - - /* create and attach channel to recv on */ - final Channel channelRx1 = ablyRx.channels.get(channelName + "_1"); - channelRx1.attach(); - (new ChannelWaiter(channelRx1)).waitFor(ChannelState.attached); - assertEquals("Verify attached state reached for rx", channelRx1.state, ChannelState.attached); - final Channel channelRx2 = ablyRx.channels.get(channelName + "_2"); - channelRx2.attach(); - (new ChannelWaiter(channelRx2)).waitFor(ChannelState.attached); - assertEquals("Verify attached state reached for rx", channelRx2.state, ChannelState.attached); - - /* subscribe */ - MessageWaiter messageWaiter1 = new MessageWaiter(channelRx1); - MessageWaiter messageWaiter2 = new MessageWaiter(channelRx2); - - /* publish first messages to channels */ - CompletionSet msgComplete1 = new CompletionSet(); - for(int i = 0; i < messageCount; i++) { - channelTx1.publish("test_event1", "Test message (resume_multiple_channel) " + i, msgComplete1.add()); - channelTx2.publish("test_event2", "Test message (resume_multiple_channel) " + i, msgComplete1.add()); - try { Thread.sleep(delay); } catch(InterruptedException e){} - } - - /* wait for the publish callback to be called */ - ErrorInfo[] errors = msgComplete1.waitFor(); - assertTrue("Verify success from all message callbacks", errors.length == 0); - - /* wait for the subscription callback to be called */ - messageWaiter1.waitFor(messageCount); - assertEquals("Verify message subscriptions all called", messageWaiter1.receivedMessages.size(), messageCount); - messageWaiter1.reset(); - messageWaiter2.waitFor(messageCount); - assertEquals("Verify message subscriptions all called", messageWaiter2.receivedMessages.size(), messageCount); - messageWaiter2.reset(); - - /* disconnect the rx connection, without closing; - * NOTE this depends on knowledge of the internal structure - * of the library, to simulate a dropped transport without - * causing the connection itself to be disposed */ - ablyRx.connection.connectionManager.requestState(ConnectionState.disconnected); - - /* wait */ - try { Thread.sleep(2000L); } catch(InterruptedException e) {} - - /* publish next messages to the channel */ - CompletionSet msgComplete2 = new CompletionSet(); - for(int i = 0; i < messageCount; i++) { - channelTx1.publish("test_event1", "Test message (resume_multiple_channel) " + i, msgComplete2.add()); - channelTx2.publish("test_event2", "Test message (resume_multiple_channel) " + i, msgComplete2.add()); - try { Thread.sleep(delay); } catch(InterruptedException e){} - } - - /* wait for the publish callback to be called */ - errors = msgComplete2.waitFor(); - assertTrue("Verify success from all message callbacks", errors.length == 0); - - /* reconnect the rx connection, and expect the messages to be delivered */ - ablyRx.connection.connect(); - /* wait for the subscription callback to be called */ - messageWaiter1.waitFor(messageCount); - messageWaiter2.waitFor(messageCount); - assertEquals("Verify message subscriptions all called after reconnection", messageWaiter1.receivedMessages.size(), messageCount); - assertEquals("Verify message subscriptions all called after reconnection", messageWaiter2.receivedMessages.size(), messageCount); - - } catch (AblyException e) { - e.printStackTrace(); - fail("init0: Unexpected exception instantiating library"); - } finally { - if(ablyTx != null) { - ablyTx.close(); - } - if(ablyRx != null) { - ablyRx.close(); - } - } - } - - /** - * Verify resume behaviour across disconnect periods covering - * multiple subminute intervals - */ - @Test - public void resume_multiple_interval() { - AblyRealtime ablyTx = null; - AblyRealtime ablyRx = null; - String channelName = "resume_multiple_interval"; - int messageCount = 5; - long delay = 200; - try { - ClientOptions opts = createOptions(testVars.keys[0].keyStr); - ablyRx = new AblyRealtime(opts); - ablyTx = new AblyRealtime(opts); - - /* create and attach channel to send on */ - final Channel channelTx = ablyTx.channels.get(channelName); - channelTx.attach(); - (new ChannelWaiter(channelTx)).waitFor(ChannelState.attached); - assertEquals("Verify attached state reached for tx", channelTx.state, ChannelState.attached); - - /* create and attach channel to recv on */ - final Channel channelRx = ablyRx.channels.get(channelName); - channelRx.attach(); - (new ChannelWaiter(channelRx)).waitFor(ChannelState.attached); - assertEquals("Verify attached state reached for rx", channelRx.state, ChannelState.attached); - - /* subscribe */ - MessageWaiter messageWaiter = new MessageWaiter(channelRx); - - /* publish first messages to the channel */ - CompletionSet msgComplete1 = new CompletionSet(); - for(int i = 0; i < messageCount; i++) { - channelTx.publish("test_event", "Test message (resume_multiple_interval) " + i, msgComplete1.add()); - try { Thread.sleep(delay); } catch(InterruptedException e){} - } - - /* wait for the publish callback to be called */ - ErrorInfo[] errors = msgComplete1.waitFor(); - assertTrue("Verify success from all message callbacks", errors.length == 0); - - /* wait for the subscription callback to be called */ - messageWaiter.waitFor(messageCount); - assertEquals("Verify message subscriptions all called", messageWaiter.receivedMessages.size(), messageCount); - messageWaiter.reset(); - - /* disconnect the rx connection, without closing; - * NOTE this depends on knowledge of the internal structure - * of the library, to simulate a dropped transport without - * causing the connection itself to be disposed */ - ablyRx.connection.connectionManager.requestState(ConnectionState.disconnected); - - /* wait */ - try { Thread.sleep(20000L); } catch(InterruptedException e) {} - - /* publish next messages to the channel */ - CompletionSet msgComplete2 = new CompletionSet(); - for(int i = 0; i < messageCount; i++) { - channelTx.publish("test_event", "Test message (resume_multiple_interval) " + i, msgComplete2.add()); - try { Thread.sleep(delay); } catch(InterruptedException e){} - } - - /* wait for the publish callback to be called */ - errors = msgComplete2.waitFor(); - assertTrue("Verify success from all message callbacks", errors.length == 0); - - /* reconnect the rx connection, and expect the messages to be delivered */ - ablyRx.connection.connect(); - /* wait for the subscription callback to be called */ - messageWaiter.waitFor(messageCount); - assertEquals("Verify message subscriptions all called after reconnection", messageWaiter.receivedMessages.size(), messageCount); - - } catch (AblyException e) { - e.printStackTrace(); - fail("init0: Unexpected exception instantiating library"); - } finally { - if(ablyTx != null) { - ablyTx.close(); - } - if(ablyRx != null) { - ablyRx.close(); - } - } - } - - /** - * Connect to the service using two library instances to set - * up separate send and recv connections. - * Disconnect and then reconnect the send connection; verify that - * each subsequent publish causes a CompletionListener call. - */ - @Test - public void resume_verify_publish() { - AblyRealtime ablyTx = null; - AblyRealtime ablyRx = null; - String channelName = "resume_verify_publish"; - int messageCount = 5; - long delay = 200; - try { - ClientOptions opts = createOptions(testVars.keys[0].keyStr); - ablyRx = new AblyRealtime(opts); - ablyTx = new AblyRealtime(opts); - - /* create and attach channel to send on */ - final Channel channelTx = ablyTx.channels.get(channelName); - channelTx.attach(); - (new ChannelWaiter(channelTx)).waitFor(ChannelState.attached); - assertEquals("Verify attached state reached for tx", channelTx.state, ChannelState.attached); - - /* create and attach channel to recv on */ - final Channel channelRx = ablyRx.channels.get(channelName); - channelRx.attach(); - (new ChannelWaiter(channelRx)).waitFor(ChannelState.attached); - assertEquals("Verify attached state reached for rx", channelRx.state, ChannelState.attached); - - /* subscribe */ - MessageWaiter messageWaiter = new MessageWaiter(channelRx); - - /* publish first messages to the channel */ - CompletionSet msgComplete1 = new CompletionSet(); - for(int i = 0; i < messageCount; i++) { - channelTx.publish("test_event", "Test message (resume_simple) " + i, msgComplete1.add()); - try { Thread.sleep(delay); } catch(InterruptedException e){} - } - - /* wait for the publish callback to be called */ - ErrorInfo[] errors = msgComplete1.waitFor(); - assertTrue("Verify success from all message callbacks", errors.length == 0); - - /* wait for the subscription callback to be called */ - messageWaiter.waitFor(messageCount); - assertEquals("Verify message subscriptions all called", messageWaiter.receivedMessages.size(), messageCount); - messageWaiter.reset(); - - /* disconnect the tx connection, without closing; - * NOTE this depends on knowledge of the internal structure - * of the library, to simulate a dropped transport without - * causing the connection itself to be disposed */ - System.out.println("*** about to disconnect tx connection"); - /* suppress automatic retries by the connection manager */ - try { - Method method = ablyTx.connection.connectionManager.getClass().getDeclaredMethod("disconnectAndSuppressRetries"); - method.setAccessible(true); - method.invoke(ablyTx.connection.connectionManager); - } catch (NoSuchMethodException|IllegalAccessException|InvocationTargetException e) { - fail("Unexpected exception in suppressing retries"); - } - - /* wait */ - try { Thread.sleep(2000L); } catch(InterruptedException e) {} - - /* reconnect the tx connection */ - System.out.println("*** about to reconnect tx connection"); - ablyTx.connection.connect(); - (new ConnectionWaiter(ablyTx.connection)).waitFor(ConnectionState.connected); - - /* publish further messages to the channel */ - CompletionSet msgComplete2 = new CompletionSet(); - for(int i = 0; i < messageCount; i++) { - channelTx.publish("test_event", "Test message (resume_simple) " + i, msgComplete2.add()); - try { Thread.sleep(delay); } catch(InterruptedException e){} - } - - /* wait for the publish callback to be called. This never finishes if - * https://github.com/ably/ably-java/issues/170 - * is not fixed. */ - System.out.println("*** published. About to wait for callbacks"); - errors = msgComplete2.waitFor(); - System.out.println("*** done"); - assertTrue("Verify success from all message callbacks", errors.length == 0); - - /* wait for the subscription callback to be called */ - messageWaiter.waitFor(messageCount); - assertEquals("Verify message subscriptions all called after reconnection", messageWaiter.receivedMessages.size(), messageCount); - - } catch (AblyException e) { - e.printStackTrace(); - fail("init0: Unexpected exception instantiating library"); - } finally { - if(ablyTx != null) { - ablyTx.close(); - } - if(ablyRx != null) { - ablyRx.close(); - } - } - } - - /** - * Connect to the service using two library instances to set - * up separate send and recv connections. - * - * Send some messages, drop the sender's transport, then send another - * round of messages which should be queued and published after - * we reconnect the sender. - */ - @Test - public void resume_publish_queue() { - AblyRealtime receiver = null; - AblyRealtime sender = null; - String channelName = "resume_publish_queue"; - int messageCount = 3; - long delay = 200; - try { - ClientOptions opts = createOptions(testVars.keys[0].keyStr); - receiver = new AblyRealtime(opts); - sender = new AblyRealtime(opts); - - /* create and attach channel to send on */ - final Channel senderChannel = sender.channels.get(channelName); - senderChannel.attach(); - (new ChannelWaiter(senderChannel)).waitFor(ChannelState.attached); - assertEquals( - "The sender's channel should be attached", - senderChannel.state, ChannelState.attached - ); - - /* create and attach channel to recv on */ - final Channel receiverChannel = receiver.channels.get(channelName); - receiverChannel.attach(); - (new ChannelWaiter(receiverChannel)).waitFor(ChannelState.attached); - assertEquals( - "The receiver's channel should be attached", - receiverChannel.state, ChannelState.attached - ); - /* subscribe */ - MessageWaiter messageWaiter = new MessageWaiter(receiverChannel); - - /* publish first messages to the channel */ - CompletionSet msgComplete1 = new CompletionSet(); - for(int i = 0; i < messageCount; i++) { - senderChannel.publish("test_event", "Test message (resume_publish_queue) " + i, msgComplete1.add()); - try { Thread.sleep(delay); } catch(InterruptedException e){} - } - - /* wait for the publish callback to be called */ - ErrorInfo[] errors = msgComplete1.waitFor(); - assertTrue( - "First round of messages has errors", errors.length == 0 - ); - - /* wait for the subscription callback to be called */ - messageWaiter.waitFor(messageCount); - assertEquals( - "Did not receive the entire first round of messages", - messageWaiter.receivedMessages.size(), messageCount - ); - messageWaiter.reset(); - - /* disconnect the sender, without closing; - * NOTE this depends on knowledge of the internal structure - * of the library, to simulate a dropped transport without - * causing the connection itself to be disposed */ - sender.connection.connectionManager.requestState(ConnectionState.disconnected); - - /* wait */ - try { Thread.sleep(2000L); } catch(InterruptedException e) {} - - /* - * publish further messages to the channel, which should be queued - * because the channel is currently disconnected. - */ - CompletionSet msgComplete2 = new CompletionSet(); - for(int i = 0; i < messageCount; i++) { - senderChannel.publish("queued_message_" + i, "Test queued message (resume_publish_queue) " + i, msgComplete2.add()); - try { Thread.sleep(delay); } catch(InterruptedException e){} - } - - /* reconnect the sender */ - sender.connection.connect(); - (new ConnectionWaiter(sender.connection)).waitFor(ConnectionState.connected); - - - /* wait for the publish callback to be called.*/ - errors = msgComplete2.waitFor(); - assertTrue( - "Second round of messages (queued) has errors", - errors.length == 0 - ); - - /* wait for the subscription callback to be called */ - messageWaiter.waitFor(messageCount); - - List received = messageWaiter.receivedMessages; - assertEquals( - "Did not receive the entire second round of messages (queued)", - received.size(), messageCount - ); - for(int i=0; i received = messageWaiter.receivedMessages; + assertEquals( + "Did not receive the entire second round of messages (queued)", + received.size(), messageCount + ); + for(int i=0; i - * Spec: RSC7b, G4 - *

- * - * Spec: RSC7a: Must have the header X-Ably-Version: 1.0 (or whatever the - * spec version is). - */ - @Test - public void header_lib_channel_publish() { - try { - /* Init values for local server */ - ClientOptions opts = createOptions(testVars.keys[0].keyStr); - opts.environment = null; - opts.tls = false; - opts.port = server.getListeningPort(); - opts.restHost = "localhost"; - AblyRest ably = new AblyRest(opts); - - /* Publish message */ - String messageName = "test message"; - String messageData = String.valueOf(System.currentTimeMillis()); - - Channel channel = ably.channels.get("test"); - channel.publish(messageName, messageData); - - /* Get last headers */ - Map headers = server.getHeaders(); - - /* Check header - * This test should not directly validate version against Defaults.ABLY_VERSION, Defaults.ABLY_LIB_VERSION, - * Defaults.ABLY_VERSION_HEADER, nor Defaults.ABLY_LIB_HEADER, as ultimately these headers have been derived - * from those values. - */ - Assert.assertNotNull("Expected headers", headers); - Assert.assertEquals(headers.get("x-ably-version"), "1.2"); - Assert.assertEquals(headers.get("x-ably-lib"), "java-1.2.2"); - } catch (AblyException e) { - e.printStackTrace(); - Assert.fail("header_lib_channel_publish: Unexpected exception"); - } - } - - private static class SessionHandlerNanoHTTPD extends NanoHTTPD { - Map requestHeaders; - - public SessionHandlerNanoHTTPD(int port) { - super(port); - } - - @Override - public Response serve(IHTTPSession session) { - requestHeaders = new HashMap<>(session.getHeaders()); - int contentLength = Integer.parseInt(requestHeaders.get("content-length")); - try { - session.getInputStream().read(new byte[contentLength]); - } catch (IOException e) {} - return newFixedLengthResponse("Ignored response"); - } - - public Map getHeaders() { - return requestHeaders; - } - } + private static SessionHandlerNanoHTTPD server; + + @BeforeClass + public static void setUp() throws IOException { + /* Create custom RouterNanoHTTPD class for getting session object */ + server = new SessionHandlerNanoHTTPD(27331); + server.start(NanoHTTPD.SOCKET_READ_TIMEOUT, true); + + /* wait for server to start */ + while (!server.wasStarted()) { + try { + Thread.sleep(100); + } catch (InterruptedException e) { + e.printStackTrace(); + } + } + } + + @AfterClass + public static void tearDown() { + server.stop(); + } + + /** + * The header X-Ably-Lib: [lib][.optional variant]?-[version] + * should be included in all REST requests to the Ably endpoint + * see {@link io.ably.lib.http.HttpUtils#ABLY_LIB_VERSION} + *

+ * Spec: RSC7b, G4 + *

+ * + * Spec: RSC7a: Must have the header X-Ably-Version: 1.0 (or whatever the + * spec version is). + */ + @Test + public void header_lib_channel_publish() { + try { + /* Init values for local server */ + ClientOptions opts = createOptions(testVars.keys[0].keyStr); + opts.environment = null; + opts.tls = false; + opts.port = server.getListeningPort(); + opts.restHost = "localhost"; + AblyRest ably = new AblyRest(opts); + + /* Publish message */ + String messageName = "test message"; + String messageData = String.valueOf(System.currentTimeMillis()); + + Channel channel = ably.channels.get("test"); + channel.publish(messageName, messageData); + + /* Get last headers */ + Map headers = server.getHeaders(); + + /* Check header + * This test should not directly validate version against Defaults.ABLY_VERSION, Defaults.ABLY_LIB_VERSION, + * Defaults.ABLY_VERSION_HEADER, nor Defaults.ABLY_LIB_HEADER, as ultimately these headers have been derived + * from those values. + */ + Assert.assertNotNull("Expected headers", headers); + Assert.assertEquals(headers.get("x-ably-version"), "1.2"); + Assert.assertEquals(headers.get("x-ably-lib"), "java-1.2.2"); + } catch (AblyException e) { + e.printStackTrace(); + Assert.fail("header_lib_channel_publish: Unexpected exception"); + } + } + + private static class SessionHandlerNanoHTTPD extends NanoHTTPD { + Map requestHeaders; + + public SessionHandlerNanoHTTPD(int port) { + super(port); + } + + @Override + public Response serve(IHTTPSession session) { + requestHeaders = new HashMap<>(session.getHeaders()); + int contentLength = Integer.parseInt(requestHeaders.get("content-length")); + try { + session.getInputStream().read(new byte[contentLength]); + } catch (IOException e) {} + return newFixedLengthResponse("Ignored response"); + } + + public Map getHeaders() { + return requestHeaders; + } + } } diff --git a/lib/src/test/java/io/ably/lib/test/rest/HttpTest.java b/lib/src/test/java/io/ably/lib/test/rest/HttpTest.java index 3fff2a197..fa3649bc3 100644 --- a/lib/src/test/java/io/ably/lib/test/rest/HttpTest.java +++ b/lib/src/test/java/io/ably/lib/test/rest/HttpTest.java @@ -51,1345 +51,1345 @@ */ public class HttpTest { - private static final String PATTERN_HOST_FALLBACK = "(?i)[a-e]\\.ably-realtime.com"; - private static final String CUSTOM_PATTERN_HOST_FALLBACK = "(?i)[f-k]\\.ably-realtime.com"; - private static final String[] CUSTOM_HOSTS = { "f.ably-realtime.com", "g.ably-realtime.com", "h.ably-realtime.com", "i.ably-realtime.com", "j.ably-realtime.com", "k.ably-realtime.com" }; - private static final String TEST_SERVER_HOST = "localhost"; - private static final int TEST_SERVER_PORT = 27331; - - @Rule - public Timeout testTimeout = Timeout.seconds(60); - - @Rule - public ExpectedException thrown = ExpectedException.none(); - private static RouterNanoHTTPD server; - - @BeforeClass - public static void setUp() throws IOException { - server = new RouterNanoHTTPD(TEST_SERVER_PORT); - server.addRoute("/status/:code", StatusHandler.class); - server.addRoute("/time", TimeHandler.class); - server.start(NanoHTTPD.SOCKET_READ_TIMEOUT, true); - - while (!server.wasStarted()) { - try { - Thread.sleep(100); - } catch (InterruptedException e) { - e.printStackTrace(); - } - } - } - - @AfterClass - public static void tearDown() { - server.stop(); - } - - - /******************************************* - * Spec: RSC15 - *******************************************/ - - /** - *

- * Validates {@code HttpCore} performs fallback behavior httpMaxRetryCount number of times at most, - * when host & fallback hosts are unreachable. Then, finally throws an error. - *

- *

- * Spec: RSC15a - *

- * - * @throws Exception - */ - @Test - public void http_ably_execute_fallback() throws AblyException { - ClientOptions options = new ClientOptions(); - options.tls = false; - /* Select a port that will be refused immediately by the production host */ - options.port = 7777; - - /* Create a list to capture the host of URL arguments that get called with httpExecute method. - * This will later be used to validate hosts used for requests - */ - ArrayList urlHostArgumentStack = new ArrayList<>(4); - - /* - * Extend the httpCore, so that we can capture provided url arguments without mocking and changing its organic behavior. - */ - HttpCore httpCore = new HttpCore(options, null) { - /* Store only string representations to avoid try/catch blocks */ - List urlArgumentStack; - - @Override - public T httpExecute(URL url, Proxy proxy, String method, Param[] headers, RequestBody requestBody, boolean withCredentials, ResponseHandler responseHandler) throws AblyException { - // Store a copy of given argument - urlArgumentStack.add(url.getHost()); - - // Execute the original method without changing behavior - return super.httpExecute(url, proxy, method, headers, requestBody, withCredentials, responseHandler); - } - - public HttpCore setUrlArgumentStack(List urlArgumentStack) { - this.urlArgumentStack = urlArgumentStack; - return this; - } - }.setUrlArgumentStack(urlHostArgumentStack); - - Http http = new Http(new AsyncHttpScheduler(httpCore, options), new SyncHttpScheduler(httpCore)); - try { - HttpHelpers.ablyHttpExecute( - http, "/path/to/fallback", /* Ignore path */ - HttpConstants.Methods.GET, /* Ignore method */ - new Param[0], /* Ignore headers */ - new Param[0], /* Ignore params */ - null, /* Ignore requestBody */ - null, /* Ignore requestHandler */ - false /* Ignore requireAblyAuth */ - ); - } catch (AblyException e) { - /* Verify that, - * - an {@code AblyException} with {@code ErrorInfo} having a `50x` status code is thrown. - */ - assertThat(e.errorInfo.statusCode / 10, is(equalTo(50))); - } - - /* Verify that, - * - {code HttpCore#httpExecute} have been called with (httpMaxRetryCount + 1) URLs - * - first call executed against production rest host - * - other calls executed against a random fallback host - */ - int expectedCallCount = options.httpMaxRetryCount + 1; - assertThat(urlHostArgumentStack.size(), is(equalTo(expectedCallCount))); - assertThat(urlHostArgumentStack.get(0), is(equalTo(Defaults.HOST_REST))); - - for (int i = 1; i < expectedCallCount; i++) { - urlHostArgumentStack.get(i).matches(PATTERN_HOST_FALLBACK); - } - } - - /** - * Validates that fallbacks are disabled when ClientOptions#fallbackHosts are set to empty and the only host used is Defaults#HOST_REST - * @throws AblyException - */ - - @Test - public void http_ably_execute_null_fallbacks() throws AblyException { - ClientOptions options = new ClientOptions(); - options.tls = false; - options.fallbackHosts = new String[0]; - options.port = 7777; - - ArrayList urlHostArgumentStack = new ArrayList<>(); - - HttpCore httpCore = new HttpCore(options, null) { - List urlArgumentStack; - - @Override - public T httpExecuteWithRetry(URL url, String method, Param[] headers, RequestBody requestBody, ResponseHandler responseHandler, boolean allowAblyAuth) throws AblyException { - urlArgumentStack.add(url.getHost()); - return super.httpExecuteWithRetry(url, method, headers, requestBody, responseHandler, allowAblyAuth); - } - - public HttpCore setUrlArgumentStack(List urlArgumentStack) { - this.urlArgumentStack = urlArgumentStack; - return this; - } - }.setUrlArgumentStack(urlHostArgumentStack); - - Http http = new Http(new AsyncHttpScheduler(httpCore, options), new SyncHttpScheduler(httpCore)); - try { - HttpHelpers.ablyHttpExecute( - http, "/path/to/fallback", /* Ignore path */ - HttpConstants.Methods.GET, /* Ignore method */ - new Param[0], /* Ignore headers */ - new Param[0], /* Ignore params */ - null, /* Ignore requestBody */ - null, /* Ignore requestHandler */ - false /* Ignore requireAblyAuth */ - ); - } catch (AblyException.HostFailedException e) { - /* Verify that, - * - a {@code AblyException.HostFailedException} is thrown. - */ - assertTrue(true); - } catch (AblyException e) { - assertTrue(false); - } - - /* Validate that only one host is used and it is Defaults#HOST_REST */ - Assert.assertTrue(urlHostArgumentStack.size() == 1); - assertThat(urlHostArgumentStack.get(0), is(equalTo(Defaults.HOST_REST))); - } - - /** - * This method mocks the API behavior - *

- * Validates {@link HttpCore} is using first attempt to default primary host - * for every new HTTP request, after expiry of any fallbackRetryTimeout. - *

- *

- * Spec: RSC15e, RSC15f - *

- * - * @throws AblyException - */ - @Test - public void http_ably_execute_first_attempt_to_default() throws AblyException { - String hostExpectedPattern = PATTERN_HOST_FALLBACK; - ClientOptions options = new ClientOptions("not:a.key"); - options.httpMaxRetryCount = 1; - options.fallbackRetryTimeout = 100; - AblyRest ably = new AblyRest(options); - - HttpCore httpCore = Mockito.spy(new HttpCore(ably.options, ably.auth)); - - String responseExpected = "Lorem Ipsum"; - ArgumentCaptor url = ArgumentCaptor.forClass(URL.class); - - /* Partially mock httpCore */ - Answer answer = new GrumpyAnswer( - 1, /* Throw exception */ - AblyException.fromErrorInfo(ErrorInfo.fromResponseStatus("Internal Server Error", 500)), /* That is HostFailedException */ - responseExpected /* Then return a valid response with second call */ - ); - - doAnswer(answer) /* Behave as defined above */ - .when(httpCore) /* when following method is executed on {@code HttpCore} instance */ - .httpExecute( - url.capture(), /* capture url arguments passed down httpExecute to assert fallback behavior executed with valid fallback url */ - any(Proxy.class), /* Ignore */ - anyString(), /* Ignore */ - aryEq(new Param[0]), /* Ignore */ - any(HttpCore.RequestBody.class), /* Ignore */ - anyBoolean(), /* Ignore */ - any(HttpCore.ResponseHandler.class) /* Ignore */ - ); - - Http http = new Http(new AsyncHttpScheduler(httpCore, options), new SyncHttpScheduler(httpCore)); - String responseActual = (String) HttpHelpers.ablyHttpExecute( - http, "", /* Ignore */ - "", /* Ignore */ - new Param[0], /* Ignore */ - new Param[0], /* Ignore */ - mock(HttpCore.RequestBody.class), /* Ignore */ - mock(HttpCore.ResponseHandler.class), /* Ignore */ - false /* Ignore */ - ); - - assertThat("Unexpected default primary host", url.getAllValues().get(0).getHost(), is(equalTo(Defaults.HOST_REST))); - assertThat("Unexpected host fallback", url.getAllValues().get(1).getHost().matches(hostExpectedPattern), is(true)); - assertThat("Unexpected response", responseActual, is(equalTo(responseExpected))); - - /* wait for expiry of fallbackRetryTimeout */ - try { - Thread.sleep(200L); - } catch(InterruptedException ie) {} - - String responseActual2 = (String) HttpHelpers.ablyHttpExecute( - http, "", /* Ignore */ - "", /* Ignore */ - new Param[0], /* Ignore */ - new Param[0], /* Ignore */ - mock(HttpCore.RequestBody.class), /* Ignore */ - mock(HttpCore.ResponseHandler.class), /* Ignore */ - false /* Ignore */ - ); - - /* Verify call causes captor to capture same arguments thrice. - * Do the validation, after we completed the {@code ArgumentCaptor} related assertions */ - verify(httpCore, times(3)) - .httpExecute( /* Just validating call counter. Ignore following parameters */ - any(URL.class), /* Ignore */ - any(Proxy.class), /* Ignore */ - anyString(), /* Ignore */ - aryEq(new Param[0]), /* Ignore */ - any(HttpCore.RequestBody.class), /* Ignore */ - anyBoolean(), /* Ignore */ - any(HttpCore.ResponseHandler.class) /* Ignore */ - ); - assertThat("Unexpected default primary host", url.getAllValues().get(2).getHost(), is(equalTo(Defaults.HOST_REST))); - assertThat("Unexpected response", responseActual2, is(equalTo(responseExpected))); - } - - /** - * This method mocks the API behavior - *

- * Validates {@link HttpCore} is using overriden host for every new HTTP request, - * even if a previous request to that endpoint has failed. - *

- *

- * Spec: RSC15e - *

- * - * @throws AblyException - */ - @Test - public void http_ably_execute_overriden_host() throws AblyException { - final String fakeHost = "fake.ably.io"; - ClientOptions options = new ClientOptions("not:a.key"); - options.restHost = fakeHost; - AblyRest ably = new AblyRest(options); - - HttpCore httpCore = Mockito.spy(new HttpCore(ably.options, ably.auth)); - - String responseExpected = "Lorem Ipsum"; - ArgumentCaptor url = ArgumentCaptor.forClass(URL.class); - - /* Partially mock httpCore */ - Answer answer = new GrumpyAnswer( - 1, /* Throw exception */ - AblyException.fromErrorInfo(ErrorInfo.fromResponseStatus("Internal Server Error", 500)), /* That is HostFailedException */ - responseExpected /* Then return a valid response with second call */ - ); - - doAnswer(answer) /* Behave as defined above */ - .when(httpCore) /* when following method is executed on {@code HttpCore} instance */ - .httpExecute( - url.capture(), /* capture url arguments passed down httpExecute to assert fallback behavior executed with valid fallback url */ - any(Proxy.class), /* Ignore */ - anyString(), /* Ignore */ - aryEq(new Param[0]), /* Ignore */ - any(HttpCore.RequestBody.class), /* Ignore */ - anyBoolean(), /* Ignore */ - any(HttpCore.ResponseHandler.class) /* Ignore */ - ); - - Http http = new Http(new AsyncHttpScheduler(httpCore, options), new SyncHttpScheduler(httpCore)); - try { - HttpHelpers.ablyHttpExecute( - http, "", /* Ignore */ - "", /* Ignore */ - new Param[0], /* Ignore */ - new Param[0], /* Ignore */ - mock(HttpCore.RequestBody.class), /* Ignore */ - mock(HttpCore.ResponseHandler.class), /* Ignore */ - false /* Ignore */ - ); - } catch (AblyException e) { - /* Verify that, - * - an {@code AblyException} with {@code ErrorInfo} having the 500 error from above - */ - ErrorInfo expectedErrorInfo = new ErrorInfo("Internal Server Error", 500, 50000); - assertThat(e, new ErrorInfoMatcher(expectedErrorInfo)); - } - - assertThat("Unexpected host", url.getAllValues().get(0).getHost(), is(equalTo(fakeHost))); - - try { - HttpHelpers.ablyHttpExecute( - http, "", /* Ignore */ - "", /* Ignore */ - new Param[0], /* Ignore */ - new Param[0], /* Ignore */ - mock(HttpCore.RequestBody.class), /* Ignore */ - mock(HttpCore.ResponseHandler.class), /* Ignore */ - false /* Ignore */ - ); - } catch (AblyException e) { - /* Verify that, - * - an {@code AblyException} with {@code ErrorInfo} having the 500 error from above - */ - ErrorInfo expectedErrorInfo = new ErrorInfo("Internal Server Error", 500, 50000); - assertThat(e, new ErrorInfoMatcher(expectedErrorInfo)); - } - - /* Verify call causes captor to capture same arguments twice. - * Do the validation, after we completed the {@code ArgumentCaptor} related assertions */ - verify(httpCore, times(2)) - .httpExecute( /* Just validating call counter. Ignore following parameters */ - any(URL.class), /* Ignore */ - any(Proxy.class), /* Ignore */ - anyString(), /* Ignore */ - aryEq(new Param[0]), /* Ignore */ - any(HttpCore.RequestBody.class), /* Ignore */ - anyBoolean(), /* Ignore */ - any(HttpCore.ResponseHandler.class) /* Ignore */ - ); - assertThat("Unexpected host", url.getAllValues().get(1).getHost(), is(equalTo(fakeHost))); - } - - /** - * This method mocks the API behavior - *

- * Validates {@link HttpCore} isn't using any fallback hosts if {@link ClientOptions#fallbackHosts} - * array is empty. - *

- *

- * Spec: RSC15a - *

- * - * @throws AblyException - */ - @Test - public void http_ably_execute_empty_fallback_array() throws AblyException { - ClientOptions options = new ClientOptions("not:a.key"); - options.fallbackHosts = new String[0]; - AblyRest ably = new AblyRest(options); - - HttpCore httpCore = Mockito.spy(new HttpCore(ably.options, ably.auth)); - - String responseExpected = "Lorem Ipsum"; - ArgumentCaptor url = ArgumentCaptor.forClass(URL.class); - - /* Partially mock httpCore */ - Answer answer = new GrumpyAnswer( - 2, /* Throw exception twice (2) */ - AblyException.fromErrorInfo(ErrorInfo.fromResponseStatus("Internal Server Error", 500)), /* That is HostFailedException */ - responseExpected /* Then return a valid response with third call */ - ); - - doAnswer(answer) /* Behave as defined above */ - .when(httpCore) /* when following method is executed on {@code HttpCore} instance */ - .httpExecute( - url.capture(), /* capture url arguments passed down httpExecute to assert fallback behavior executed with valid fallback url */ - any(Proxy.class), /* Ignore */ - anyString(), /* Ignore */ - aryEq(new Param[0]), /* Ignore */ - any(HttpCore.RequestBody.class), /* Ignore */ - anyBoolean(), /* Ignore */ - any(HttpCore.ResponseHandler.class) /* Ignore */ - ); - - Http http = new Http(new AsyncHttpScheduler(httpCore, options), new SyncHttpScheduler(httpCore)); - try { - HttpHelpers.ablyHttpExecute( - http, "", /* Ignore */ - "", /* Ignore */ - new Param[0], /* Ignore */ - new Param[0], /* Ignore */ - mock(HttpCore.RequestBody.class), /* Ignore */ - mock(HttpCore.ResponseHandler.class), /* Ignore */ - false /* Ignore */ - ); - } catch (AblyException e) { - /* Verify that, - * - an {@code AblyException} with {@code ErrorInfo} with the 500 error from above. - */ - ErrorInfo expectedErrorInfo = new ErrorInfo("Internal Server Error", 500, 50000); - assertThat(e, new ErrorInfoMatcher(expectedErrorInfo)); - } - - /* Verify call causes captor to capture same arguments once. - * Do the validation, after we completed the {@code ArgumentCaptor} related assertions */ - verify(httpCore, times(1)) - .httpExecute( /* Just validating call counter. Ignore following parameters */ - any(URL.class), /* Ignore */ - any(Proxy.class), /* Ignore */ - anyString(), /* Ignore */ - aryEq(new Param[0]), /* Ignore */ - any(HttpCore.RequestBody.class), /* Ignore */ - anyBoolean(), /* Ignore */ - any(HttpCore.ResponseHandler.class) /* Ignore */ - ); - assertThat("Unexpected host", url.getAllValues().get(0).getHost(), is(equalTo(Defaults.HOST_REST))); - } - - /** - *

- * Validates {@code HttpCore} performs fallback behavior with custom {@link ClientOptions#fallbackHosts} - * array httpMaxRetryCount number of times at most, - * when host & fallback hosts are unreachable. Then, finally throws an error. - *

- *

- * Spec: RSC15a - *

- * - * @throws Exception - */ - @Test - public void http_ably_execute_custom_fallback_array() throws AblyException { - final String[] expectedFallbackHosts = new String[]{"f.ably-realtime.com", "g.ably-realtime.com", "h.ably-realtime.com", "i.ably-realtime.com", "j.ably-realtime.com"}; - final List fallbackHostsList = Arrays.asList(expectedFallbackHosts); - - ClientOptions options = new ClientOptions("not.a:key"); - options.fallbackHosts = expectedFallbackHosts; - int expectedCallCount = options.httpMaxRetryCount + 1; - AblyRest ably = new AblyRest(options); - - HttpCore httpCore = Mockito.spy(new HttpCore(ably.options, ably.auth)); - - String responseExpected = "Lorem Ipsum"; - ArgumentCaptor url = ArgumentCaptor.forClass(URL.class); - - /* Partially mock httpCore */ - Answer answer = new GrumpyAnswer( - options.httpMaxRetryCount, /* Throw exception options.httpMaxRetryCount */ - AblyException.fromErrorInfo(ErrorInfo.fromResponseStatus("Internal Server Error", 500)), /* That is HostFailedException */ - responseExpected /* Then return a valid response with third call */ - ); - - doAnswer(answer) /* Behave as defined above */ - .when(httpCore) /* when following method is executed on {@code HttpCore} instance */ - .httpExecute( - url.capture(), /* capture url arguments passed down httpExecute to assert fallback behavior executed with valid fallback url */ - any(Proxy.class), /* Ignore */ - anyString(), /* Ignore */ - aryEq(new Param[0]), /* Ignore */ - any(HttpCore.RequestBody.class), /* Ignore */ - anyBoolean(), /* Ignore */ - any(HttpCore.ResponseHandler.class) /* Ignore */ - ); - - Http http = new Http(new AsyncHttpScheduler(httpCore, options), new SyncHttpScheduler(httpCore)); - String responseActual = (String) HttpHelpers.ablyHttpExecute( - http, "", /* Ignore */ - "", /* Ignore */ - new Param[0], /* Ignore */ - new Param[0], /* Ignore */ - mock(HttpCore.RequestBody.class), /* Ignore */ - mock(HttpCore.ResponseHandler.class), /* Ignore */ - false /* Ignore */ - ); - - /* Verify {code HttpCore#httpExecute} have been called (httpMaxRetryCount + 1) times */ - verify(httpCore, times(expectedCallCount)) - .httpExecute( /* Just validating call counter. Ignore following parameters */ - any(URL.class), /* Ignore */ - any(Proxy.class), /* Ignore */ - anyString(), /* Ignore */ - aryEq(new Param[0]), /* Ignore */ - any(HttpCore.RequestBody.class), /* Ignore */ - anyBoolean(), /* Ignore */ - any(HttpCore.ResponseHandler.class) /* Ignore */ - ); - - /* Verify that, - * - delivered expected response - * - first call executed against production rest host - * - other calls executed against a random custom fallback host */ - List allValues = url.getAllValues(); - assertThat("Unexpected response", responseActual, is(equalTo(responseExpected))); - assertThat("Unexpected default primary host", allValues.get(0).getHost(), is(equalTo(Defaults.HOST_REST))); - for (int i = 1; i < allValues.size(); i++) { - assertThat("Unexpected host fallback", fallbackHostsList.contains(allValues.get(i).getHost()), is(true)); - } - } - - /** - * Validates that HttpCore uses ClientOptions#fallbackHosts when there is AblyException.HostFailedException thrown - * @throws AblyException - */ - - @Test - public void http_ably_execute_custom_fallback() throws AblyException { - ClientOptions options = new ClientOptions(); - options.tls = false; - options.fallbackHosts = CUSTOM_HOSTS; - options.port = 7777; - - ArrayList urlHostArgumentStack = new ArrayList<>(); - - HttpCore httpCore = new HttpCore(options, null) { - /* Store only string representations to avoid try/catch blocks */ - List urlArgumentStack; - - @Override - public T httpExecuteWithRetry(URL url, String method, Param[] headers, RequestBody requestBody, ResponseHandler responseHandler, boolean allowAblyAuth) throws AblyException { - urlArgumentStack.add(url.getHost()); - /* verify if fallback hosts are from specified list */ - if(!url.getHost().equals(Defaults.HOST_REST)) - assertTrue(Arrays.asList(CUSTOM_HOSTS).contains(url.getHost())); - - return super.httpExecuteWithRetry(url, method, headers, requestBody, responseHandler, allowAblyAuth); - } - - public HttpCore setUrlArgumentStack(List urlArgumentStack) { - this.urlArgumentStack = urlArgumentStack; - return this; - } - }.setUrlArgumentStack(urlHostArgumentStack); - - Http http = new Http(new AsyncHttpScheduler(httpCore, options), new SyncHttpScheduler(httpCore)); - try { - HttpHelpers.ablyHttpExecute( - http, "/path/to/fallback", /* Ignore path */ - HttpConstants.Methods.GET, /* Ignore method */ - new Param[0], /* Ignore headers */ - new Param[0], /* Ignore params */ - null, /* Ignore requestBody */ - null, /* Ignore requestHandler */ - false /* Ignore requireAblyAuth */ - ); - } catch (AblyException.HostFailedException e) { - /* Verify that, - * - a {@code AblyException.HostFailedException} is thrown. - */ - assertTrue(true); - } catch (AblyException e) { - assertTrue(false); - } - - int expectedCallCount = options.httpMaxRetryCount + 1; - Assert.assertTrue(urlHostArgumentStack.size() == expectedCallCount); - assertThat(urlHostArgumentStack.get(0), is(equalTo(Defaults.HOST_REST))); - - for (int i = 1; i < expectedCallCount; i++) { - Assert.assertTrue(urlHostArgumentStack.get(i).matches(CUSTOM_PATTERN_HOST_FALLBACK)); - } - } - - /** - * This method mocks the API behavior - *

- * Validates httpCore is not using any fallback host when we receive valid response from httpCore's host - *

- *

- * Spec: RSC15a - *

- * - * @throws Exception - */ - @Test - public void http_execute_nofallback() throws Exception { - HttpCore httpCore = Mockito.spy(new HttpCore(new ClientOptions(), null)); - - String responseExpected = "Lorem Ipsum"; - String hostExpected = Defaults.HOST_REST; - ArgumentCaptor url = ArgumentCaptor.forClass(URL.class); - - /* Partially mock httpCore */ - doReturn(responseExpected) /* Provide response */ - .when(httpCore) /* when following method is executed on {@code HttpCore} instance */ - .httpExecute( - url.capture(), /* capture url arguments passed down httpExecute to assert fallback behavior executed with valid rest host */ - any(Proxy.class), /* Ignore */ - anyString(), /* Ignore */ - aryEq(new Param[0]), /* Ignore */ - any(HttpCore.RequestBody.class), /* Ignore */ - anyBoolean(), /* Ignore */ - any(HttpCore.ResponseHandler.class) /* Ignore */ - ); - - Http http = new Http(new AsyncHttpScheduler(httpCore, new ClientOptions()), new SyncHttpScheduler(httpCore)); - String responseActual = (String) HttpHelpers.ablyHttpExecute( - http, "", /* Ignore */ - "", /* Ignore */ - new Param[0], /* Ignore */ - new Param[0], /* Ignore */ - mock(HttpCore.RequestBody.class), /* Ignore */ - mock(HttpCore.ResponseHandler.class), /* Ignore */ - false /* Ignore */ - ); - - - /* Verify - * - httpCore call executed once, - * - with given host, - * - and delivered expected response */ - verify(httpCore, times(1)) - .httpExecute( /* Just validating call counter. Ignore following parameters */ - url.capture(), /* capture url arguments passed down httpExecute to assert fallback behavior executed with valid rest host */ - any(Proxy.class), /* Ignore */ - anyString(), /* Ignore */ - aryEq(new Param[0]), /* Ignore */ - any(HttpCore.RequestBody.class), /* Ignore */ - anyBoolean(), /* Ignore */ - any(HttpCore.ResponseHandler.class) /* Ignore */ - ); - - assertThat(url.getValue().getHost(), is(equalTo(hostExpected))); - assertThat(responseActual, is(equalTo(responseExpected))); - } - - /** - * This method mocks the API behavior - *

- * Validates httpCore is using a fallback host when HostFailedException thrown - *

- *

- * Spec: RSC15a - *

- * - * @throws Exception - */ - @Test - public void http_execute_singlefallback() throws Exception { - HttpCore httpCore = Mockito.spy(new HttpCore(new ClientOptions(), null)); - - String hostExpectedPattern = PATTERN_HOST_FALLBACK; - String responseExpected = "Lorem Ipsum"; - ArgumentCaptor url = ArgumentCaptor.forClass(URL.class); - - /* Partially mock httpCore */ - Answer answer = new GrumpyAnswer( - 1, /* Throw exception once (1) */ - AblyException.fromErrorInfo(ErrorInfo.fromResponseStatus("Internal Server Error", 500)), /* That is HostFailedException */ - responseExpected /* Then return a valid response with the second call */ - ); - - doAnswer(answer) /* Behave as defined above */ - .when(httpCore) /* when following method is executed on {@code HttpCore} instance */ - .httpExecute( - url.capture(), /* capture url arguments passed down httpExecute to assert fallback behavior executed with valid rest host */ - any(Proxy.class), /* Ignore */ - anyString(), /* Ignore */ - aryEq(new Param[0]), /* Ignore */ - any(HttpCore.RequestBody.class), /* Ignore */ - anyBoolean(), /* Ignore */ - any(HttpCore.ResponseHandler.class) /* Ignore */ - ); - - /* Call method with real parameters */ - Http http = new Http(new AsyncHttpScheduler(httpCore, new ClientOptions()), new SyncHttpScheduler(httpCore)); - String responseActual = (String) HttpHelpers.ablyHttpExecute( - http, "", /* Ignore */ - "", /* Ignore */ - new Param[0], /* Ignore */ - new Param[0], /* Ignore */ - mock(HttpCore.RequestBody.class), /* Ignore */ - mock(HttpCore.ResponseHandler.class), /* Ignore */ - false /* Ignore */ - ); - - - /* Verify - * - httpCore call executed twice (one for prod host and 1 for fallback), - * - last call performed against a fallback host, - * - and fallback host delivered expected response */ - - verify(httpCore, times(2)) - .httpExecute( /* Just validating call counter. Ignore following parameters */ - url.capture(), /* capture url arguments passed down httpExecute to assert fallback behavior executed with valid rest host */ - any(Proxy.class), /* Ignore */ - anyString(), /* Ignore */ - aryEq(new Param[0]), /* Ignore */ - any(HttpCore.RequestBody.class), /* Ignore */ - anyBoolean(), /* Ignore */ - any(HttpCore.ResponseHandler.class) /* Ignore */ - ); - - assertThat(url.getValue().getHost().matches(hostExpectedPattern), is(true)); - assertThat(responseActual, is(equalTo(responseExpected))); - } - - /** - * This method mocks the API behavior - *

- * Validates httpCore is using different hosts when HostFailedException happened multiple times. - *

- *

- * Spec: RSC15a - *

- * - * @throws Exception - */ - @Test - public void http_execute_multiplefallback() throws Exception { - HttpCore httpCore = Mockito.spy(new HttpCore(new ClientOptions(), null)); - - String hostExpectedPattern = PATTERN_HOST_FALLBACK; - String responseExpected = "Lorem Ipsum"; - ArgumentCaptor url = ArgumentCaptor.forClass(URL.class); - - /* Partially mock httpCore */ - Answer answer = new GrumpyAnswer( - 2, /* Throw exception twice (2) */ - AblyException.fromErrorInfo(ErrorInfo.fromResponseStatus("Internal Server Error", 500)), /* That is HostFailedException */ - responseExpected /* Then return a valid response with third call */ - ); - - doAnswer(answer) /* Behave as defined above */ - .when(httpCore) /* when following method is executed on {@code HttpCore} instance */ - .httpExecute( - url.capture(), /* capture url arguments passed down httpExecute to assert fallback behavior executed with valid rest host */ - any(Proxy.class), /* Ignore */ - anyString(), /* Ignore */ - aryEq(new Param[0]), /* Ignore */ - any(HttpCore.RequestBody.class), /* Ignore */ - anyBoolean(), /* Ignore */ - any(HttpCore.ResponseHandler.class) /* Ignore */ - ); - - Http http = new Http(new AsyncHttpScheduler(httpCore, new ClientOptions()), new SyncHttpScheduler(httpCore)); - String responseActual = (String) HttpHelpers.ablyHttpExecute( - http, "", /* Ignore */ - "", /* Ignore */ - new Param[0], /* Ignore */ - new Param[0], /* Ignore */ - mock(HttpCore.RequestBody.class), /* Ignore */ - mock(HttpCore.ResponseHandler.class), /* Ignore */ - false /* Ignore */ - ); - - - /* Verify - * - httpCore call executed thrice, - * - with 2 fallback hosts, - * - each host having a unique value, - * - and delivered expected response */ - - assertThat(url.getAllValues().get(1).getHost().matches(hostExpectedPattern), is(true)); - assertThat(url.getAllValues().get(2).getHost().matches(hostExpectedPattern), is(true)); - assertThat(url.getAllValues().get(1).toString(), is(not(equalTo(url.getAllValues().get(2).toString())))); - - assertThat(responseActual, is(equalTo(responseExpected))); - - /* Verify call causes captor to capture same arguments twice. - * Do the validation, after we completed the {@code ArgumentCaptor} related assertions */ - verify(httpCore, times(3)) - .httpExecute( /* Just validating call counter. Ignore following parameters */ - url.capture(), /* capture url arguments passed down httpExecute to assert fallback behavior executed with valid rest host */ - any(Proxy.class), /* Ignore */ - anyString(), /* Ignore */ - aryEq(new Param[0]), /* Ignore */ - any(HttpCore.RequestBody.class), /* Ignore */ - anyBoolean(), /* Ignore */ - any(HttpCore.ResponseHandler.class) /* Ignore */ - ); - } - - /** - * This method mocks the API behavior - *

- * Validates {@code HttpCore} is using its a successful fallback host first - * when a consecutive call happens within the fallbackRetryTimeout, so long - * as calls to that fallback continue to succeed - *

- *

- * Spec: RSC15f - *

- * - * @throws Exception - */ - @Test - public void http_execute_fallback_success_timeout_unexpired() throws Exception { - ClientOptions opts = new ClientOptions(); - opts.fallbackRetryTimeout = 2000L; - HttpCore httpCore = Mockito.spy(new HttpCore(opts, null)); - - String hostExpected = Defaults.HOST_REST; - ArgumentCaptor url = ArgumentCaptor.forClass(URL.class); - - /* Partially mock httpCore */ - Answer answer = new GrumpyAnswer( - 1, /* Throw exception once (1) */ - AblyException.fromErrorInfo(ErrorInfo.fromResponseStatus("Internal Server Error", 500)), /* That is HostFailedException */ - "Lorem Ipsum" /* Ignore */ - ); - - doAnswer(answer) /* Behave as defined above */ - .when(httpCore) /* when following method is executed on {@code HttpCore} instance */ - .httpExecute( - url.capture(), /* capture url arguments passed down httpExecute to assert fallback behavior executed with valid rest host */ - any(Proxy.class), /* Ignore */ - anyString(), /* Ignore */ - aryEq(new Param[0]), /* Ignore */ - any(HttpCore.RequestBody.class), /* Ignore */ - anyBoolean(), /* Ignore */ - any(HttpCore.ResponseHandler.class) /* Ignore */ - ); - - Http http = new Http(new AsyncHttpScheduler(httpCore, new ClientOptions()), new SyncHttpScheduler(httpCore)); - HttpHelpers.ablyHttpExecute( - http, "", /* Ignore */ - "", /* Ignore */ - new Param[0], /* Ignore */ - new Param[0], /* Ignore */ - mock(HttpCore.RequestBody.class), /* Ignore */ - mock(HttpCore.ResponseHandler.class), /* Ignore */ - false /* Ignore */ - ); - - /* Verify there was a fallback with first call */ - String successFallback = url.getValue().getHost(); - assertThat(successFallback.matches(PATTERN_HOST_FALLBACK), is(true)); - - /* wait for a short time, but not enough for the fallbackRetryTimeout to expire */ - try { Thread.sleep(200L); } catch(InterruptedException ie) {} - - /* Update behavior to succeed on next attempt */ - url = ArgumentCaptor.forClass(URL.class); - doReturn("Lorem Ipsum") /* Return some response string that we will ignore */ - .when(httpCore) /* when following method is executed on {@code HttpCore} instance */ - .httpExecute( - url.capture(), /* capture url arguments passed down httpExecute to assert fallback behavior executed with valid rest host */ - any(Proxy.class), /* Ignore */ - anyString(), /* Ignore */ - aryEq(new Param[0]), /* Ignore */ - any(HttpCore.RequestBody.class), /* Ignore */ - anyBoolean(), /* Ignore */ - any(HttpCore.ResponseHandler.class) /* Ignore */ - ); - - HttpHelpers.ablyHttpExecute( - http, "", /* Ignore */ - "", /* Ignore */ - new Param[0], /* Ignore */ - new Param[0], /* Ignore */ - mock(HttpCore.RequestBody.class), /* Ignore */ - mock(HttpCore.ResponseHandler.class), /* Ignore */ - false /* Ignore */ - ); - - /* Verify second call was called with the cached fallback host */ - assertThat(url.getValue().getHost().equals(successFallback), is(true)); - } - - /** - * This method mocks the API behavior - *

- * Validates {@code HttpCore} reverts to using the primary endpoint - * if a request to a preferred fallback fails (after initially succeeding - * and being cached) - *

- *

- * Spec: RSC15f - *

- * - * @throws Exception - */ - @Test - public void http_execute_fallback_failure_timeout_unexpired() throws Exception { - ClientOptions opts = new ClientOptions(); - opts.fallbackRetryTimeout = 2000L; - HttpCore httpCore = Mockito.spy(new HttpCore(opts, null)); - - String primaryHost = Defaults.HOST_REST; - ArgumentCaptor url = ArgumentCaptor.forClass(URL.class); - - /* Partially mock httpCore */ - Answer answer = new GrumpyAnswer( - 1, /* Throw exception once */ - AblyException.fromErrorInfo(ErrorInfo.fromResponseStatus("Internal Server Error", 500)), /* That is HostFailedException */ - "Lorem Ipsum" /* Ignore */ - ); - - doAnswer(answer) /* Behave as defined above */ - .when(httpCore) /* when following method is executed on {@code HttpCore} instance */ - .httpExecute( - url.capture(), /* capture url arguments passed down httpExecute to assert fallback behavior executed with valid rest host */ - any(Proxy.class), /* Ignore */ - anyString(), /* Ignore */ - aryEq(new Param[0]), /* Ignore */ - any(HttpCore.RequestBody.class), /* Ignore */ - anyBoolean(), /* Ignore */ - any(HttpCore.ResponseHandler.class) /* Ignore */ - ); - - Http http = new Http(new AsyncHttpScheduler(httpCore, new ClientOptions()), new SyncHttpScheduler(httpCore)); - HttpHelpers.ablyHttpExecute( - http, "", /* Ignore */ - "", /* Ignore */ - new Param[0], /* Ignore */ - new Param[0], /* Ignore */ - mock(HttpCore.RequestBody.class), /* Ignore */ - mock(HttpCore.ResponseHandler.class), /* Ignore */ - false /* Ignore */ - ); - - /* Verify there was a fallback with first call */ - String failFallback = url.getValue().getHost(); - assertThat(failFallback.matches(PATTERN_HOST_FALLBACK), is(true)); - - /* wait for a short time, but not enough for the fallbackRetryTimeout to expire */ - try { Thread.sleep(200L); } catch(InterruptedException ie) {} - - /* reset the mocked response so that the next request fails */ - answer = new GrumpyAnswer( - 1, /* Throw exception once */ - AblyException.fromErrorInfo(ErrorInfo.fromResponseStatus("Internal Server Error", 500)), /* That is HostFailedException */ - "Lorem Ipsum" /* Ignore */ - ); - - doAnswer(answer) /* Behave as defined above */ - .when(httpCore) /* when following method is executed on {@code HttpCore} instance */ - .httpExecute( - url.capture(), /* capture url arguments passed down httpExecute to assert fallback behavior executed with valid rest host */ - any(Proxy.class), /* Ignore */ - anyString(), /* Ignore */ - aryEq(new Param[0]), /* Ignore */ - any(HttpCore.RequestBody.class), /* Ignore */ - anyBoolean(), /* Ignore */ - any(HttpCore.ResponseHandler.class) /* Ignore */ - ); - - HttpHelpers.ablyHttpExecute( - http, "", /* Ignore */ - "", /* Ignore */ - new Param[0], /* Ignore */ - new Param[0], /* Ignore */ - mock(HttpCore.RequestBody.class), /* Ignore */ - mock(HttpCore.ResponseHandler.class), /* Ignore */ - false /* Ignore */ - ); - - /* Verify second call was called with httpCore host */ - assertThat(url.getValue().getHost().equals(primaryHost), is(true)); - } - - /** - * This method mocks the API behavior - *

- * Validates {@code HttpCore} is using the primary host first - * when a consecutive call happens after the fallbackRetryTimeout - *

- *

- * Spec: RSC15f - *

- * - * @throws Exception - */ - @Test - public void http_execute_fallback_timeout_expired() throws Exception { - ClientOptions opts = new ClientOptions(); - opts.fallbackRetryTimeout = 2000L; - HttpCore httpCore = Mockito.spy(new HttpCore(opts, null)); - - String hostExpected = Defaults.HOST_REST; - ArgumentCaptor url = ArgumentCaptor.forClass(URL.class); - - /* Partially mock httpCore */ - Answer answer = new GrumpyAnswer( - 1, /* Throw exception once (1) */ - AblyException.fromErrorInfo(ErrorInfo.fromResponseStatus("Internal Server Error", 500)), /* That is HostFailedException */ - "Lorem Ipsum" /* Ignore */ - ); - - doAnswer(answer) /* Behave as defined above */ - .when(httpCore) /* when following method is executed on {@code HttpCore} instance */ - .httpExecute( - url.capture(), /* capture url arguments passed down httpExecute to assert fallback behavior executed with valid rest host */ - any(Proxy.class), /* Ignore */ - anyString(), /* Ignore */ - aryEq(new Param[0]), /* Ignore */ - any(HttpCore.RequestBody.class), /* Ignore */ - anyBoolean(), /* Ignore */ - any(HttpCore.ResponseHandler.class) /* Ignore */ - ); - - Http http = new Http(new AsyncHttpScheduler(httpCore, new ClientOptions()), new SyncHttpScheduler(httpCore)); - HttpHelpers.ablyHttpExecute( - http, "", /* Ignore */ - "", /* Ignore */ - new Param[0], /* Ignore */ - new Param[0], /* Ignore */ - mock(HttpCore.RequestBody.class), /* Ignore */ - mock(HttpCore.ResponseHandler.class), /* Ignore */ - false /* Ignore */ - ); - - /* Verify there was a fallback with first call */ - assertThat(url.getValue().getHost().matches(PATTERN_HOST_FALLBACK), is(true)); - - /* wait for the fallbackRetryTimeout to expire */ - try { Thread.sleep(2000L); } catch(InterruptedException ie) {} - - /* Update behavior to perform a call without a fallback */ - url = ArgumentCaptor.forClass(URL.class); - doReturn("Lorem Ipsum") /* Return some response string that we will ignore */ - .when(httpCore) /* when following method is executed on {@code HttpCore} instance */ - .httpExecute( - url.capture(), /* capture url arguments passed down httpExecute to assert fallback behavior executed with valid rest host */ - any(Proxy.class), /* Ignore */ - anyString(), /* Ignore */ - aryEq(new Param[0]), /* Ignore */ - any(HttpCore.RequestBody.class), /* Ignore */ - anyBoolean(), /* Ignore */ - any(HttpCore.ResponseHandler.class) /* Ignore */ - ); - - HttpHelpers.ablyHttpExecute( - http, "", /* Ignore */ - "", /* Ignore */ - new Param[0], /* Ignore */ - new Param[0], /* Ignore */ - mock(HttpCore.RequestBody.class), /* Ignore */ - mock(HttpCore.ResponseHandler.class), /* Ignore */ - false /* Ignore */ - ); - - /* Verify second call was called with httpCore host */ - assertThat(url.getValue().getHost().equals(hostExpected), is(true)); - } - - /** - * This method mocks the API behavior - *

- * Validates {@code HttpCore} is throwing an exception, - * when connection to host failed more than allowed count ({@code Defaults.HTTP_MAX_RETRY_COUNT}) - *

- *

- * Spec: - - *

- * - * @throws Exception - */ - @Test - public void http_execute_excessivefallback() throws AblyException { - ClientOptions options = new ClientOptions(); - HttpCore httpCore = Mockito.spy(new HttpCore(options, null)); - - ArgumentCaptor url = ArgumentCaptor.forClass(URL.class); - int excessiveFallbackCount = options.httpMaxRetryCount + 1; - - /* Partially mock httpCore */ - Answer answer = new GrumpyAnswer( - excessiveFallbackCount, /* Throw exception more than httpMaxRetryCount number of times */ - AblyException.fromErrorInfo(ErrorInfo.fromResponseStatus("Internal Server Error", 500)), /* That is HostFailedException */ - "Lorem Ipsum" /* Ignore */ - ); - - doAnswer(answer) /* Behave as defined above */ - .when(httpCore) /* when following method is executed on {@code HttpCore} instance */ - .httpExecute( - url.capture(), /* capture url arguments passed down httpExecute to assert fallback behavior executed with valid rest host */ - any(Proxy.class), /* Ignore */ - anyString(), /* Ignore */ - aryEq(new Param[0]), /* Ignore */ - any(HttpCore.RequestBody.class), /* Ignore */ - anyBoolean(), /* Ignore */ - any(HttpCore.ResponseHandler.class) /* Ignore */ - ); - - - /* Verify - * - ably exception with 50x status code is thrown - */ - thrown.expect(AblyException.HostFailedException.class); - - Http http = new Http(new AsyncHttpScheduler(httpCore, new ClientOptions()), new SyncHttpScheduler(httpCore)); - HttpHelpers.ablyHttpExecute( - http, "", /* Ignore */ - "", /* Ignore */ - new Param[0], /* Ignore */ - new Param[0], /* Ignore */ - mock(HttpCore.RequestBody.class), /* Ignore */ - mock(HttpCore.ResponseHandler.class), /* Ignore */ - false /* Ignore */ - ); - } - - /** - *

- * Validates {@code HttpCore#httpExecute} is throwing an {@code HostFailedException}, - * when api returns a response code between 500 and 504 - *

- *

- * Spec: RSC15d - *

- * - * @throws Exception - */ - @Test - public void http_execute_response_50x() throws AblyException, MalformedURLException { - URL url; - HttpCore httpCore = new HttpCore(new ClientOptions(), null); - - AblyException.HostFailedException hfe; - - for (int statusCode = 500; statusCode <= 504; statusCode++) { - url = new URL("http://localhost:" + server.getListeningPort() + "/status/" + statusCode); - hfe = null; - - try { - String result = HttpHelpers.httpExecute(httpCore, url, HttpConstants.Methods.GET, new Param[0], null, null); - } catch (AblyException.HostFailedException e) { - hfe = e; - } catch (AblyException e) { - e.printStackTrace(); - } - - Assert.assertNotNull("Status code " + statusCode + " should throw an exception", hfe); - } - } - - /** - *

- * Validates {@code HttpCore#httpExecute} isn't throwing an {@code HostFailedException}, - * when api returns a non-server-error response code (Informational 1xx, - * Multiple Choices 3xx, Client Error 4xx) - *

- *

- * Spec: RSC15d - *

- * - * @throws Exception - */ - @Test - public void http_execute_response_non5xx() throws AblyException, MalformedURLException { - URL url; - HttpCore httpCore = new HttpCore(new ClientOptions(), null); - - /* Informational 1xx */ - - for (int statusCode = 100; statusCode <= 101; statusCode++) { - url = new URL("http://localhost:" + server.getListeningPort() + "/status/" + statusCode); - - try { - HttpHelpers.httpExecute(httpCore, url, HttpConstants.Methods.GET, new Param[0], null, null); - } catch (AblyException.HostFailedException e) { - fail("Informal status code " + statusCode + " shouldn't throw an exception"); - } catch (Exception e) { - /* non HostFailedExceptions are ignored */ - } - } - - - /* Informational 3xx */ - - for (int statusCode = 300; statusCode <= 307; statusCode++) { - url = new URL("http://localhost:" + server.getListeningPort() + "/status/" + statusCode); - - try { - HttpHelpers.httpExecute(httpCore, url, HttpConstants.Methods.GET, new Param[0], null, null); - } catch (AblyException.HostFailedException e) { - fail("Multiple choices status code " + statusCode + " shouldn't throw an exception"); - } catch (Exception e) { - /* non HostFailedExceptions are ignored */ - } - } - - - /* Informational 4xx */ - - for (int statusCode = 400; statusCode <= 417; statusCode++) { - url = new URL("http://localhost:" + server.getListeningPort() + "/status/" + statusCode); - - try { - HttpHelpers.httpExecute(httpCore, url, HttpConstants.Methods.GET, new Param[0], null, null); - } catch (AblyException.HostFailedException e) { - fail("Client error status code " + statusCode + " shouldn't throw an exception"); - } catch (Exception e) { - /* non HostFailedExceptions are ignored */ - } - } - } - - @Test - public void test_asynchttp_concurrent_default_notqueued() { - test_asynchttp_concurrent(0, 64, 5000, 20000); - } - - @Test - public void test_asynchttp_concurrent_default_queued() { - test_asynchttp_concurrent(0, 128, 10000, 25000); - } - - @Test - public void test_asynchttp_concurrent_10_notqueued() { - test_asynchttp_concurrent(10, 10, 5000, 20000); - } - - @Test - public void test_asynchttp_concurrent_11_queued() { - test_asynchttp_concurrent(10, 11, 10000, 25000); - } - - private void test_asynchttp_concurrent(int poolSize, int requestCount, int expectedMinDuration, int expectedMaxDuration) { - try { - ClientOptions options = new ClientOptions("not.a:key"); - options.tls = false; - options.restHost = TEST_SERVER_HOST; - options.port = TEST_SERVER_PORT; - if(poolSize > 0) { - options.asyncHttpThreadpoolSize = poolSize; - } - final AblyRest ablyRest = new AblyRest(options); - - final Object waiter = new Object(); - final long startTime = System.currentTimeMillis(); - final int[] counter = new int[1]; - final boolean[] error = new boolean[1]; - - /* start parallel requests */ - for(int i = 0; i < requestCount; i++) { - ablyRest.timeAsync(new Callback() { - @Override - public void onSuccess(Long result) { - synchronized(waiter) { - counter[0]++; - waiter.notify(); - } - } - - @Override - public void onError(ErrorInfo reason) { - synchronized(waiter) { - counter[0]++; - error[0] = true; - waiter.notify(); - } - fail("Unexpected error: " + reason.message); - } - }); - } - - /* wait for all requests to complete */ - while(counter[0] < requestCount) { - synchronized(waiter) { - try { - waiter.wait(); - } catch(InterruptedException ie) {} - } - } - - /* assert */ - assertEquals("Verify all requests completed", counter[0], requestCount); - assertFalse("Verify there were no errors", error[0]); - - long endTime = System.currentTimeMillis(), duration = endTime - startTime; - assertTrue("Verify duration at least minimum", duration >= expectedMinDuration); - assertTrue("Verify duration at most maximum", duration <= expectedMaxDuration); - - } catch(AblyException ae) { - ae.printStackTrace(); - } - } - - /********************************************* - * Minions - *********************************************/ - - static class GrumpyAnswer implements Answer { - private int grumpinessLevel; - private Throwable nope; - private String value; - - /** - * Throws grumpinessLevel number of nope to you and then gives its response properly. - * - * @param grumpinessLevel Quantity of nope that will be thrown into your face, each time. - * @param nope Expected nope - * @param value Expected value that will be returned after grumpiness level goes below or equal to 0. - */ - public GrumpyAnswer(int grumpinessLevel, Throwable nope, String value) { - this.grumpinessLevel = grumpinessLevel; - this.nope = nope; - this.value = value; - } - - @Override - public String answer(InvocationOnMock invocation) throws Throwable { - if (grumpinessLevel-- > 0) { - throw nope; - } - - return value; - } - } - - static class ErrorInfoMatcher extends TypeSafeMatcher { - ErrorInfo errorInfo; - - public ErrorInfoMatcher(ErrorInfo errorInfo) { - super(); - this.errorInfo = errorInfo; - } - - @Override - protected boolean matchesSafely(AblyException item) { - return errorInfo.code == item.errorInfo.code && - errorInfo.statusCode == item.errorInfo.statusCode; - } - - @Override - protected void describeMismatchSafely(AblyException item, Description mismatchDescription) { - mismatchDescription.appendText(item.errorInfo.toString()); - } - - @Override - public void describeTo(Description description) { - description.appendText(errorInfo.toString()); - } - } + private static final String PATTERN_HOST_FALLBACK = "(?i)[a-e]\\.ably-realtime.com"; + private static final String CUSTOM_PATTERN_HOST_FALLBACK = "(?i)[f-k]\\.ably-realtime.com"; + private static final String[] CUSTOM_HOSTS = { "f.ably-realtime.com", "g.ably-realtime.com", "h.ably-realtime.com", "i.ably-realtime.com", "j.ably-realtime.com", "k.ably-realtime.com" }; + private static final String TEST_SERVER_HOST = "localhost"; + private static final int TEST_SERVER_PORT = 27331; + + @Rule + public Timeout testTimeout = Timeout.seconds(60); + + @Rule + public ExpectedException thrown = ExpectedException.none(); + private static RouterNanoHTTPD server; + + @BeforeClass + public static void setUp() throws IOException { + server = new RouterNanoHTTPD(TEST_SERVER_PORT); + server.addRoute("/status/:code", StatusHandler.class); + server.addRoute("/time", TimeHandler.class); + server.start(NanoHTTPD.SOCKET_READ_TIMEOUT, true); + + while (!server.wasStarted()) { + try { + Thread.sleep(100); + } catch (InterruptedException e) { + e.printStackTrace(); + } + } + } + + @AfterClass + public static void tearDown() { + server.stop(); + } + + + /******************************************* + * Spec: RSC15 + *******************************************/ + + /** + *

+ * Validates {@code HttpCore} performs fallback behavior httpMaxRetryCount number of times at most, + * when host & fallback hosts are unreachable. Then, finally throws an error. + *

+ *

+ * Spec: RSC15a + *

+ * + * @throws Exception + */ + @Test + public void http_ably_execute_fallback() throws AblyException { + ClientOptions options = new ClientOptions(); + options.tls = false; + /* Select a port that will be refused immediately by the production host */ + options.port = 7777; + + /* Create a list to capture the host of URL arguments that get called with httpExecute method. + * This will later be used to validate hosts used for requests + */ + ArrayList urlHostArgumentStack = new ArrayList<>(4); + + /* + * Extend the httpCore, so that we can capture provided url arguments without mocking and changing its organic behavior. + */ + HttpCore httpCore = new HttpCore(options, null) { + /* Store only string representations to avoid try/catch blocks */ + List urlArgumentStack; + + @Override + public T httpExecute(URL url, Proxy proxy, String method, Param[] headers, RequestBody requestBody, boolean withCredentials, ResponseHandler responseHandler) throws AblyException { + // Store a copy of given argument + urlArgumentStack.add(url.getHost()); + + // Execute the original method without changing behavior + return super.httpExecute(url, proxy, method, headers, requestBody, withCredentials, responseHandler); + } + + public HttpCore setUrlArgumentStack(List urlArgumentStack) { + this.urlArgumentStack = urlArgumentStack; + return this; + } + }.setUrlArgumentStack(urlHostArgumentStack); + + Http http = new Http(new AsyncHttpScheduler(httpCore, options), new SyncHttpScheduler(httpCore)); + try { + HttpHelpers.ablyHttpExecute( + http, "/path/to/fallback", /* Ignore path */ + HttpConstants.Methods.GET, /* Ignore method */ + new Param[0], /* Ignore headers */ + new Param[0], /* Ignore params */ + null, /* Ignore requestBody */ + null, /* Ignore requestHandler */ + false /* Ignore requireAblyAuth */ + ); + } catch (AblyException e) { + /* Verify that, + * - an {@code AblyException} with {@code ErrorInfo} having a `50x` status code is thrown. + */ + assertThat(e.errorInfo.statusCode / 10, is(equalTo(50))); + } + + /* Verify that, + * - {code HttpCore#httpExecute} have been called with (httpMaxRetryCount + 1) URLs + * - first call executed against production rest host + * - other calls executed against a random fallback host + */ + int expectedCallCount = options.httpMaxRetryCount + 1; + assertThat(urlHostArgumentStack.size(), is(equalTo(expectedCallCount))); + assertThat(urlHostArgumentStack.get(0), is(equalTo(Defaults.HOST_REST))); + + for (int i = 1; i < expectedCallCount; i++) { + urlHostArgumentStack.get(i).matches(PATTERN_HOST_FALLBACK); + } + } + + /** + * Validates that fallbacks are disabled when ClientOptions#fallbackHosts are set to empty and the only host used is Defaults#HOST_REST + * @throws AblyException + */ + + @Test + public void http_ably_execute_null_fallbacks() throws AblyException { + ClientOptions options = new ClientOptions(); + options.tls = false; + options.fallbackHosts = new String[0]; + options.port = 7777; + + ArrayList urlHostArgumentStack = new ArrayList<>(); + + HttpCore httpCore = new HttpCore(options, null) { + List urlArgumentStack; + + @Override + public T httpExecuteWithRetry(URL url, String method, Param[] headers, RequestBody requestBody, ResponseHandler responseHandler, boolean allowAblyAuth) throws AblyException { + urlArgumentStack.add(url.getHost()); + return super.httpExecuteWithRetry(url, method, headers, requestBody, responseHandler, allowAblyAuth); + } + + public HttpCore setUrlArgumentStack(List urlArgumentStack) { + this.urlArgumentStack = urlArgumentStack; + return this; + } + }.setUrlArgumentStack(urlHostArgumentStack); + + Http http = new Http(new AsyncHttpScheduler(httpCore, options), new SyncHttpScheduler(httpCore)); + try { + HttpHelpers.ablyHttpExecute( + http, "/path/to/fallback", /* Ignore path */ + HttpConstants.Methods.GET, /* Ignore method */ + new Param[0], /* Ignore headers */ + new Param[0], /* Ignore params */ + null, /* Ignore requestBody */ + null, /* Ignore requestHandler */ + false /* Ignore requireAblyAuth */ + ); + } catch (AblyException.HostFailedException e) { + /* Verify that, + * - a {@code AblyException.HostFailedException} is thrown. + */ + assertTrue(true); + } catch (AblyException e) { + assertTrue(false); + } + + /* Validate that only one host is used and it is Defaults#HOST_REST */ + Assert.assertTrue(urlHostArgumentStack.size() == 1); + assertThat(urlHostArgumentStack.get(0), is(equalTo(Defaults.HOST_REST))); + } + + /** + * This method mocks the API behavior + *

+ * Validates {@link HttpCore} is using first attempt to default primary host + * for every new HTTP request, after expiry of any fallbackRetryTimeout. + *

+ *

+ * Spec: RSC15e, RSC15f + *

+ * + * @throws AblyException + */ + @Test + public void http_ably_execute_first_attempt_to_default() throws AblyException { + String hostExpectedPattern = PATTERN_HOST_FALLBACK; + ClientOptions options = new ClientOptions("not:a.key"); + options.httpMaxRetryCount = 1; + options.fallbackRetryTimeout = 100; + AblyRest ably = new AblyRest(options); + + HttpCore httpCore = Mockito.spy(new HttpCore(ably.options, ably.auth)); + + String responseExpected = "Lorem Ipsum"; + ArgumentCaptor url = ArgumentCaptor.forClass(URL.class); + + /* Partially mock httpCore */ + Answer answer = new GrumpyAnswer( + 1, /* Throw exception */ + AblyException.fromErrorInfo(ErrorInfo.fromResponseStatus("Internal Server Error", 500)), /* That is HostFailedException */ + responseExpected /* Then return a valid response with second call */ + ); + + doAnswer(answer) /* Behave as defined above */ + .when(httpCore) /* when following method is executed on {@code HttpCore} instance */ + .httpExecute( + url.capture(), /* capture url arguments passed down httpExecute to assert fallback behavior executed with valid fallback url */ + any(Proxy.class), /* Ignore */ + anyString(), /* Ignore */ + aryEq(new Param[0]), /* Ignore */ + any(HttpCore.RequestBody.class), /* Ignore */ + anyBoolean(), /* Ignore */ + any(HttpCore.ResponseHandler.class) /* Ignore */ + ); + + Http http = new Http(new AsyncHttpScheduler(httpCore, options), new SyncHttpScheduler(httpCore)); + String responseActual = (String) HttpHelpers.ablyHttpExecute( + http, "", /* Ignore */ + "", /* Ignore */ + new Param[0], /* Ignore */ + new Param[0], /* Ignore */ + mock(HttpCore.RequestBody.class), /* Ignore */ + mock(HttpCore.ResponseHandler.class), /* Ignore */ + false /* Ignore */ + ); + + assertThat("Unexpected default primary host", url.getAllValues().get(0).getHost(), is(equalTo(Defaults.HOST_REST))); + assertThat("Unexpected host fallback", url.getAllValues().get(1).getHost().matches(hostExpectedPattern), is(true)); + assertThat("Unexpected response", responseActual, is(equalTo(responseExpected))); + + /* wait for expiry of fallbackRetryTimeout */ + try { + Thread.sleep(200L); + } catch(InterruptedException ie) {} + + String responseActual2 = (String) HttpHelpers.ablyHttpExecute( + http, "", /* Ignore */ + "", /* Ignore */ + new Param[0], /* Ignore */ + new Param[0], /* Ignore */ + mock(HttpCore.RequestBody.class), /* Ignore */ + mock(HttpCore.ResponseHandler.class), /* Ignore */ + false /* Ignore */ + ); + + /* Verify call causes captor to capture same arguments thrice. + * Do the validation, after we completed the {@code ArgumentCaptor} related assertions */ + verify(httpCore, times(3)) + .httpExecute( /* Just validating call counter. Ignore following parameters */ + any(URL.class), /* Ignore */ + any(Proxy.class), /* Ignore */ + anyString(), /* Ignore */ + aryEq(new Param[0]), /* Ignore */ + any(HttpCore.RequestBody.class), /* Ignore */ + anyBoolean(), /* Ignore */ + any(HttpCore.ResponseHandler.class) /* Ignore */ + ); + assertThat("Unexpected default primary host", url.getAllValues().get(2).getHost(), is(equalTo(Defaults.HOST_REST))); + assertThat("Unexpected response", responseActual2, is(equalTo(responseExpected))); + } + + /** + * This method mocks the API behavior + *

+ * Validates {@link HttpCore} is using overriden host for every new HTTP request, + * even if a previous request to that endpoint has failed. + *

+ *

+ * Spec: RSC15e + *

+ * + * @throws AblyException + */ + @Test + public void http_ably_execute_overriden_host() throws AblyException { + final String fakeHost = "fake.ably.io"; + ClientOptions options = new ClientOptions("not:a.key"); + options.restHost = fakeHost; + AblyRest ably = new AblyRest(options); + + HttpCore httpCore = Mockito.spy(new HttpCore(ably.options, ably.auth)); + + String responseExpected = "Lorem Ipsum"; + ArgumentCaptor url = ArgumentCaptor.forClass(URL.class); + + /* Partially mock httpCore */ + Answer answer = new GrumpyAnswer( + 1, /* Throw exception */ + AblyException.fromErrorInfo(ErrorInfo.fromResponseStatus("Internal Server Error", 500)), /* That is HostFailedException */ + responseExpected /* Then return a valid response with second call */ + ); + + doAnswer(answer) /* Behave as defined above */ + .when(httpCore) /* when following method is executed on {@code HttpCore} instance */ + .httpExecute( + url.capture(), /* capture url arguments passed down httpExecute to assert fallback behavior executed with valid fallback url */ + any(Proxy.class), /* Ignore */ + anyString(), /* Ignore */ + aryEq(new Param[0]), /* Ignore */ + any(HttpCore.RequestBody.class), /* Ignore */ + anyBoolean(), /* Ignore */ + any(HttpCore.ResponseHandler.class) /* Ignore */ + ); + + Http http = new Http(new AsyncHttpScheduler(httpCore, options), new SyncHttpScheduler(httpCore)); + try { + HttpHelpers.ablyHttpExecute( + http, "", /* Ignore */ + "", /* Ignore */ + new Param[0], /* Ignore */ + new Param[0], /* Ignore */ + mock(HttpCore.RequestBody.class), /* Ignore */ + mock(HttpCore.ResponseHandler.class), /* Ignore */ + false /* Ignore */ + ); + } catch (AblyException e) { + /* Verify that, + * - an {@code AblyException} with {@code ErrorInfo} having the 500 error from above + */ + ErrorInfo expectedErrorInfo = new ErrorInfo("Internal Server Error", 500, 50000); + assertThat(e, new ErrorInfoMatcher(expectedErrorInfo)); + } + + assertThat("Unexpected host", url.getAllValues().get(0).getHost(), is(equalTo(fakeHost))); + + try { + HttpHelpers.ablyHttpExecute( + http, "", /* Ignore */ + "", /* Ignore */ + new Param[0], /* Ignore */ + new Param[0], /* Ignore */ + mock(HttpCore.RequestBody.class), /* Ignore */ + mock(HttpCore.ResponseHandler.class), /* Ignore */ + false /* Ignore */ + ); + } catch (AblyException e) { + /* Verify that, + * - an {@code AblyException} with {@code ErrorInfo} having the 500 error from above + */ + ErrorInfo expectedErrorInfo = new ErrorInfo("Internal Server Error", 500, 50000); + assertThat(e, new ErrorInfoMatcher(expectedErrorInfo)); + } + + /* Verify call causes captor to capture same arguments twice. + * Do the validation, after we completed the {@code ArgumentCaptor} related assertions */ + verify(httpCore, times(2)) + .httpExecute( /* Just validating call counter. Ignore following parameters */ + any(URL.class), /* Ignore */ + any(Proxy.class), /* Ignore */ + anyString(), /* Ignore */ + aryEq(new Param[0]), /* Ignore */ + any(HttpCore.RequestBody.class), /* Ignore */ + anyBoolean(), /* Ignore */ + any(HttpCore.ResponseHandler.class) /* Ignore */ + ); + assertThat("Unexpected host", url.getAllValues().get(1).getHost(), is(equalTo(fakeHost))); + } + + /** + * This method mocks the API behavior + *

+ * Validates {@link HttpCore} isn't using any fallback hosts if {@link ClientOptions#fallbackHosts} + * array is empty. + *

+ *

+ * Spec: RSC15a + *

+ * + * @throws AblyException + */ + @Test + public void http_ably_execute_empty_fallback_array() throws AblyException { + ClientOptions options = new ClientOptions("not:a.key"); + options.fallbackHosts = new String[0]; + AblyRest ably = new AblyRest(options); + + HttpCore httpCore = Mockito.spy(new HttpCore(ably.options, ably.auth)); + + String responseExpected = "Lorem Ipsum"; + ArgumentCaptor url = ArgumentCaptor.forClass(URL.class); + + /* Partially mock httpCore */ + Answer answer = new GrumpyAnswer( + 2, /* Throw exception twice (2) */ + AblyException.fromErrorInfo(ErrorInfo.fromResponseStatus("Internal Server Error", 500)), /* That is HostFailedException */ + responseExpected /* Then return a valid response with third call */ + ); + + doAnswer(answer) /* Behave as defined above */ + .when(httpCore) /* when following method is executed on {@code HttpCore} instance */ + .httpExecute( + url.capture(), /* capture url arguments passed down httpExecute to assert fallback behavior executed with valid fallback url */ + any(Proxy.class), /* Ignore */ + anyString(), /* Ignore */ + aryEq(new Param[0]), /* Ignore */ + any(HttpCore.RequestBody.class), /* Ignore */ + anyBoolean(), /* Ignore */ + any(HttpCore.ResponseHandler.class) /* Ignore */ + ); + + Http http = new Http(new AsyncHttpScheduler(httpCore, options), new SyncHttpScheduler(httpCore)); + try { + HttpHelpers.ablyHttpExecute( + http, "", /* Ignore */ + "", /* Ignore */ + new Param[0], /* Ignore */ + new Param[0], /* Ignore */ + mock(HttpCore.RequestBody.class), /* Ignore */ + mock(HttpCore.ResponseHandler.class), /* Ignore */ + false /* Ignore */ + ); + } catch (AblyException e) { + /* Verify that, + * - an {@code AblyException} with {@code ErrorInfo} with the 500 error from above. + */ + ErrorInfo expectedErrorInfo = new ErrorInfo("Internal Server Error", 500, 50000); + assertThat(e, new ErrorInfoMatcher(expectedErrorInfo)); + } + + /* Verify call causes captor to capture same arguments once. + * Do the validation, after we completed the {@code ArgumentCaptor} related assertions */ + verify(httpCore, times(1)) + .httpExecute( /* Just validating call counter. Ignore following parameters */ + any(URL.class), /* Ignore */ + any(Proxy.class), /* Ignore */ + anyString(), /* Ignore */ + aryEq(new Param[0]), /* Ignore */ + any(HttpCore.RequestBody.class), /* Ignore */ + anyBoolean(), /* Ignore */ + any(HttpCore.ResponseHandler.class) /* Ignore */ + ); + assertThat("Unexpected host", url.getAllValues().get(0).getHost(), is(equalTo(Defaults.HOST_REST))); + } + + /** + *

+ * Validates {@code HttpCore} performs fallback behavior with custom {@link ClientOptions#fallbackHosts} + * array httpMaxRetryCount number of times at most, + * when host & fallback hosts are unreachable. Then, finally throws an error. + *

+ *

+ * Spec: RSC15a + *

+ * + * @throws Exception + */ + @Test + public void http_ably_execute_custom_fallback_array() throws AblyException { + final String[] expectedFallbackHosts = new String[]{"f.ably-realtime.com", "g.ably-realtime.com", "h.ably-realtime.com", "i.ably-realtime.com", "j.ably-realtime.com"}; + final List fallbackHostsList = Arrays.asList(expectedFallbackHosts); + + ClientOptions options = new ClientOptions("not.a:key"); + options.fallbackHosts = expectedFallbackHosts; + int expectedCallCount = options.httpMaxRetryCount + 1; + AblyRest ably = new AblyRest(options); + + HttpCore httpCore = Mockito.spy(new HttpCore(ably.options, ably.auth)); + + String responseExpected = "Lorem Ipsum"; + ArgumentCaptor url = ArgumentCaptor.forClass(URL.class); + + /* Partially mock httpCore */ + Answer answer = new GrumpyAnswer( + options.httpMaxRetryCount, /* Throw exception options.httpMaxRetryCount */ + AblyException.fromErrorInfo(ErrorInfo.fromResponseStatus("Internal Server Error", 500)), /* That is HostFailedException */ + responseExpected /* Then return a valid response with third call */ + ); + + doAnswer(answer) /* Behave as defined above */ + .when(httpCore) /* when following method is executed on {@code HttpCore} instance */ + .httpExecute( + url.capture(), /* capture url arguments passed down httpExecute to assert fallback behavior executed with valid fallback url */ + any(Proxy.class), /* Ignore */ + anyString(), /* Ignore */ + aryEq(new Param[0]), /* Ignore */ + any(HttpCore.RequestBody.class), /* Ignore */ + anyBoolean(), /* Ignore */ + any(HttpCore.ResponseHandler.class) /* Ignore */ + ); + + Http http = new Http(new AsyncHttpScheduler(httpCore, options), new SyncHttpScheduler(httpCore)); + String responseActual = (String) HttpHelpers.ablyHttpExecute( + http, "", /* Ignore */ + "", /* Ignore */ + new Param[0], /* Ignore */ + new Param[0], /* Ignore */ + mock(HttpCore.RequestBody.class), /* Ignore */ + mock(HttpCore.ResponseHandler.class), /* Ignore */ + false /* Ignore */ + ); + + /* Verify {code HttpCore#httpExecute} have been called (httpMaxRetryCount + 1) times */ + verify(httpCore, times(expectedCallCount)) + .httpExecute( /* Just validating call counter. Ignore following parameters */ + any(URL.class), /* Ignore */ + any(Proxy.class), /* Ignore */ + anyString(), /* Ignore */ + aryEq(new Param[0]), /* Ignore */ + any(HttpCore.RequestBody.class), /* Ignore */ + anyBoolean(), /* Ignore */ + any(HttpCore.ResponseHandler.class) /* Ignore */ + ); + + /* Verify that, + * - delivered expected response + * - first call executed against production rest host + * - other calls executed against a random custom fallback host */ + List allValues = url.getAllValues(); + assertThat("Unexpected response", responseActual, is(equalTo(responseExpected))); + assertThat("Unexpected default primary host", allValues.get(0).getHost(), is(equalTo(Defaults.HOST_REST))); + for (int i = 1; i < allValues.size(); i++) { + assertThat("Unexpected host fallback", fallbackHostsList.contains(allValues.get(i).getHost()), is(true)); + } + } + + /** + * Validates that HttpCore uses ClientOptions#fallbackHosts when there is AblyException.HostFailedException thrown + * @throws AblyException + */ + + @Test + public void http_ably_execute_custom_fallback() throws AblyException { + ClientOptions options = new ClientOptions(); + options.tls = false; + options.fallbackHosts = CUSTOM_HOSTS; + options.port = 7777; + + ArrayList urlHostArgumentStack = new ArrayList<>(); + + HttpCore httpCore = new HttpCore(options, null) { + /* Store only string representations to avoid try/catch blocks */ + List urlArgumentStack; + + @Override + public T httpExecuteWithRetry(URL url, String method, Param[] headers, RequestBody requestBody, ResponseHandler responseHandler, boolean allowAblyAuth) throws AblyException { + urlArgumentStack.add(url.getHost()); + /* verify if fallback hosts are from specified list */ + if(!url.getHost().equals(Defaults.HOST_REST)) + assertTrue(Arrays.asList(CUSTOM_HOSTS).contains(url.getHost())); + + return super.httpExecuteWithRetry(url, method, headers, requestBody, responseHandler, allowAblyAuth); + } + + public HttpCore setUrlArgumentStack(List urlArgumentStack) { + this.urlArgumentStack = urlArgumentStack; + return this; + } + }.setUrlArgumentStack(urlHostArgumentStack); + + Http http = new Http(new AsyncHttpScheduler(httpCore, options), new SyncHttpScheduler(httpCore)); + try { + HttpHelpers.ablyHttpExecute( + http, "/path/to/fallback", /* Ignore path */ + HttpConstants.Methods.GET, /* Ignore method */ + new Param[0], /* Ignore headers */ + new Param[0], /* Ignore params */ + null, /* Ignore requestBody */ + null, /* Ignore requestHandler */ + false /* Ignore requireAblyAuth */ + ); + } catch (AblyException.HostFailedException e) { + /* Verify that, + * - a {@code AblyException.HostFailedException} is thrown. + */ + assertTrue(true); + } catch (AblyException e) { + assertTrue(false); + } + + int expectedCallCount = options.httpMaxRetryCount + 1; + Assert.assertTrue(urlHostArgumentStack.size() == expectedCallCount); + assertThat(urlHostArgumentStack.get(0), is(equalTo(Defaults.HOST_REST))); + + for (int i = 1; i < expectedCallCount; i++) { + Assert.assertTrue(urlHostArgumentStack.get(i).matches(CUSTOM_PATTERN_HOST_FALLBACK)); + } + } + + /** + * This method mocks the API behavior + *

+ * Validates httpCore is not using any fallback host when we receive valid response from httpCore's host + *

+ *

+ * Spec: RSC15a + *

+ * + * @throws Exception + */ + @Test + public void http_execute_nofallback() throws Exception { + HttpCore httpCore = Mockito.spy(new HttpCore(new ClientOptions(), null)); + + String responseExpected = "Lorem Ipsum"; + String hostExpected = Defaults.HOST_REST; + ArgumentCaptor url = ArgumentCaptor.forClass(URL.class); + + /* Partially mock httpCore */ + doReturn(responseExpected) /* Provide response */ + .when(httpCore) /* when following method is executed on {@code HttpCore} instance */ + .httpExecute( + url.capture(), /* capture url arguments passed down httpExecute to assert fallback behavior executed with valid rest host */ + any(Proxy.class), /* Ignore */ + anyString(), /* Ignore */ + aryEq(new Param[0]), /* Ignore */ + any(HttpCore.RequestBody.class), /* Ignore */ + anyBoolean(), /* Ignore */ + any(HttpCore.ResponseHandler.class) /* Ignore */ + ); + + Http http = new Http(new AsyncHttpScheduler(httpCore, new ClientOptions()), new SyncHttpScheduler(httpCore)); + String responseActual = (String) HttpHelpers.ablyHttpExecute( + http, "", /* Ignore */ + "", /* Ignore */ + new Param[0], /* Ignore */ + new Param[0], /* Ignore */ + mock(HttpCore.RequestBody.class), /* Ignore */ + mock(HttpCore.ResponseHandler.class), /* Ignore */ + false /* Ignore */ + ); + + + /* Verify + * - httpCore call executed once, + * - with given host, + * - and delivered expected response */ + verify(httpCore, times(1)) + .httpExecute( /* Just validating call counter. Ignore following parameters */ + url.capture(), /* capture url arguments passed down httpExecute to assert fallback behavior executed with valid rest host */ + any(Proxy.class), /* Ignore */ + anyString(), /* Ignore */ + aryEq(new Param[0]), /* Ignore */ + any(HttpCore.RequestBody.class), /* Ignore */ + anyBoolean(), /* Ignore */ + any(HttpCore.ResponseHandler.class) /* Ignore */ + ); + + assertThat(url.getValue().getHost(), is(equalTo(hostExpected))); + assertThat(responseActual, is(equalTo(responseExpected))); + } + + /** + * This method mocks the API behavior + *

+ * Validates httpCore is using a fallback host when HostFailedException thrown + *

+ *

+ * Spec: RSC15a + *

+ * + * @throws Exception + */ + @Test + public void http_execute_singlefallback() throws Exception { + HttpCore httpCore = Mockito.spy(new HttpCore(new ClientOptions(), null)); + + String hostExpectedPattern = PATTERN_HOST_FALLBACK; + String responseExpected = "Lorem Ipsum"; + ArgumentCaptor url = ArgumentCaptor.forClass(URL.class); + + /* Partially mock httpCore */ + Answer answer = new GrumpyAnswer( + 1, /* Throw exception once (1) */ + AblyException.fromErrorInfo(ErrorInfo.fromResponseStatus("Internal Server Error", 500)), /* That is HostFailedException */ + responseExpected /* Then return a valid response with the second call */ + ); + + doAnswer(answer) /* Behave as defined above */ + .when(httpCore) /* when following method is executed on {@code HttpCore} instance */ + .httpExecute( + url.capture(), /* capture url arguments passed down httpExecute to assert fallback behavior executed with valid rest host */ + any(Proxy.class), /* Ignore */ + anyString(), /* Ignore */ + aryEq(new Param[0]), /* Ignore */ + any(HttpCore.RequestBody.class), /* Ignore */ + anyBoolean(), /* Ignore */ + any(HttpCore.ResponseHandler.class) /* Ignore */ + ); + + /* Call method with real parameters */ + Http http = new Http(new AsyncHttpScheduler(httpCore, new ClientOptions()), new SyncHttpScheduler(httpCore)); + String responseActual = (String) HttpHelpers.ablyHttpExecute( + http, "", /* Ignore */ + "", /* Ignore */ + new Param[0], /* Ignore */ + new Param[0], /* Ignore */ + mock(HttpCore.RequestBody.class), /* Ignore */ + mock(HttpCore.ResponseHandler.class), /* Ignore */ + false /* Ignore */ + ); + + + /* Verify + * - httpCore call executed twice (one for prod host and 1 for fallback), + * - last call performed against a fallback host, + * - and fallback host delivered expected response */ + + verify(httpCore, times(2)) + .httpExecute( /* Just validating call counter. Ignore following parameters */ + url.capture(), /* capture url arguments passed down httpExecute to assert fallback behavior executed with valid rest host */ + any(Proxy.class), /* Ignore */ + anyString(), /* Ignore */ + aryEq(new Param[0]), /* Ignore */ + any(HttpCore.RequestBody.class), /* Ignore */ + anyBoolean(), /* Ignore */ + any(HttpCore.ResponseHandler.class) /* Ignore */ + ); + + assertThat(url.getValue().getHost().matches(hostExpectedPattern), is(true)); + assertThat(responseActual, is(equalTo(responseExpected))); + } + + /** + * This method mocks the API behavior + *

+ * Validates httpCore is using different hosts when HostFailedException happened multiple times. + *

+ *

+ * Spec: RSC15a + *

+ * + * @throws Exception + */ + @Test + public void http_execute_multiplefallback() throws Exception { + HttpCore httpCore = Mockito.spy(new HttpCore(new ClientOptions(), null)); + + String hostExpectedPattern = PATTERN_HOST_FALLBACK; + String responseExpected = "Lorem Ipsum"; + ArgumentCaptor url = ArgumentCaptor.forClass(URL.class); + + /* Partially mock httpCore */ + Answer answer = new GrumpyAnswer( + 2, /* Throw exception twice (2) */ + AblyException.fromErrorInfo(ErrorInfo.fromResponseStatus("Internal Server Error", 500)), /* That is HostFailedException */ + responseExpected /* Then return a valid response with third call */ + ); + + doAnswer(answer) /* Behave as defined above */ + .when(httpCore) /* when following method is executed on {@code HttpCore} instance */ + .httpExecute( + url.capture(), /* capture url arguments passed down httpExecute to assert fallback behavior executed with valid rest host */ + any(Proxy.class), /* Ignore */ + anyString(), /* Ignore */ + aryEq(new Param[0]), /* Ignore */ + any(HttpCore.RequestBody.class), /* Ignore */ + anyBoolean(), /* Ignore */ + any(HttpCore.ResponseHandler.class) /* Ignore */ + ); + + Http http = new Http(new AsyncHttpScheduler(httpCore, new ClientOptions()), new SyncHttpScheduler(httpCore)); + String responseActual = (String) HttpHelpers.ablyHttpExecute( + http, "", /* Ignore */ + "", /* Ignore */ + new Param[0], /* Ignore */ + new Param[0], /* Ignore */ + mock(HttpCore.RequestBody.class), /* Ignore */ + mock(HttpCore.ResponseHandler.class), /* Ignore */ + false /* Ignore */ + ); + + + /* Verify + * - httpCore call executed thrice, + * - with 2 fallback hosts, + * - each host having a unique value, + * - and delivered expected response */ + + assertThat(url.getAllValues().get(1).getHost().matches(hostExpectedPattern), is(true)); + assertThat(url.getAllValues().get(2).getHost().matches(hostExpectedPattern), is(true)); + assertThat(url.getAllValues().get(1).toString(), is(not(equalTo(url.getAllValues().get(2).toString())))); + + assertThat(responseActual, is(equalTo(responseExpected))); + + /* Verify call causes captor to capture same arguments twice. + * Do the validation, after we completed the {@code ArgumentCaptor} related assertions */ + verify(httpCore, times(3)) + .httpExecute( /* Just validating call counter. Ignore following parameters */ + url.capture(), /* capture url arguments passed down httpExecute to assert fallback behavior executed with valid rest host */ + any(Proxy.class), /* Ignore */ + anyString(), /* Ignore */ + aryEq(new Param[0]), /* Ignore */ + any(HttpCore.RequestBody.class), /* Ignore */ + anyBoolean(), /* Ignore */ + any(HttpCore.ResponseHandler.class) /* Ignore */ + ); + } + + /** + * This method mocks the API behavior + *

+ * Validates {@code HttpCore} is using its a successful fallback host first + * when a consecutive call happens within the fallbackRetryTimeout, so long + * as calls to that fallback continue to succeed + *

+ *

+ * Spec: RSC15f + *

+ * + * @throws Exception + */ + @Test + public void http_execute_fallback_success_timeout_unexpired() throws Exception { + ClientOptions opts = new ClientOptions(); + opts.fallbackRetryTimeout = 2000L; + HttpCore httpCore = Mockito.spy(new HttpCore(opts, null)); + + String hostExpected = Defaults.HOST_REST; + ArgumentCaptor url = ArgumentCaptor.forClass(URL.class); + + /* Partially mock httpCore */ + Answer answer = new GrumpyAnswer( + 1, /* Throw exception once (1) */ + AblyException.fromErrorInfo(ErrorInfo.fromResponseStatus("Internal Server Error", 500)), /* That is HostFailedException */ + "Lorem Ipsum" /* Ignore */ + ); + + doAnswer(answer) /* Behave as defined above */ + .when(httpCore) /* when following method is executed on {@code HttpCore} instance */ + .httpExecute( + url.capture(), /* capture url arguments passed down httpExecute to assert fallback behavior executed with valid rest host */ + any(Proxy.class), /* Ignore */ + anyString(), /* Ignore */ + aryEq(new Param[0]), /* Ignore */ + any(HttpCore.RequestBody.class), /* Ignore */ + anyBoolean(), /* Ignore */ + any(HttpCore.ResponseHandler.class) /* Ignore */ + ); + + Http http = new Http(new AsyncHttpScheduler(httpCore, new ClientOptions()), new SyncHttpScheduler(httpCore)); + HttpHelpers.ablyHttpExecute( + http, "", /* Ignore */ + "", /* Ignore */ + new Param[0], /* Ignore */ + new Param[0], /* Ignore */ + mock(HttpCore.RequestBody.class), /* Ignore */ + mock(HttpCore.ResponseHandler.class), /* Ignore */ + false /* Ignore */ + ); + + /* Verify there was a fallback with first call */ + String successFallback = url.getValue().getHost(); + assertThat(successFallback.matches(PATTERN_HOST_FALLBACK), is(true)); + + /* wait for a short time, but not enough for the fallbackRetryTimeout to expire */ + try { Thread.sleep(200L); } catch(InterruptedException ie) {} + + /* Update behavior to succeed on next attempt */ + url = ArgumentCaptor.forClass(URL.class); + doReturn("Lorem Ipsum") /* Return some response string that we will ignore */ + .when(httpCore) /* when following method is executed on {@code HttpCore} instance */ + .httpExecute( + url.capture(), /* capture url arguments passed down httpExecute to assert fallback behavior executed with valid rest host */ + any(Proxy.class), /* Ignore */ + anyString(), /* Ignore */ + aryEq(new Param[0]), /* Ignore */ + any(HttpCore.RequestBody.class), /* Ignore */ + anyBoolean(), /* Ignore */ + any(HttpCore.ResponseHandler.class) /* Ignore */ + ); + + HttpHelpers.ablyHttpExecute( + http, "", /* Ignore */ + "", /* Ignore */ + new Param[0], /* Ignore */ + new Param[0], /* Ignore */ + mock(HttpCore.RequestBody.class), /* Ignore */ + mock(HttpCore.ResponseHandler.class), /* Ignore */ + false /* Ignore */ + ); + + /* Verify second call was called with the cached fallback host */ + assertThat(url.getValue().getHost().equals(successFallback), is(true)); + } + + /** + * This method mocks the API behavior + *

+ * Validates {@code HttpCore} reverts to using the primary endpoint + * if a request to a preferred fallback fails (after initially succeeding + * and being cached) + *

+ *

+ * Spec: RSC15f + *

+ * + * @throws Exception + */ + @Test + public void http_execute_fallback_failure_timeout_unexpired() throws Exception { + ClientOptions opts = new ClientOptions(); + opts.fallbackRetryTimeout = 2000L; + HttpCore httpCore = Mockito.spy(new HttpCore(opts, null)); + + String primaryHost = Defaults.HOST_REST; + ArgumentCaptor url = ArgumentCaptor.forClass(URL.class); + + /* Partially mock httpCore */ + Answer answer = new GrumpyAnswer( + 1, /* Throw exception once */ + AblyException.fromErrorInfo(ErrorInfo.fromResponseStatus("Internal Server Error", 500)), /* That is HostFailedException */ + "Lorem Ipsum" /* Ignore */ + ); + + doAnswer(answer) /* Behave as defined above */ + .when(httpCore) /* when following method is executed on {@code HttpCore} instance */ + .httpExecute( + url.capture(), /* capture url arguments passed down httpExecute to assert fallback behavior executed with valid rest host */ + any(Proxy.class), /* Ignore */ + anyString(), /* Ignore */ + aryEq(new Param[0]), /* Ignore */ + any(HttpCore.RequestBody.class), /* Ignore */ + anyBoolean(), /* Ignore */ + any(HttpCore.ResponseHandler.class) /* Ignore */ + ); + + Http http = new Http(new AsyncHttpScheduler(httpCore, new ClientOptions()), new SyncHttpScheduler(httpCore)); + HttpHelpers.ablyHttpExecute( + http, "", /* Ignore */ + "", /* Ignore */ + new Param[0], /* Ignore */ + new Param[0], /* Ignore */ + mock(HttpCore.RequestBody.class), /* Ignore */ + mock(HttpCore.ResponseHandler.class), /* Ignore */ + false /* Ignore */ + ); + + /* Verify there was a fallback with first call */ + String failFallback = url.getValue().getHost(); + assertThat(failFallback.matches(PATTERN_HOST_FALLBACK), is(true)); + + /* wait for a short time, but not enough for the fallbackRetryTimeout to expire */ + try { Thread.sleep(200L); } catch(InterruptedException ie) {} + + /* reset the mocked response so that the next request fails */ + answer = new GrumpyAnswer( + 1, /* Throw exception once */ + AblyException.fromErrorInfo(ErrorInfo.fromResponseStatus("Internal Server Error", 500)), /* That is HostFailedException */ + "Lorem Ipsum" /* Ignore */ + ); + + doAnswer(answer) /* Behave as defined above */ + .when(httpCore) /* when following method is executed on {@code HttpCore} instance */ + .httpExecute( + url.capture(), /* capture url arguments passed down httpExecute to assert fallback behavior executed with valid rest host */ + any(Proxy.class), /* Ignore */ + anyString(), /* Ignore */ + aryEq(new Param[0]), /* Ignore */ + any(HttpCore.RequestBody.class), /* Ignore */ + anyBoolean(), /* Ignore */ + any(HttpCore.ResponseHandler.class) /* Ignore */ + ); + + HttpHelpers.ablyHttpExecute( + http, "", /* Ignore */ + "", /* Ignore */ + new Param[0], /* Ignore */ + new Param[0], /* Ignore */ + mock(HttpCore.RequestBody.class), /* Ignore */ + mock(HttpCore.ResponseHandler.class), /* Ignore */ + false /* Ignore */ + ); + + /* Verify second call was called with httpCore host */ + assertThat(url.getValue().getHost().equals(primaryHost), is(true)); + } + + /** + * This method mocks the API behavior + *

+ * Validates {@code HttpCore} is using the primary host first + * when a consecutive call happens after the fallbackRetryTimeout + *

+ *

+ * Spec: RSC15f + *

+ * + * @throws Exception + */ + @Test + public void http_execute_fallback_timeout_expired() throws Exception { + ClientOptions opts = new ClientOptions(); + opts.fallbackRetryTimeout = 2000L; + HttpCore httpCore = Mockito.spy(new HttpCore(opts, null)); + + String hostExpected = Defaults.HOST_REST; + ArgumentCaptor url = ArgumentCaptor.forClass(URL.class); + + /* Partially mock httpCore */ + Answer answer = new GrumpyAnswer( + 1, /* Throw exception once (1) */ + AblyException.fromErrorInfo(ErrorInfo.fromResponseStatus("Internal Server Error", 500)), /* That is HostFailedException */ + "Lorem Ipsum" /* Ignore */ + ); + + doAnswer(answer) /* Behave as defined above */ + .when(httpCore) /* when following method is executed on {@code HttpCore} instance */ + .httpExecute( + url.capture(), /* capture url arguments passed down httpExecute to assert fallback behavior executed with valid rest host */ + any(Proxy.class), /* Ignore */ + anyString(), /* Ignore */ + aryEq(new Param[0]), /* Ignore */ + any(HttpCore.RequestBody.class), /* Ignore */ + anyBoolean(), /* Ignore */ + any(HttpCore.ResponseHandler.class) /* Ignore */ + ); + + Http http = new Http(new AsyncHttpScheduler(httpCore, new ClientOptions()), new SyncHttpScheduler(httpCore)); + HttpHelpers.ablyHttpExecute( + http, "", /* Ignore */ + "", /* Ignore */ + new Param[0], /* Ignore */ + new Param[0], /* Ignore */ + mock(HttpCore.RequestBody.class), /* Ignore */ + mock(HttpCore.ResponseHandler.class), /* Ignore */ + false /* Ignore */ + ); + + /* Verify there was a fallback with first call */ + assertThat(url.getValue().getHost().matches(PATTERN_HOST_FALLBACK), is(true)); + + /* wait for the fallbackRetryTimeout to expire */ + try { Thread.sleep(2000L); } catch(InterruptedException ie) {} + + /* Update behavior to perform a call without a fallback */ + url = ArgumentCaptor.forClass(URL.class); + doReturn("Lorem Ipsum") /* Return some response string that we will ignore */ + .when(httpCore) /* when following method is executed on {@code HttpCore} instance */ + .httpExecute( + url.capture(), /* capture url arguments passed down httpExecute to assert fallback behavior executed with valid rest host */ + any(Proxy.class), /* Ignore */ + anyString(), /* Ignore */ + aryEq(new Param[0]), /* Ignore */ + any(HttpCore.RequestBody.class), /* Ignore */ + anyBoolean(), /* Ignore */ + any(HttpCore.ResponseHandler.class) /* Ignore */ + ); + + HttpHelpers.ablyHttpExecute( + http, "", /* Ignore */ + "", /* Ignore */ + new Param[0], /* Ignore */ + new Param[0], /* Ignore */ + mock(HttpCore.RequestBody.class), /* Ignore */ + mock(HttpCore.ResponseHandler.class), /* Ignore */ + false /* Ignore */ + ); + + /* Verify second call was called with httpCore host */ + assertThat(url.getValue().getHost().equals(hostExpected), is(true)); + } + + /** + * This method mocks the API behavior + *

+ * Validates {@code HttpCore} is throwing an exception, + * when connection to host failed more than allowed count ({@code Defaults.HTTP_MAX_RETRY_COUNT}) + *

+ *

+ * Spec: - + *

+ * + * @throws Exception + */ + @Test + public void http_execute_excessivefallback() throws AblyException { + ClientOptions options = new ClientOptions(); + HttpCore httpCore = Mockito.spy(new HttpCore(options, null)); + + ArgumentCaptor url = ArgumentCaptor.forClass(URL.class); + int excessiveFallbackCount = options.httpMaxRetryCount + 1; + + /* Partially mock httpCore */ + Answer answer = new GrumpyAnswer( + excessiveFallbackCount, /* Throw exception more than httpMaxRetryCount number of times */ + AblyException.fromErrorInfo(ErrorInfo.fromResponseStatus("Internal Server Error", 500)), /* That is HostFailedException */ + "Lorem Ipsum" /* Ignore */ + ); + + doAnswer(answer) /* Behave as defined above */ + .when(httpCore) /* when following method is executed on {@code HttpCore} instance */ + .httpExecute( + url.capture(), /* capture url arguments passed down httpExecute to assert fallback behavior executed with valid rest host */ + any(Proxy.class), /* Ignore */ + anyString(), /* Ignore */ + aryEq(new Param[0]), /* Ignore */ + any(HttpCore.RequestBody.class), /* Ignore */ + anyBoolean(), /* Ignore */ + any(HttpCore.ResponseHandler.class) /* Ignore */ + ); + + + /* Verify + * - ably exception with 50x status code is thrown + */ + thrown.expect(AblyException.HostFailedException.class); + + Http http = new Http(new AsyncHttpScheduler(httpCore, new ClientOptions()), new SyncHttpScheduler(httpCore)); + HttpHelpers.ablyHttpExecute( + http, "", /* Ignore */ + "", /* Ignore */ + new Param[0], /* Ignore */ + new Param[0], /* Ignore */ + mock(HttpCore.RequestBody.class), /* Ignore */ + mock(HttpCore.ResponseHandler.class), /* Ignore */ + false /* Ignore */ + ); + } + + /** + *

+ * Validates {@code HttpCore#httpExecute} is throwing an {@code HostFailedException}, + * when api returns a response code between 500 and 504 + *

+ *

+ * Spec: RSC15d + *

+ * + * @throws Exception + */ + @Test + public void http_execute_response_50x() throws AblyException, MalformedURLException { + URL url; + HttpCore httpCore = new HttpCore(new ClientOptions(), null); + + AblyException.HostFailedException hfe; + + for (int statusCode = 500; statusCode <= 504; statusCode++) { + url = new URL("http://localhost:" + server.getListeningPort() + "/status/" + statusCode); + hfe = null; + + try { + String result = HttpHelpers.httpExecute(httpCore, url, HttpConstants.Methods.GET, new Param[0], null, null); + } catch (AblyException.HostFailedException e) { + hfe = e; + } catch (AblyException e) { + e.printStackTrace(); + } + + Assert.assertNotNull("Status code " + statusCode + " should throw an exception", hfe); + } + } + + /** + *

+ * Validates {@code HttpCore#httpExecute} isn't throwing an {@code HostFailedException}, + * when api returns a non-server-error response code (Informational 1xx, + * Multiple Choices 3xx, Client Error 4xx) + *

+ *

+ * Spec: RSC15d + *

+ * + * @throws Exception + */ + @Test + public void http_execute_response_non5xx() throws AblyException, MalformedURLException { + URL url; + HttpCore httpCore = new HttpCore(new ClientOptions(), null); + + /* Informational 1xx */ + + for (int statusCode = 100; statusCode <= 101; statusCode++) { + url = new URL("http://localhost:" + server.getListeningPort() + "/status/" + statusCode); + + try { + HttpHelpers.httpExecute(httpCore, url, HttpConstants.Methods.GET, new Param[0], null, null); + } catch (AblyException.HostFailedException e) { + fail("Informal status code " + statusCode + " shouldn't throw an exception"); + } catch (Exception e) { + /* non HostFailedExceptions are ignored */ + } + } + + + /* Informational 3xx */ + + for (int statusCode = 300; statusCode <= 307; statusCode++) { + url = new URL("http://localhost:" + server.getListeningPort() + "/status/" + statusCode); + + try { + HttpHelpers.httpExecute(httpCore, url, HttpConstants.Methods.GET, new Param[0], null, null); + } catch (AblyException.HostFailedException e) { + fail("Multiple choices status code " + statusCode + " shouldn't throw an exception"); + } catch (Exception e) { + /* non HostFailedExceptions are ignored */ + } + } + + + /* Informational 4xx */ + + for (int statusCode = 400; statusCode <= 417; statusCode++) { + url = new URL("http://localhost:" + server.getListeningPort() + "/status/" + statusCode); + + try { + HttpHelpers.httpExecute(httpCore, url, HttpConstants.Methods.GET, new Param[0], null, null); + } catch (AblyException.HostFailedException e) { + fail("Client error status code " + statusCode + " shouldn't throw an exception"); + } catch (Exception e) { + /* non HostFailedExceptions are ignored */ + } + } + } + + @Test + public void test_asynchttp_concurrent_default_notqueued() { + test_asynchttp_concurrent(0, 64, 5000, 20000); + } + + @Test + public void test_asynchttp_concurrent_default_queued() { + test_asynchttp_concurrent(0, 128, 10000, 25000); + } + + @Test + public void test_asynchttp_concurrent_10_notqueued() { + test_asynchttp_concurrent(10, 10, 5000, 20000); + } + + @Test + public void test_asynchttp_concurrent_11_queued() { + test_asynchttp_concurrent(10, 11, 10000, 25000); + } + + private void test_asynchttp_concurrent(int poolSize, int requestCount, int expectedMinDuration, int expectedMaxDuration) { + try { + ClientOptions options = new ClientOptions("not.a:key"); + options.tls = false; + options.restHost = TEST_SERVER_HOST; + options.port = TEST_SERVER_PORT; + if(poolSize > 0) { + options.asyncHttpThreadpoolSize = poolSize; + } + final AblyRest ablyRest = new AblyRest(options); + + final Object waiter = new Object(); + final long startTime = System.currentTimeMillis(); + final int[] counter = new int[1]; + final boolean[] error = new boolean[1]; + + /* start parallel requests */ + for(int i = 0; i < requestCount; i++) { + ablyRest.timeAsync(new Callback() { + @Override + public void onSuccess(Long result) { + synchronized(waiter) { + counter[0]++; + waiter.notify(); + } + } + + @Override + public void onError(ErrorInfo reason) { + synchronized(waiter) { + counter[0]++; + error[0] = true; + waiter.notify(); + } + fail("Unexpected error: " + reason.message); + } + }); + } + + /* wait for all requests to complete */ + while(counter[0] < requestCount) { + synchronized(waiter) { + try { + waiter.wait(); + } catch(InterruptedException ie) {} + } + } + + /* assert */ + assertEquals("Verify all requests completed", counter[0], requestCount); + assertFalse("Verify there were no errors", error[0]); + + long endTime = System.currentTimeMillis(), duration = endTime - startTime; + assertTrue("Verify duration at least minimum", duration >= expectedMinDuration); + assertTrue("Verify duration at most maximum", duration <= expectedMaxDuration); + + } catch(AblyException ae) { + ae.printStackTrace(); + } + } + + /********************************************* + * Minions + *********************************************/ + + static class GrumpyAnswer implements Answer { + private int grumpinessLevel; + private Throwable nope; + private String value; + + /** + * Throws grumpinessLevel number of nope to you and then gives its response properly. + * + * @param grumpinessLevel Quantity of nope that will be thrown into your face, each time. + * @param nope Expected nope + * @param value Expected value that will be returned after grumpiness level goes below or equal to 0. + */ + public GrumpyAnswer(int grumpinessLevel, Throwable nope, String value) { + this.grumpinessLevel = grumpinessLevel; + this.nope = nope; + this.value = value; + } + + @Override + public String answer(InvocationOnMock invocation) throws Throwable { + if (grumpinessLevel-- > 0) { + throw nope; + } + + return value; + } + } + + static class ErrorInfoMatcher extends TypeSafeMatcher { + ErrorInfo errorInfo; + + public ErrorInfoMatcher(ErrorInfo errorInfo) { + super(); + this.errorInfo = errorInfo; + } + + @Override + protected boolean matchesSafely(AblyException item) { + return errorInfo.code == item.errorInfo.code && + errorInfo.statusCode == item.errorInfo.statusCode; + } + + @Override + protected void describeMismatchSafely(AblyException item, Description mismatchDescription) { + mismatchDescription.appendText(item.errorInfo.toString()); + } + + @Override + public void describeTo(Description description) { + description.appendText(errorInfo.toString()); + } + } } diff --git a/lib/src/test/java/io/ably/lib/test/rest/RestAppStatsTest.java b/lib/src/test/java/io/ably/lib/test/rest/RestAppStatsTest.java index 9987d2c04..61fa62b4f 100644 --- a/lib/src/test/java/io/ably/lib/test/rest/RestAppStatsTest.java +++ b/lib/src/test/java/io/ably/lib/test/rest/RestAppStatsTest.java @@ -26,402 +26,402 @@ @SuppressWarnings("deprecation") public class RestAppStatsTest extends ParameterizedTest { - private AblyRest ably; - private static String[] intervalIds; + private AblyRest ably; + private static String[] intervalIds; - @Before - public void setUpBefore() throws Exception { - ClientOptions opts = createOptions(testVars.keys[0].keyStr); - ably = new AblyRest(opts); - } + @Before + public void setUpBefore() throws Exception { + ClientOptions opts = createOptions(testVars.keys[0].keyStr); + ably = new AblyRest(opts); + } - @BeforeClass - public static void populateStats() { - try { - ClientOptions opts = testVars.createOptions(testVars.keys[0].keyStr, Setup.TestParameters.TEXT); - AblyRest ably = new AblyRest(opts); - /* get time, preferring time from Ably */ - long currentTime = System.currentTimeMillis(); - try { - currentTime = ably.time(); - } catch (AblyException e) {} + @BeforeClass + public static void populateStats() { + try { + ClientOptions opts = testVars.createOptions(testVars.keys[0].keyStr, Setup.TestParameters.TEXT); + AblyRest ably = new AblyRest(opts); + /* get time, preferring time from Ably */ + long currentTime = System.currentTimeMillis(); + try { + currentTime = ably.time(); + } catch (AblyException e) {} - /* round down to the start of the current minute */ - Date currentDate = new Date(currentTime); - currentDate.setSeconds(0); - currentTime = currentDate.getTime(); + /* round down to the start of the current minute */ + Date currentDate = new Date(currentTime); + currentDate.setSeconds(0); + currentTime = currentDate.getTime(); - /* Make it the same time last year, to avoid problems with the app - * already having stats from other tests in the same test run. */ - currentTime -= 365 * 24 * 3600 * 1000L; + /* Make it the same time last year, to avoid problems with the app + * already having stats from other tests in the same test run. */ + currentTime -= 365 * 24 * 3600 * 1000L; - /* get time bounds for test */ - intervalIds = new String[3]; - for(int i = 0; i < 3; i++) { - long intervalTime = currentTime + (i - 3) * 60 * 1000; - intervalIds[i] = Stats.toIntervalId(intervalTime, Stats.Granularity.minute); - } + /* get time bounds for test */ + intervalIds = new String[3]; + for(int i = 0; i < 3; i++) { + long intervalTime = currentTime + (i - 3) * 60 * 1000; + intervalIds[i] = Stats.toIntervalId(intervalTime, Stats.Granularity.minute); + } - /* add stats for each of the minutes within the interval */ - Stats[] testStats = StatsReader.readJson( - '[' - + "{ \"intervalId\": \"" + intervalIds[0] + "\"," - + "\"inbound\": {\"realtime\":{\"messages\":{\"count\":50,\"data\":5000,\"uncompressedData\":5000,\"category\":{\"delta\":{\"count\":10,\"data\":1000,\"uncompressedData\":5000}}}}}," - + "\"processed\": {\"delta\": {\"xdelta\": {\"succeeded\": 10, \"skipped\": 5, \"failed\": 1}}}" - + "}," - + "{ \"intervalId\": \"" + intervalIds[1] + "\"," - + "\"inbound\": {\"realtime\":{\"messages\":{\"count\":60,\"data\":6000,\"uncompressedData\":6000,\"category\":{\"delta\":{\"count\":20,\"data\":2000,\"uncompressedData\":6000}}}}}," - + "\"processed\": {\"delta\": {\"xdelta\": {\"succeeded\": 20, \"skipped\": 10, \"failed\": 2}}}" - + "}," - + "{ \"intervalId\": \"" + intervalIds[2] + "\"," - + "\"inbound\": {\"realtime\":{\"messages\":{\"count\":70,\"data\":7000,\"uncompressedData\":7000,\"category\":{\"delta\":{\"count\":40,\"data\":4000,\"uncompressedData\":7000}}}}}," - + "\"processed\": {\"delta\": {\"xdelta\": {\"succeeded\": 40, \"skipped\": 20, \"failed\": 4}}}" - + '}' - + ']' - ); + /* add stats for each of the minutes within the interval */ + Stats[] testStats = StatsReader.readJson( + '[' + + "{ \"intervalId\": \"" + intervalIds[0] + "\"," + + "\"inbound\": {\"realtime\":{\"messages\":{\"count\":50,\"data\":5000,\"uncompressedData\":5000,\"category\":{\"delta\":{\"count\":10,\"data\":1000,\"uncompressedData\":5000}}}}}," + + "\"processed\": {\"delta\": {\"xdelta\": {\"succeeded\": 10, \"skipped\": 5, \"failed\": 1}}}" + + "}," + + "{ \"intervalId\": \"" + intervalIds[1] + "\"," + + "\"inbound\": {\"realtime\":{\"messages\":{\"count\":60,\"data\":6000,\"uncompressedData\":6000,\"category\":{\"delta\":{\"count\":20,\"data\":2000,\"uncompressedData\":6000}}}}}," + + "\"processed\": {\"delta\": {\"xdelta\": {\"succeeded\": 20, \"skipped\": 10, \"failed\": 2}}}" + + "}," + + "{ \"intervalId\": \"" + intervalIds[2] + "\"," + + "\"inbound\": {\"realtime\":{\"messages\":{\"count\":70,\"data\":7000,\"uncompressedData\":7000,\"category\":{\"delta\":{\"count\":40,\"data\":4000,\"uncompressedData\":7000}}}}}," + + "\"processed\": {\"delta\": {\"xdelta\": {\"succeeded\": 40, \"skipped\": 20, \"failed\": 4}}}" + + '}' + + ']' + ); - HttpHelpers.postSync(ably.http, "/stats", HttpUtils.defaultAcceptHeaders(false), null, StatsWriter.asJsonRequest(testStats), null, true); - } catch (AblyException e) {} - } + HttpHelpers.postSync(ably.http, "/stats", HttpUtils.defaultAcceptHeaders(false), null, StatsWriter.asJsonRequest(testStats), null, true); + } catch (AblyException e) {} + } - /** - * Check minute-level stats exist (forwards) - */ - @Test - public void appstats_minute0() { - /* get the stats for this channel */ - try { - /* note that bounds are inclusive */ - PaginatedResult stats = ably.stats(new Param[] { - new Param("direction", "forwards"), - new Param("start", intervalIds[0]), - new Param("end", intervalIds[0]) - }); - assertNotNull("Expected non-null stats", stats); - assertThat("Expected 1 record", stats.items().length, is(equalTo(1))); - assertThat("Expected 50 messages", (int)stats.items()[0].inbound.all.all.count, is(equalTo(50))); - assertThat("Expected 5000 bytes of uncompressed data", (int)stats.items()[0].inbound.all.all.uncompressedData, is(equalTo(5000))); - assertThat("Expected 10 delta messages", (int)stats.items()[0].inbound.all.all.category.get("delta").count, is(equalTo(10))); - assertThat("Expected 10 successful delta generations", (int)stats.items()[0].processed.delta.get("xdelta").succeeded, is(equalTo(10))); + /** + * Check minute-level stats exist (forwards) + */ + @Test + public void appstats_minute0() { + /* get the stats for this channel */ + try { + /* note that bounds are inclusive */ + PaginatedResult stats = ably.stats(new Param[] { + new Param("direction", "forwards"), + new Param("start", intervalIds[0]), + new Param("end", intervalIds[0]) + }); + assertNotNull("Expected non-null stats", stats); + assertThat("Expected 1 record", stats.items().length, is(equalTo(1))); + assertThat("Expected 50 messages", (int)stats.items()[0].inbound.all.all.count, is(equalTo(50))); + assertThat("Expected 5000 bytes of uncompressed data", (int)stats.items()[0].inbound.all.all.uncompressedData, is(equalTo(5000))); + assertThat("Expected 10 delta messages", (int)stats.items()[0].inbound.all.all.category.get("delta").count, is(equalTo(10))); + assertThat("Expected 10 successful delta generations", (int)stats.items()[0].processed.delta.get("xdelta").succeeded, is(equalTo(10))); - stats = ably.stats(new Param[] { - new Param("direction", "forwards"), - new Param("start", intervalIds[1]), - new Param("end", intervalIds[1]) - }); - assertNotNull("Expected non-null stats", stats); - assertThat("Expected 1 record", stats.items().length, is(equalTo(1))); - assertThat("Expected 60 messages", (int)stats.items()[0].inbound.all.all.count, is(equalTo(60))); - assertThat("Expected 6000 bytes of uncompressed data", (int)stats.items()[0].inbound.all.all.uncompressedData, is(equalTo(6000))); - assertThat("Expected 20 delta messages", (int)stats.items()[0].inbound.all.all.category.get("delta").count, is(equalTo(20))); - assertThat("Expected 20 successful delta generations", (int)stats.items()[0].processed.delta.get("xdelta").succeeded, is(equalTo(20))); + stats = ably.stats(new Param[] { + new Param("direction", "forwards"), + new Param("start", intervalIds[1]), + new Param("end", intervalIds[1]) + }); + assertNotNull("Expected non-null stats", stats); + assertThat("Expected 1 record", stats.items().length, is(equalTo(1))); + assertThat("Expected 60 messages", (int)stats.items()[0].inbound.all.all.count, is(equalTo(60))); + assertThat("Expected 6000 bytes of uncompressed data", (int)stats.items()[0].inbound.all.all.uncompressedData, is(equalTo(6000))); + assertThat("Expected 20 delta messages", (int)stats.items()[0].inbound.all.all.category.get("delta").count, is(equalTo(20))); + assertThat("Expected 20 successful delta generations", (int)stats.items()[0].processed.delta.get("xdelta").succeeded, is(equalTo(20))); - stats = ably.stats(new Param[] { - new Param("direction", "forwards"), - new Param("start", intervalIds[2]), - new Param("end", intervalIds[2]) - }); - assertNotNull("Expected non-null stats", stats); - assertThat("Expected 1 record", stats.items().length, is(equalTo(1))); - assertThat("Expected 70 messages", (int)stats.items()[0].inbound.all.all.count, is(equalTo(70))); - assertThat("Expected 7000 bytes of uncompressed data", (int)stats.items()[0].inbound.all.all.uncompressedData, is(equalTo(7000))); - assertThat("Expected 40 delta messages", (int)stats.items()[0].inbound.all.all.category.get("delta").count, is(equalTo(40))); - assertThat("Expected 40 successful delta generations", (int)stats.items()[0].processed.delta.get("xdelta").succeeded, is(equalTo(40))); - } catch (AblyException e) { - e.printStackTrace(); - fail("appstats_minute0: Unexpected exception"); - return; - } - } + stats = ably.stats(new Param[] { + new Param("direction", "forwards"), + new Param("start", intervalIds[2]), + new Param("end", intervalIds[2]) + }); + assertNotNull("Expected non-null stats", stats); + assertThat("Expected 1 record", stats.items().length, is(equalTo(1))); + assertThat("Expected 70 messages", (int)stats.items()[0].inbound.all.all.count, is(equalTo(70))); + assertThat("Expected 7000 bytes of uncompressed data", (int)stats.items()[0].inbound.all.all.uncompressedData, is(equalTo(7000))); + assertThat("Expected 40 delta messages", (int)stats.items()[0].inbound.all.all.category.get("delta").count, is(equalTo(40))); + assertThat("Expected 40 successful delta generations", (int)stats.items()[0].processed.delta.get("xdelta").succeeded, is(equalTo(40))); + } catch (AblyException e) { + e.printStackTrace(); + fail("appstats_minute0: Unexpected exception"); + return; + } + } - /** - * Check minute-level stats exist (backwards) - */ - @Test - public void appstats_minute1() { - /* get the stats for this channel */ - try { - /* note that bounds are inclusive */ - PaginatedResult stats = ably.stats(new Param[] { - new Param("direction", "backwards"), - new Param("start", intervalIds[0]), - new Param("end", intervalIds[0]) - }); - assertNotNull("Expected non-null stats", stats); - assertThat("Expected 1 record", stats.items().length, is(equalTo(1))); - assertThat("Expected 50 messages", (int)stats.items()[0].inbound.all.all.count, is(equalTo(50))); + /** + * Check minute-level stats exist (backwards) + */ + @Test + public void appstats_minute1() { + /* get the stats for this channel */ + try { + /* note that bounds are inclusive */ + PaginatedResult stats = ably.stats(new Param[] { + new Param("direction", "backwards"), + new Param("start", intervalIds[0]), + new Param("end", intervalIds[0]) + }); + assertNotNull("Expected non-null stats", stats); + assertThat("Expected 1 record", stats.items().length, is(equalTo(1))); + assertThat("Expected 50 messages", (int)stats.items()[0].inbound.all.all.count, is(equalTo(50))); - stats = ably.stats(new Param[] { - new Param("direction", "backwards"), - new Param("start", intervalIds[1]), - new Param("end", intervalIds[1]) - }); - assertNotNull("Expected non-null stats", stats); - assertThat("Expected 1 record", stats.items().length, is(equalTo(1))); - assertThat("Expected 60 messages", (int)stats.items()[0].inbound.all.all.count, is(equalTo(60))); + stats = ably.stats(new Param[] { + new Param("direction", "backwards"), + new Param("start", intervalIds[1]), + new Param("end", intervalIds[1]) + }); + assertNotNull("Expected non-null stats", stats); + assertThat("Expected 1 record", stats.items().length, is(equalTo(1))); + assertThat("Expected 60 messages", (int)stats.items()[0].inbound.all.all.count, is(equalTo(60))); - stats = ably.stats(new Param[] { - new Param("direction", "backwards"), - new Param("start", intervalIds[2]), - new Param("end", intervalIds[2]) - }); - assertNotNull("Expected non-null stats", stats); - assertThat("Expected 1 record", stats.items().length, is(equalTo(1))); - assertThat("Expected 70 messages", (int)stats.items()[0].inbound.all.all.count, is(equalTo(70))); - } catch (AblyException e) { - e.printStackTrace(); - fail("appstats_minute1: Unexpected exception"); - return; - } - } + stats = ably.stats(new Param[] { + new Param("direction", "backwards"), + new Param("start", intervalIds[2]), + new Param("end", intervalIds[2]) + }); + assertNotNull("Expected non-null stats", stats); + assertThat("Expected 1 record", stats.items().length, is(equalTo(1))); + assertThat("Expected 70 messages", (int)stats.items()[0].inbound.all.all.count, is(equalTo(70))); + } catch (AblyException e) { + e.printStackTrace(); + fail("appstats_minute1: Unexpected exception"); + return; + } + } - /** - * Check hour-level stats exist (forwards) - */ - @Test - public void appstats_hour0() { - /* get the stats for this channel */ - try { - PaginatedResult stats = ably.stats(new Param[] { - new Param("direction", "forwards"), - new Param("start", intervalIds[0]), - new Param("end", intervalIds[2]), - new Param("unit", "hour") - }); - assertNotNull("Expected non-null stats", stats); - assertThat("Expected 1 record", stats.items().length, is(equalTo(1))); - assertThat("Expected 180 messages", (int)stats.items()[0].inbound.all.all.count, is(equalTo(180))); - assertThat("Expected 18000 bytes of uncompressed data", (int)stats.items()[0].inbound.all.all.uncompressedData, is(equalTo(18000))); - assertThat("Expected 70 delta messages", (int)stats.items()[0].inbound.all.all.category.get("delta").count, is(equalTo(70))); - assertThat("Expected 70 successful delta generations", (int)stats.items()[0].processed.delta.get("xdelta").succeeded, is(equalTo(70))); - } catch (AblyException e) { - e.printStackTrace(); - fail("appstats_hour0: Unexpected exception"); - return; - } - } + /** + * Check hour-level stats exist (forwards) + */ + @Test + public void appstats_hour0() { + /* get the stats for this channel */ + try { + PaginatedResult stats = ably.stats(new Param[] { + new Param("direction", "forwards"), + new Param("start", intervalIds[0]), + new Param("end", intervalIds[2]), + new Param("unit", "hour") + }); + assertNotNull("Expected non-null stats", stats); + assertThat("Expected 1 record", stats.items().length, is(equalTo(1))); + assertThat("Expected 180 messages", (int)stats.items()[0].inbound.all.all.count, is(equalTo(180))); + assertThat("Expected 18000 bytes of uncompressed data", (int)stats.items()[0].inbound.all.all.uncompressedData, is(equalTo(18000))); + assertThat("Expected 70 delta messages", (int)stats.items()[0].inbound.all.all.category.get("delta").count, is(equalTo(70))); + assertThat("Expected 70 successful delta generations", (int)stats.items()[0].processed.delta.get("xdelta").succeeded, is(equalTo(70))); + } catch (AblyException e) { + e.printStackTrace(); + fail("appstats_hour0: Unexpected exception"); + return; + } + } - /** - * Check day-level stats exist (forwards) - */ - @Test - public void appstats_day0() { - /* get the stats for this channel */ - try { - PaginatedResult stats = ably.stats(new Param[] { - new Param("direction", "forwards"), - new Param("start", intervalIds[0]), - new Param("end", intervalIds[2]), - new Param("unit", "day") - }); - assertNotNull("Expected non-null stats", stats); - assertThat("Expected 1 record", stats.items().length, is(equalTo(1))); - assertThat("Expected 180 messages", (int)stats.items()[0].inbound.all.all.count, is(equalTo(180))); - } catch (AblyException e) { - e.printStackTrace(); - fail("appstats_day0: Unexpected exception"); - return; - } - } + /** + * Check day-level stats exist (forwards) + */ + @Test + public void appstats_day0() { + /* get the stats for this channel */ + try { + PaginatedResult stats = ably.stats(new Param[] { + new Param("direction", "forwards"), + new Param("start", intervalIds[0]), + new Param("end", intervalIds[2]), + new Param("unit", "day") + }); + assertNotNull("Expected non-null stats", stats); + assertThat("Expected 1 record", stats.items().length, is(equalTo(1))); + assertThat("Expected 180 messages", (int)stats.items()[0].inbound.all.all.count, is(equalTo(180))); + } catch (AblyException e) { + e.printStackTrace(); + fail("appstats_day0: Unexpected exception"); + return; + } + } - /** - * Check month-level stats exist (forwards) - */ - @Test - public void appstats_month0() { - /* get the stats for this channel */ - try { - PaginatedResult stats = ably.stats(new Param[] { - new Param("direction", "forwards"), - new Param("start", intervalIds[0]), - new Param("end", intervalIds[2]), - new Param("unit", "month") - }); - assertNotNull("Expected non-null stats", stats); - assertThat("Expected 1 record", stats.items().length, is(equalTo(1))); - assertThat("Expected 180 messages", (int)stats.items()[0].inbound.all.all.count, is(equalTo(180))); - } catch (AblyException e) { - e.printStackTrace(); - fail("appstats_month0: Unexpected exception"); - return; - } - } + /** + * Check month-level stats exist (forwards) + */ + @Test + public void appstats_month0() { + /* get the stats for this channel */ + try { + PaginatedResult stats = ably.stats(new Param[] { + new Param("direction", "forwards"), + new Param("start", intervalIds[0]), + new Param("end", intervalIds[2]), + new Param("unit", "month") + }); + assertNotNull("Expected non-null stats", stats); + assertThat("Expected 1 record", stats.items().length, is(equalTo(1))); + assertThat("Expected 180 messages", (int)stats.items()[0].inbound.all.all.count, is(equalTo(180))); + } catch (AblyException e) { + e.printStackTrace(); + fail("appstats_month0: Unexpected exception"); + return; + } + } - /** - * Publish events and check limit query param (backwards) - */ - @Test - public void appstats_limit0() { - /* get the stats for this channel */ - try { - PaginatedResult stats = ably.stats(new Param[] { - new Param("direction", "backwards"), - new Param("start", intervalIds[0]), - new Param("end", intervalIds[2]), - new Param("limit", String.valueOf(1)) - }); - assertNotNull("Expected non-null stats", stats); - assertThat("Expected 1 records", stats.items().length, is(equalTo(1))); - assertThat("Expected 70 messages", (int)stats.items()[0].inbound.all.all.count, is(equalTo(70))); - } catch (AblyException e) { - e.printStackTrace(); - fail("appstats_limit0: Unexpected exception"); - return; - } - } + /** + * Publish events and check limit query param (backwards) + */ + @Test + public void appstats_limit0() { + /* get the stats for this channel */ + try { + PaginatedResult stats = ably.stats(new Param[] { + new Param("direction", "backwards"), + new Param("start", intervalIds[0]), + new Param("end", intervalIds[2]), + new Param("limit", String.valueOf(1)) + }); + assertNotNull("Expected non-null stats", stats); + assertThat("Expected 1 records", stats.items().length, is(equalTo(1))); + assertThat("Expected 70 messages", (int)stats.items()[0].inbound.all.all.count, is(equalTo(70))); + } catch (AblyException e) { + e.printStackTrace(); + fail("appstats_limit0: Unexpected exception"); + return; + } + } - /** - * Check limit query param (forwards) - */ - @Test - public void appstats_limit1() { - try { - PaginatedResult stats = ably.stats(new Param[] { - new Param("direction", "forwards"), - new Param("start", intervalIds[0]), - new Param("end", intervalIds[2]), - new Param("limit", String.valueOf(1)) - }); - assertNotNull("Expected non-null stats", stats); - assertThat("Expected 1 records", stats.items().length, is(equalTo(1))); - assertThat("Expected 50 messages", (int)stats.items()[0].inbound.all.all.count, is(equalTo(50))); - } catch (AblyException e) { - e.printStackTrace(); - fail("appstats_limit1: Unexpected exception"); - return; - } - } + /** + * Check limit query param (forwards) + */ + @Test + public void appstats_limit1() { + try { + PaginatedResult stats = ably.stats(new Param[] { + new Param("direction", "forwards"), + new Param("start", intervalIds[0]), + new Param("end", intervalIds[2]), + new Param("limit", String.valueOf(1)) + }); + assertNotNull("Expected non-null stats", stats); + assertThat("Expected 1 records", stats.items().length, is(equalTo(1))); + assertThat("Expected 50 messages", (int)stats.items()[0].inbound.all.all.count, is(equalTo(50))); + } catch (AblyException e) { + e.printStackTrace(); + fail("appstats_limit1: Unexpected exception"); + return; + } + } - /** - * Check query pagination (backwards) - */ - @Test - public void appstats_pagination0() { - try { - PaginatedResult stats = ably.stats(new Param[] { - new Param("direction", "backwards"), - new Param("start", intervalIds[0]), - new Param("end", intervalIds[2]), - new Param("limit", String.valueOf(1)) - }); - assertNotNull("Expected non-null stats", stats); - assertThat("Expected 1 records", stats.items().length, is(equalTo(1))); - assertThat("Expected 70 messages", (int)stats.items()[0].inbound.all.all.count, is(equalTo(70))); - /* get next page */ - stats = stats.next(); - assertNotNull("Expected non-null stats", stats); - assertThat("Expected 1 records", stats.items().length, is(equalTo(1))); - assertThat("Expected 60 messages", (int)stats.items()[0].inbound.all.all.count, is(equalTo(60))); - /* get next page */ - stats = stats.next(); - assertNotNull("Expected non-null stats", stats); - assertThat("Expected 1 records", stats.items().length, is(equalTo(1))); - assertThat("Expected 50 messages", (int)stats.items()[0].inbound.all.all.count, is(equalTo(50))); - /* verify that there is no next page */ - assertFalse("Expected null next page", stats.hasNext()); - } catch (AblyException e) { - e.printStackTrace(); - fail("appstats_pagination0: Unexpected exception"); - return; - } - } + /** + * Check query pagination (backwards) + */ + @Test + public void appstats_pagination0() { + try { + PaginatedResult stats = ably.stats(new Param[] { + new Param("direction", "backwards"), + new Param("start", intervalIds[0]), + new Param("end", intervalIds[2]), + new Param("limit", String.valueOf(1)) + }); + assertNotNull("Expected non-null stats", stats); + assertThat("Expected 1 records", stats.items().length, is(equalTo(1))); + assertThat("Expected 70 messages", (int)stats.items()[0].inbound.all.all.count, is(equalTo(70))); + /* get next page */ + stats = stats.next(); + assertNotNull("Expected non-null stats", stats); + assertThat("Expected 1 records", stats.items().length, is(equalTo(1))); + assertThat("Expected 60 messages", (int)stats.items()[0].inbound.all.all.count, is(equalTo(60))); + /* get next page */ + stats = stats.next(); + assertNotNull("Expected non-null stats", stats); + assertThat("Expected 1 records", stats.items().length, is(equalTo(1))); + assertThat("Expected 50 messages", (int)stats.items()[0].inbound.all.all.count, is(equalTo(50))); + /* verify that there is no next page */ + assertFalse("Expected null next page", stats.hasNext()); + } catch (AblyException e) { + e.printStackTrace(); + fail("appstats_pagination0: Unexpected exception"); + return; + } + } - /** - * Check query pagination (forwards) - */ - @Test - public void appstats_pagination1() { - try { - PaginatedResult stats = ably.stats(new Param[] { - new Param("direction", "forwards"), - new Param("start", intervalIds[0]), - new Param("end", intervalIds[2]), - new Param("limit", String.valueOf(1)) - }); - assertNotNull("Expected non-null stats", stats); - assertThat("Expected 1 records", stats.items().length, is(equalTo(1))); - assertThat("Expected 50 messages", (int)stats.items()[0].inbound.all.all.count, is(equalTo(50))); - /* get next page */ - stats = stats.next(); - assertNotNull("Expected non-null stats", stats); - assertThat("Expected 1 records", stats.items().length, is(equalTo(1))); - assertThat("Expected 60 messages", (int)stats.items()[0].inbound.all.all.count, is(equalTo(60))); - /* get next page */ - stats = stats.next(); - assertNotNull("Expected non-null stats", stats); - assertThat("Expected 1 records", stats.items().length, is(equalTo(1))); - assertThat("Expected 70 messages", (int)stats.items()[0].inbound.all.all.count, is(equalTo(70))); - /* verify that there is no next page */ - assertFalse("Expected null next page", stats.hasNext()); - } catch (AblyException e) { - e.printStackTrace(); - fail("appstats_pagination1: Unexpected exception"); - return; - } - } + /** + * Check query pagination (forwards) + */ + @Test + public void appstats_pagination1() { + try { + PaginatedResult stats = ably.stats(new Param[] { + new Param("direction", "forwards"), + new Param("start", intervalIds[0]), + new Param("end", intervalIds[2]), + new Param("limit", String.valueOf(1)) + }); + assertNotNull("Expected non-null stats", stats); + assertThat("Expected 1 records", stats.items().length, is(equalTo(1))); + assertThat("Expected 50 messages", (int)stats.items()[0].inbound.all.all.count, is(equalTo(50))); + /* get next page */ + stats = stats.next(); + assertNotNull("Expected non-null stats", stats); + assertThat("Expected 1 records", stats.items().length, is(equalTo(1))); + assertThat("Expected 60 messages", (int)stats.items()[0].inbound.all.all.count, is(equalTo(60))); + /* get next page */ + stats = stats.next(); + assertNotNull("Expected non-null stats", stats); + assertThat("Expected 1 records", stats.items().length, is(equalTo(1))); + assertThat("Expected 70 messages", (int)stats.items()[0].inbound.all.all.count, is(equalTo(70))); + /* verify that there is no next page */ + assertFalse("Expected null next page", stats.hasNext()); + } catch (AblyException e) { + e.printStackTrace(); + fail("appstats_pagination1: Unexpected exception"); + return; + } + } - /** - * Check query pagination rel="first" (backwards) - */ - @Test - public void appstats_pagination2() { - try { - PaginatedResult stats = ably.stats(new Param[] { - new Param("direction", "backwards"), - new Param("start", intervalIds[0]), - new Param("end", intervalIds[2]), - new Param("limit", String.valueOf(1)) - }); - assertNotNull("Expected non-null stats", stats); - assertThat("Expected 1 records", stats.items().length, is(equalTo(1))); - assertThat("Expected 70 messages", (int)stats.items()[0].inbound.all.all.count, is(equalTo(70))); - /* get next page */ - stats = stats.next(); - assertNotNull("Expected non-null stats", stats); - assertThat("Expected 1 records", stats.items().length, is(equalTo(1))); - assertThat("Expected 60 messages", (int)stats.items()[0].inbound.all.all.count, is(equalTo(60))); - /* get first page */ - stats = stats.first(); - assertNotNull("Expected non-null stats", stats); - assertThat("Expected 1 records", stats.items().length, is(equalTo(1))); - assertThat("Expected 70 messages", (int)stats.items()[0].inbound.all.all.count, is(equalTo(70))); - } catch (AblyException e) { - e.printStackTrace(); - fail("appstats_pagination2: Unexpected exception"); - return; - } - } + /** + * Check query pagination rel="first" (backwards) + */ + @Test + public void appstats_pagination2() { + try { + PaginatedResult stats = ably.stats(new Param[] { + new Param("direction", "backwards"), + new Param("start", intervalIds[0]), + new Param("end", intervalIds[2]), + new Param("limit", String.valueOf(1)) + }); + assertNotNull("Expected non-null stats", stats); + assertThat("Expected 1 records", stats.items().length, is(equalTo(1))); + assertThat("Expected 70 messages", (int)stats.items()[0].inbound.all.all.count, is(equalTo(70))); + /* get next page */ + stats = stats.next(); + assertNotNull("Expected non-null stats", stats); + assertThat("Expected 1 records", stats.items().length, is(equalTo(1))); + assertThat("Expected 60 messages", (int)stats.items()[0].inbound.all.all.count, is(equalTo(60))); + /* get first page */ + stats = stats.first(); + assertNotNull("Expected non-null stats", stats); + assertThat("Expected 1 records", stats.items().length, is(equalTo(1))); + assertThat("Expected 70 messages", (int)stats.items()[0].inbound.all.all.count, is(equalTo(70))); + } catch (AblyException e) { + e.printStackTrace(); + fail("appstats_pagination2: Unexpected exception"); + return; + } + } - /** - * Check query pagination rel="first" (forwards) - */ - @Test - public void appstats_pagination3() { - try { - PaginatedResult stats = ably.stats(new Param[] { - new Param("direction", "forwards"), - new Param("start", intervalIds[0]), - new Param("end", intervalIds[2]), - new Param("limit", String.valueOf(1)) - }); - assertNotNull("Expected non-null stats", stats); - assertThat("Expected 1 records", stats.items().length, is(equalTo(1))); - assertThat("Expected 50 messages", (int)stats.items()[0].inbound.all.all.count, is(equalTo(50))); - /* get next page */ - stats = stats.next(); - assertNotNull("Expected non-null stats", stats); - assertThat("Expected 1 records", stats.items().length, is(equalTo(1))); - assertThat("Expected 60 messages", (int)stats.items()[0].inbound.all.all.count, is(equalTo(60))); - /* get first page */ - stats = stats.first(); - assertNotNull("Expected non-null stats", stats); - assertThat("Expected 1 records", stats.items().length, is(equalTo(1))); - assertThat("Expected 50 messages", (int)stats.items()[0].inbound.all.all.count, is(equalTo(50))); - } catch (AblyException e) { - e.printStackTrace(); - fail("appstats_pagination3: Unexpected exception"); - return; - } - } + /** + * Check query pagination rel="first" (forwards) + */ + @Test + public void appstats_pagination3() { + try { + PaginatedResult stats = ably.stats(new Param[] { + new Param("direction", "forwards"), + new Param("start", intervalIds[0]), + new Param("end", intervalIds[2]), + new Param("limit", String.valueOf(1)) + }); + assertNotNull("Expected non-null stats", stats); + assertThat("Expected 1 records", stats.items().length, is(equalTo(1))); + assertThat("Expected 50 messages", (int)stats.items()[0].inbound.all.all.count, is(equalTo(50))); + /* get next page */ + stats = stats.next(); + assertNotNull("Expected non-null stats", stats); + assertThat("Expected 1 records", stats.items().length, is(equalTo(1))); + assertThat("Expected 60 messages", (int)stats.items()[0].inbound.all.all.count, is(equalTo(60))); + /* get first page */ + stats = stats.first(); + assertNotNull("Expected non-null stats", stats); + assertThat("Expected 1 records", stats.items().length, is(equalTo(1))); + assertThat("Expected 50 messages", (int)stats.items()[0].inbound.all.all.count, is(equalTo(50))); + } catch (AblyException e) { + e.printStackTrace(); + fail("appstats_pagination3: Unexpected exception"); + return; + } + } } diff --git a/lib/src/test/java/io/ably/lib/test/rest/RestAuthAttributeTest.java b/lib/src/test/java/io/ably/lib/test/rest/RestAuthAttributeTest.java index 87655e736..107f25cf8 100644 --- a/lib/src/test/java/io/ably/lib/test/rest/RestAuthAttributeTest.java +++ b/lib/src/test/java/io/ably/lib/test/rest/RestAuthAttributeTest.java @@ -32,243 +32,243 @@ */ public class RestAuthAttributeTest extends ParameterizedTest { - private AblyRest ably; - - @Before - public void setupClient() throws Exception { - ClientOptions opts = createOptions(testVars.keys[0].keyStr); - opts.useTokenAuth = true; - ably = new AblyRest(opts); - } - - /** - * Stores the AuthOptions and TokenParams arguments as defaults for subsequent authorizations - *

- * Spec: RSA10g,RSA10j - *

- */ - @Test - public void auth_stores_options_params() { - try { - /* init custom TokenParams */ - Capability capability = new Capability(); - capability.addResource("testchannel", "subscribe"); - final String capabilityStr = capability.toString(); - final String testClientId = "firstClientId"; - TokenParams tokenParams = new TokenParams() {{ - ttl = 4000L; - clientId = testClientId; - capability = capabilityStr; - }}; - - /* init custom AuthOptions */ - AuthOptions authOptions = new AuthOptions() {{ - authCallback = new TokenCallback() { - private AblyRest ably = new AblyRest(createOptions(testVars.keys[0].keyStr)); - - @Override - public Object getTokenRequest(TokenParams params) throws AblyException { - return ably.auth.requestToken(params, null); - } - }; - key = testVars.keys[1].keyStr; - }}; - - /* authorise with custom options - * Deliberate use of British spelling alias authorise() to check that - * it works (0.9 RSA10l) */ - @SuppressWarnings("deprecation") - TokenDetails tokenDetails1 = ably.auth.authorise(tokenParams, authOptions); - - /* Verify that, - * tokenDetails1 isn't null, - * capability and clientId equals to the values of corresponding attributes in tokenParams */ - assertNotNull(tokenDetails1); - assertEquals(tokenDetails1.clientId, testClientId); - assertEquals(tokenDetails1.capability, capabilityStr); - - /* wait until token expires */ - try { - Thread.sleep(5000L); - } catch(InterruptedException ie) {} - - /* authorize with default options */ - TokenDetails tokenDetails2 = ably.auth.authorize(null, null); - - /* Verify that, - * tokenDetails2 isn't null, - * new token has to be issued, - * capability and clientId for different TokenDetails are the same */ - assertNotNull(tokenDetails2); - assertNotEquals(tokenDetails1.token, tokenDetails2.token); - assertEquals(tokenDetails1.capability, tokenDetails2.capability); - assertEquals(tokenDetails1.clientId, tokenDetails2.clientId); - } catch (Exception e) { - e.printStackTrace(); - fail("auth_stores_options_params: Unexpected exception"); - } - } - - /** - * Verify that {@link AuthOptions#queryTime} attribute don't stored/used for subsequent authorizations - *

- * Spec: RSA10g - *

- */ - @Test - public void auth_stores_options_exception_querytime() { - try { - final long fakeServerTime = -1000; - final String expectedClientId = "testClientId"; - ClientOptions opts = createOptions(testVars.keys[0].keyStr); - opts.clientId = expectedClientId; - AblyRest ablyForTime = new AblyRest(opts) { - @Override - public long time() throws AblyException { - return fakeServerTime; - } - }; - final AuthOptions authOptions = new AuthOptions(); - authOptions.key = ablyForTime.options.key; - authOptions.queryTime = true; - TokenParams tokenParams = new TokenParams(); - - /* create token request with custom AuthOptions that has attribute queryTime */ - TokenRequest tokenRequest = ablyForTime.auth.createTokenRequest(tokenParams, authOptions); - - /* verify that issued time of server equals fake expected value */ - assertEquals(expectedClientId, tokenRequest.clientId); - assertEquals(fakeServerTime, tokenRequest.timestamp); - - /* authorize for store custom AuthOptions that has attribute queryTime */ - try { - ablyForTime.auth.authorize(tokenParams, authOptions); - } catch (Throwable e) { - } - - /* create token request with stored AuthOptions */ - tokenRequest = ablyForTime.auth.createTokenRequest(tokenParams, null); - - /* Verify that, - * - timestamp not equals fake server time - * - timestamp equals local time */ - assertEquals(expectedClientId, tokenRequest.clientId); - assertNotEquals(fakeServerTime, tokenRequest.timestamp); - long localTime = System.currentTimeMillis(); - assertTrue((tokenRequest.timestamp >= (localTime - 500)) && (tokenRequest.timestamp <= (localTime + 500))); - } catch (Exception e) { - e.printStackTrace(); - fail("auth_stores_options_exception_querytime: Unexpected exception"); - } - } - - /** - * Verify that {@link TokenParams#timestamp} attribute don't stored/used for subsequent authorizations - *

- * Spec: RSA10g - *

- */ - @Test - public void auth_stores_options_exception_timestamp() { - final String expectedClientId = "clientIdForToken"; - final long expectedTimestamp = 11111; - try { - /* init ably for token */ - final ClientOptions optsForToken = createOptions(testVars.keys[0].keyStr); - optsForToken.clientId = expectedClientId; - final AblyRest ablyForToken = new AblyRest(optsForToken); - - /* create custom token callback for capturing timestamp values */ - final List timestampCapturedList = new ArrayList<>(); - TokenCallback tokenCallback = new TokenCallback() { - private List timestampCapturedList; - - public TokenCallback setTimestampCapturedList(List timestampCapturedList) { - this.timestampCapturedList = timestampCapturedList; - return this; - } - - @Override - public Object getTokenRequest(TokenParams params) throws AblyException { - this.timestampCapturedList.add(params.timestamp); - return ablyForToken.auth.requestToken(null, null); - } - }.setTimestampCapturedList(timestampCapturedList); - - /* authorize with custom timestamp */ - AuthOptions authOptions = new AuthOptions(); - authOptions.key = ably.options.key; - authOptions.authCallback = tokenCallback; - TokenParams tokenParams = new TokenParams(); - tokenParams.timestamp = expectedTimestamp; - TokenDetails tokenDetails1 = ably.auth.authorize(tokenParams, authOptions); - final String token1 = tokenDetails1.token; - final String clientId1 = tokenDetails1.clientId; - - /* force authorize with stored TokenParams values */ - TokenDetails tokenDetails2 = ably.auth.authorize(null, authOptions); - final String token2 = tokenDetails2.token; - final String clientId2 = tokenDetails2.clientId; - - /* Verify that, - * - new token was issued - * - authorize called twice - * - first timestamp value equals expected timestamp - * - second timestamp value is not expected - * tokenDetails1 and tokenDetails2 aren't null, - * the values of each attribute are equals */ - assertNotNull(tokenDetails1); - assertNotNull(tokenDetails2); - assertEquals(expectedClientId, clientId1); - assertEquals(clientId1, clientId2); - assertNotEquals(token1, token2); - assertThat(timestampCapturedList.size(), is(equalTo(2))); - assertEquals((long) timestampCapturedList.get(0), expectedTimestamp); - assertNotEquals(timestampCapturedList.get(0), timestampCapturedList.get(1)); - } catch (Exception e) { - e.printStackTrace(); - fail("auth_stores_options_exception_timestamp: Unexpected exception"); - } - } - - /** - * Verify - * will to issue a new token even if an existing token exists. - *

- * Spec: RSA10d - *

- */ - @Test - public void auth_authorize_force() { - try { - /* authorize with default options */ - TokenDetails tokenDetails1 = ably.auth.authorize(null, null); - - /* init custom AuthOptions */ - final String custom_test_value = "test_forced_token"; - AuthOptions authOptions = new AuthOptions() {{ - authCallback = new TokenCallback() { - @Override - public Object getTokenRequest(TokenParams params) throws AblyException { - return custom_test_value; - } - }; - }}; - - /* authorize with custom AuthOptions */ - TokenDetails tokenDetails2 = ably.auth.authorize(null, authOptions); - - /* Verify that, - * tokenDetails1 and tokenDetails2 aren't null, - * tokens are different, - * token from tokenDetails2 equals custom_test_value */ - assertNotNull(tokenDetails1); - assertNotNull(tokenDetails2); - assertNotEquals(tokenDetails1.token, tokenDetails2.token); - assertEquals(tokenDetails2.token, custom_test_value); - } catch (Exception e) { - e.printStackTrace(); - fail("auth_custom_options_authorize: Unexpected exception"); - } - } + private AblyRest ably; + + @Before + public void setupClient() throws Exception { + ClientOptions opts = createOptions(testVars.keys[0].keyStr); + opts.useTokenAuth = true; + ably = new AblyRest(opts); + } + + /** + * Stores the AuthOptions and TokenParams arguments as defaults for subsequent authorizations + *

+ * Spec: RSA10g,RSA10j + *

+ */ + @Test + public void auth_stores_options_params() { + try { + /* init custom TokenParams */ + Capability capability = new Capability(); + capability.addResource("testchannel", "subscribe"); + final String capabilityStr = capability.toString(); + final String testClientId = "firstClientId"; + TokenParams tokenParams = new TokenParams() {{ + ttl = 4000L; + clientId = testClientId; + capability = capabilityStr; + }}; + + /* init custom AuthOptions */ + AuthOptions authOptions = new AuthOptions() {{ + authCallback = new TokenCallback() { + private AblyRest ably = new AblyRest(createOptions(testVars.keys[0].keyStr)); + + @Override + public Object getTokenRequest(TokenParams params) throws AblyException { + return ably.auth.requestToken(params, null); + } + }; + key = testVars.keys[1].keyStr; + }}; + + /* authorise with custom options + * Deliberate use of British spelling alias authorise() to check that + * it works (0.9 RSA10l) */ + @SuppressWarnings("deprecation") + TokenDetails tokenDetails1 = ably.auth.authorise(tokenParams, authOptions); + + /* Verify that, + * tokenDetails1 isn't null, + * capability and clientId equals to the values of corresponding attributes in tokenParams */ + assertNotNull(tokenDetails1); + assertEquals(tokenDetails1.clientId, testClientId); + assertEquals(tokenDetails1.capability, capabilityStr); + + /* wait until token expires */ + try { + Thread.sleep(5000L); + } catch(InterruptedException ie) {} + + /* authorize with default options */ + TokenDetails tokenDetails2 = ably.auth.authorize(null, null); + + /* Verify that, + * tokenDetails2 isn't null, + * new token has to be issued, + * capability and clientId for different TokenDetails are the same */ + assertNotNull(tokenDetails2); + assertNotEquals(tokenDetails1.token, tokenDetails2.token); + assertEquals(tokenDetails1.capability, tokenDetails2.capability); + assertEquals(tokenDetails1.clientId, tokenDetails2.clientId); + } catch (Exception e) { + e.printStackTrace(); + fail("auth_stores_options_params: Unexpected exception"); + } + } + + /** + * Verify that {@link AuthOptions#queryTime} attribute don't stored/used for subsequent authorizations + *

+ * Spec: RSA10g + *

+ */ + @Test + public void auth_stores_options_exception_querytime() { + try { + final long fakeServerTime = -1000; + final String expectedClientId = "testClientId"; + ClientOptions opts = createOptions(testVars.keys[0].keyStr); + opts.clientId = expectedClientId; + AblyRest ablyForTime = new AblyRest(opts) { + @Override + public long time() throws AblyException { + return fakeServerTime; + } + }; + final AuthOptions authOptions = new AuthOptions(); + authOptions.key = ablyForTime.options.key; + authOptions.queryTime = true; + TokenParams tokenParams = new TokenParams(); + + /* create token request with custom AuthOptions that has attribute queryTime */ + TokenRequest tokenRequest = ablyForTime.auth.createTokenRequest(tokenParams, authOptions); + + /* verify that issued time of server equals fake expected value */ + assertEquals(expectedClientId, tokenRequest.clientId); + assertEquals(fakeServerTime, tokenRequest.timestamp); + + /* authorize for store custom AuthOptions that has attribute queryTime */ + try { + ablyForTime.auth.authorize(tokenParams, authOptions); + } catch (Throwable e) { + } + + /* create token request with stored AuthOptions */ + tokenRequest = ablyForTime.auth.createTokenRequest(tokenParams, null); + + /* Verify that, + * - timestamp not equals fake server time + * - timestamp equals local time */ + assertEquals(expectedClientId, tokenRequest.clientId); + assertNotEquals(fakeServerTime, tokenRequest.timestamp); + long localTime = System.currentTimeMillis(); + assertTrue((tokenRequest.timestamp >= (localTime - 500)) && (tokenRequest.timestamp <= (localTime + 500))); + } catch (Exception e) { + e.printStackTrace(); + fail("auth_stores_options_exception_querytime: Unexpected exception"); + } + } + + /** + * Verify that {@link TokenParams#timestamp} attribute don't stored/used for subsequent authorizations + *

+ * Spec: RSA10g + *

+ */ + @Test + public void auth_stores_options_exception_timestamp() { + final String expectedClientId = "clientIdForToken"; + final long expectedTimestamp = 11111; + try { + /* init ably for token */ + final ClientOptions optsForToken = createOptions(testVars.keys[0].keyStr); + optsForToken.clientId = expectedClientId; + final AblyRest ablyForToken = new AblyRest(optsForToken); + + /* create custom token callback for capturing timestamp values */ + final List timestampCapturedList = new ArrayList<>(); + TokenCallback tokenCallback = new TokenCallback() { + private List timestampCapturedList; + + public TokenCallback setTimestampCapturedList(List timestampCapturedList) { + this.timestampCapturedList = timestampCapturedList; + return this; + } + + @Override + public Object getTokenRequest(TokenParams params) throws AblyException { + this.timestampCapturedList.add(params.timestamp); + return ablyForToken.auth.requestToken(null, null); + } + }.setTimestampCapturedList(timestampCapturedList); + + /* authorize with custom timestamp */ + AuthOptions authOptions = new AuthOptions(); + authOptions.key = ably.options.key; + authOptions.authCallback = tokenCallback; + TokenParams tokenParams = new TokenParams(); + tokenParams.timestamp = expectedTimestamp; + TokenDetails tokenDetails1 = ably.auth.authorize(tokenParams, authOptions); + final String token1 = tokenDetails1.token; + final String clientId1 = tokenDetails1.clientId; + + /* force authorize with stored TokenParams values */ + TokenDetails tokenDetails2 = ably.auth.authorize(null, authOptions); + final String token2 = tokenDetails2.token; + final String clientId2 = tokenDetails2.clientId; + + /* Verify that, + * - new token was issued + * - authorize called twice + * - first timestamp value equals expected timestamp + * - second timestamp value is not expected + * tokenDetails1 and tokenDetails2 aren't null, + * the values of each attribute are equals */ + assertNotNull(tokenDetails1); + assertNotNull(tokenDetails2); + assertEquals(expectedClientId, clientId1); + assertEquals(clientId1, clientId2); + assertNotEquals(token1, token2); + assertThat(timestampCapturedList.size(), is(equalTo(2))); + assertEquals((long) timestampCapturedList.get(0), expectedTimestamp); + assertNotEquals(timestampCapturedList.get(0), timestampCapturedList.get(1)); + } catch (Exception e) { + e.printStackTrace(); + fail("auth_stores_options_exception_timestamp: Unexpected exception"); + } + } + + /** + * Verify + * will to issue a new token even if an existing token exists. + *

+ * Spec: RSA10d + *

+ */ + @Test + public void auth_authorize_force() { + try { + /* authorize with default options */ + TokenDetails tokenDetails1 = ably.auth.authorize(null, null); + + /* init custom AuthOptions */ + final String custom_test_value = "test_forced_token"; + AuthOptions authOptions = new AuthOptions() {{ + authCallback = new TokenCallback() { + @Override + public Object getTokenRequest(TokenParams params) throws AblyException { + return custom_test_value; + } + }; + }}; + + /* authorize with custom AuthOptions */ + TokenDetails tokenDetails2 = ably.auth.authorize(null, authOptions); + + /* Verify that, + * tokenDetails1 and tokenDetails2 aren't null, + * tokens are different, + * token from tokenDetails2 equals custom_test_value */ + assertNotNull(tokenDetails1); + assertNotNull(tokenDetails2); + assertNotEquals(tokenDetails1.token, tokenDetails2.token); + assertEquals(tokenDetails2.token, custom_test_value); + } catch (Exception e) { + e.printStackTrace(); + fail("auth_custom_options_authorize: Unexpected exception"); + } + } } diff --git a/lib/src/test/java/io/ably/lib/test/rest/RestAuthTest.java b/lib/src/test/java/io/ably/lib/test/rest/RestAuthTest.java index 5e16c5978..b1844d4bc 100644 --- a/lib/src/test/java/io/ably/lib/test/rest/RestAuthTest.java +++ b/lib/src/test/java/io/ably/lib/test/rest/RestAuthTest.java @@ -40,1883 +40,1883 @@ public class RestAuthTest extends ParameterizedTest { - @Rule - public Timeout testTimeout = Timeout.seconds(40); - - @Rule - public ExpectedException thrown = ExpectedException.none(); - - /** - * Init token server - */ - @BeforeClass - public static void auth_start_tokenserver() { - try { - ClientOptions opts = testVars.createOptions(testVars.keys[0].keyStr); - AblyRest ably = new AblyRest(opts); - tokenServer = new TokenServer(ably, 8982); - tokenServer.start(); - - nanoHTTPD = new SessionHandlerNanoHTTPD(27335); - nanoHTTPD.start(NanoHTTPD.SOCKET_READ_TIMEOUT, true); - - while (!nanoHTTPD.wasStarted()) { - try { - Thread.sleep(100); - } catch (InterruptedException e) { - e.printStackTrace(); - } - } - } catch (IOException e) { - e.printStackTrace(); - fail("auth_start_tokenserver: Unexpected exception starting server"); - } catch (AblyException e) { - e.printStackTrace(); - fail("auth_start_tokenserver: Unexpected exception starting server"); - } - } - - /** - * Kill token server - */ - @AfterClass - public static void auth_stop_tokenserver() { - if(tokenServer != null) - tokenServer.stop(); - if (nanoHTTPD != null) - nanoHTTPD.stop(); - } - - /** - * Init library with a key only - */ - @Test - public void authinit0() { - try { - AblyRest ably = new AblyRest(testVars.keys[0].keyStr); - assertEquals("Unexpected Auth method mismatch", ably.auth.getAuthMethod(), AuthMethod.basic); - assertEquals("Unexpected clientId mismatch", ably.auth.clientId, "*"); - } catch (AblyException e) { - e.printStackTrace(); - fail("authinit0: Unexpected exception instantiating library"); - } - } - - /** - * Init library with a key and tls=false results in an error - * Spec: RSC18 - */ - @Test - public void auth_basic_nontls() { - try { - ClientOptions opts = createOptions(testVars.keys[0].keyStr); - opts.tls = false; - AblyRest ably = new AblyRest(opts); - ably.stats(null); - fail("Unexpected success calling with Basic auth over httpCore"); - } catch (AblyException e) { - e.printStackTrace(); - assertEquals("Verify expected error code", e.errorInfo.statusCode, 401); - } - } - - /** - * Init library with useTokenAuth set - */ - @Test - public void authinit0_useTokenAuth() { - try { - ClientOptions opts = createOptions(testVars.keys[0].keyStr); - opts.useTokenAuth = true; - AblyRest ably = new AblyRest(opts); - assertEquals("Unexpected Auth method mismatch", ably.auth.getAuthMethod(), AuthMethod.token); - /* Spec: RSA12a */ - assertEquals("Unexpected clientId mismatch", ably.auth.clientId, null); - } catch (AblyException e) { - e.printStackTrace(); - fail("authinit0point5: Unexpected exception instantiating library"); - } - } - - /** - * Init library with a token only - */ - @Test - public void authinit1() { - try { - ClientOptions opts = new ClientOptions(); - opts.token = "this_is_not_really_a_token"; - AblyRest ably = new AblyRest(opts); - assertEquals("Unexpected Auth method mismatch", ably.auth.getAuthMethod(), AuthMethod.token); - /* Spec: RSA12a */ - assertEquals("Unexpected clientId mismatch", ably.auth.clientId, null); - } catch (AblyException e) { - e.printStackTrace(); - fail("authinit1: Unexpected exception instantiating library"); - } - } - - /** - * Init library with a token callback - */ - private boolean authinit2_cbCalled; - @Test - public void authinit2() { - try { - ClientOptions opts = createOptions(); - opts.restHost = testVars.restHost; - opts.environment = testVars.environment; - opts.port = testVars.port; - opts.tlsPort = testVars.tlsPort; - opts.tls = testVars.tls; - opts.authCallback = new TokenCallback() { - @Override - public String getTokenRequest(TokenParams params) throws AblyException { - authinit2_cbCalled = true; - return "this_is_not_really_a_token_request"; - }}; - AblyRest ably = new AblyRest(opts); - /* make a call to trigger token request */ - try { - ably.stats(null); - } catch(Throwable t) {} - assertEquals("Unexpected Auth method mismatch", ably.auth.getAuthMethod(), AuthMethod.token); - /* Spec: RSA12a */ - assertEquals("Unexpected clientId mismatch", ably.auth.clientId, null); - assertTrue("Token callback not called", authinit2_cbCalled); - } catch (AblyException e) { - e.printStackTrace(); - fail("authinit2: Unexpected exception instantiating library"); - } - } - - /** - * Init library with a key and clientId; expect token auth to be chosen - * Spec: RSA4, RSC17, RSA7b1 - */ - @Test - public void authinit_clientId_implies_token() { - try { - ClientOptions opts = createOptions(testVars.keys[0].keyStr); - opts.clientId = "testClientId"; - AblyRest ably = new AblyRest(opts); - assertEquals("Unexpected Auth method mismatch", ably.auth.getAuthMethod(), AuthMethod.token); - assertEquals("Unexpected clientId mismatch", ably.auth.clientId, "testClientId"); - } catch (AblyException e) { - e.printStackTrace(); - fail("authinit_clientId_implies_token: Unexpected exception instantiating library"); - } - } - - /** - * Init library with a key and token; verify Auth.clientId is null before - * authorization and set following auth - * Spec: RSA12b, RSA7b2 - */ - @Test - public void auth_clientid_null_before_auth() { - try { - final String defaultClientId = "default clientId"; - ClientOptions opts = createOptions(testVars.keys[0].keyStr); - opts.useTokenAuth = true; - opts.defaultTokenParams = new TokenParams() {{ this.clientId = defaultClientId; }}; - AblyRest ably = new AblyRest(opts); - assertEquals("Unexpected Auth method mismatch", ably.auth.getAuthMethod(), AuthMethod.token); - assertEquals("Unexpected clientId mismatch", ably.auth.clientId, null); - ably.auth.authorize(null, null); - assertEquals("Unexpected clientId mismatch", ably.auth.clientId, defaultClientId); - } catch (AblyException e) { - e.printStackTrace(); - fail("authinit_token_implies_token: Unexpected exception instantiating library"); - } - } - - /** - * Init library with a key and token; expect token auth to be chosen - * Spec: RSA4 - */ - @Test - public void authinit_token_implies_token() { - try { - ClientOptions opts = createOptions(testVars.keys[0].keyStr); - opts.token = testVars.keys[0].keyStr; - AblyRest ably = new AblyRest(opts); - assertEquals("Unexpected Auth method mismatch", ably.auth.getAuthMethod(), AuthMethod.token); - assertEquals("Unexpected clientId mismatch", ably.auth.clientId, null); - } catch (AblyException e) { - e.printStackTrace(); - fail("authinit_token_implies_token: Unexpected exception instantiating library"); - } - } - - /** - * Init library with a key and tokenDetails; expect token auth to be chosen - * Spec: RSA4 - */ - @Test - public void authinit_tokendetails_implies_token() { - try { - ClientOptions opts = createOptions(testVars.keys[0].keyStr); - opts.tokenDetails = new TokenDetails() {{ token = testVars.keys[0].keyStr; }}; - AblyRest ably = new AblyRest(opts); - assertEquals("Unexpected Auth method mismatch", ably.auth.getAuthMethod(), AuthMethod.token); - assertEquals("Unexpected clientId mismatch", ably.auth.clientId, null); - } catch (AblyException e) { - e.printStackTrace(); - fail("authinit_token_implies_token: Unexpected exception instantiating library"); - } - } - - /** - * Init library with a key and authCallback; expect token auth to be chosen - * Spec: RSA4 - */ - @Test - public void authinit_authcallback_implies_token() { - try { - ClientOptions opts = createOptions(testVars.keys[0].keyStr); - opts.authCallback = new TokenCallback() { - @Override - public Object getTokenRequest(TokenParams params) throws AblyException { - return null; - }}; - AblyRest ably = new AblyRest(opts); - assertEquals("Unexpected Auth method mismatch", ably.auth.getAuthMethod(), AuthMethod.token); - assertEquals("Unexpected clientId mismatch", ably.auth.clientId, null); - } catch (AblyException e) { - e.printStackTrace(); - fail("authinit_token_implies_token: Unexpected exception instantiating library"); - } - } - - /** - * Init library with a key and authUrl; expect token auth to be chosen - * Spec: RSA4 - */ - @Test - public void authinit_authurl_implies_token() { - try { - ClientOptions opts = createOptions(testVars.keys[0].keyStr); - opts.authUrl = "http://auth.url"; - AblyRest ably = new AblyRest(opts); - assertEquals("Unexpected Auth method mismatch", ably.auth.getAuthMethod(), AuthMethod.token); - } catch (AblyException e) { - e.printStackTrace(); - fail("authinit_token_implies_token: Unexpected exception instantiating library"); - } - } - - /** - * Init library with a token - */ - @Test - public void authinit4() { - try { - ClientOptions optsForToken = createOptions(testVars.keys[0].keyStr); - AblyRest ablyForToken = new AblyRest(optsForToken); - TokenDetails tokenDetails = ablyForToken.auth.requestToken(null, null); - assertNotNull("Expected token value", tokenDetails.token); - ClientOptions opts = new ClientOptions(); - opts.token = tokenDetails.token; - opts.environment = testVars.environment; - AblyRest ably = new AblyRest(opts); - assertEquals("Unexpected Auth method mismatch", ably.auth.getAuthMethod(), AuthMethod.token); - assertEquals("Unexpected clientId mismatch", ably.auth.clientId, null); - } catch (AblyException e) { - e.printStackTrace(); - fail("authinit3: Unexpected exception instantiating library"); - } - } - - /** - * Verify authURL called and handled when returning token request - * Spec: RSA8c - */ - @Test - public void auth_authURL_tokenrequest() { - try { - ClientOptions opts = createOptions(); - opts.environment = testVars.environment; - opts.authUrl = "http://localhost:8982/get-token-request"; - AblyRest ably = new AblyRest(opts); - /* make a call to trigger token request */ - try { - TokenDetails tokenDetails = ably.auth.requestToken(null, null); - assertNotNull("Expected token value", tokenDetails.token); - } catch (AblyException e) { - e.printStackTrace(); - fail("auth_authURL_tokenrequest: Unexpected exception requesting token"); - } - } catch (AblyException e) { - e.printStackTrace(); - fail("auth_authURL_tokenrequest: Unexpected exception instantiating library"); - } - } - - /** - * Verify authURL called and handled when returning token request, use POST method - * Spec: RSA8c1b - */ - @Test - public void auth_authURL_tokenrequest_post() { - try { - ClientOptions opts = createOptions(); - opts.environment = testVars.environment; - opts.authUrl = "http://localhost:8982/post-token-request"; - opts.authMethod = HttpConstants.Methods.POST; - AblyRest ably = new AblyRest(opts); - /* make a call to trigger token request */ - try { - TokenDetails tokenDetails = ably.auth.requestToken(null, null); - assertNotNull("Expected token value", tokenDetails.token); - } catch (AblyException e) { - e.printStackTrace(); - fail("auth_authURL_tokenrequest: Unexpected exception requesting token"); - } - } catch (AblyException e) { - e.printStackTrace(); - fail("auth_authURL_tokenrequest: Unexpected exception instantiating library"); - } - } - - - /** - * Verify authURL called and handled when returning token - * Spec: RSA8c - */ - @Test - public void auth_authURL_token() { - try { - ClientOptions opts = createOptions(); - opts.environment = testVars.environment; - opts.authUrl = "http://localhost:8982/get-token"; - AblyRest ably = new AblyRest(opts); - /* make a call to trigger token request */ - try { - TokenDetails tokenDetails = ably.auth.requestToken(null, null); - assertNotNull("Expected token value", tokenDetails.token); - } catch (AblyException e) { - e.printStackTrace(); - fail("auth_authURL_token: Unexpected exception requesting token"); - } - } catch (AblyException e) { - e.printStackTrace(); - fail("auth_authURL_token: Unexpected exception instantiating library"); - } - } - - /** - * Verify authURL called and handled when returning error - * Spec: RSA8c - */ - @Test - public void auth_authURL_err() { - try { - ClientOptions opts = createOptions(); - opts.environment = testVars.environment; - opts.authUrl = "http://localhost:8982/404"; - AblyRest ably = new AblyRest(opts); - /* make a call to trigger token request */ - try { - ably.auth.requestToken(null, null); - fail("auth_authURL_err: Unexpected success requesting token"); - } catch (AblyException e) { - assertEquals("Expected error code", e.errorInfo.code, 80019); - assertEquals("Expected forwarded error code", ((AblyException)e.getCause()).errorInfo.statusCode, 404); - } - } catch (AblyException e) { - e.printStackTrace(); - fail("auth_authURL_token: Unexpected exception instantiating library"); - } - } - - /** - * Verify authURL called and handled when timing out - * Spec: RSA8c, RSA4c - */ - @Test - public void auth_authURL_timeout() { - try { - ClientOptions opts = createOptions(); - opts.environment = testVars.environment; - opts.authUrl = "http://localhost:8982/wait?delay=6000"; - opts.httpRequestTimeout = 5000; - AblyRest ably = new AblyRest(opts); - /* make a call to trigger token request */ - try { - ably.auth.requestToken(null, null); - fail("auth_authURL_err: Unexpected success requesting token"); - } catch (AblyException e) { - assertEquals("Expected error code", e.errorInfo.code, 80019); - assertEquals("Expected forwarded error code", ((AblyException)e.getCause()).errorInfo.statusCode, 500); - assertTrue("Expected forwarded forwarded exception", (e.getCause().getCause()) instanceof SocketTimeoutException); - } - } catch (AblyException e) { - e.printStackTrace(); - fail("auth_authURL_timeout: Unexpected exception instantiating library"); - } - } - - /** - * Verify authURL is passed specified params in a GET - * Spec: RSA8c1a - */ - @Test - public void auth_authURL_authParams_get() { - try { - DebugOptions opts = new DebugOptions(testVars.keys[0].keyStr); - fillInOptions(opts); - RawHttpTracker httpListener = new RawHttpTracker(); - opts.httpListener = httpListener; - - opts.authUrl = "http://localhost:8982/get-token-request"; - opts.authParams = new Param[]{new Param("test-param", "test-value")}; - AblyRest ably = new AblyRest(opts); - - /* make a call to trigger token request */ - ably.auth.requestToken(null, null); - - /* check request contained expected params */ - assertTrue("Verify expected params passed to authURL", httpListener.getFirstRequest().url.getQuery().contains("test-param=test-value")); - } catch (AblyException e) { - e.printStackTrace(); - fail("auth_authURL_authParams_get: Unexpected exception instantiating library"); - } - } - - /** - * Verify authURL is passed specified params in a POST body - * Spec: RSA8c1b - */ - @Test - public void auth_authURL_authParams_post() { - try { - DebugOptions opts = new DebugOptions(testVars.keys[0].keyStr); - fillInOptions(opts); - RawHttpTracker httpListener = new RawHttpTracker(); - opts.httpListener = httpListener; - - opts.authUrl = "http://localhost:8982/post-token-request"; - opts.authMethod = HttpConstants.Methods.POST; - opts.authParams = new Param[]{new Param("test-param", "test-value")}; - AblyRest ably = new AblyRest(opts); - - /* make a call to trigger token request */ - ably.auth.requestToken(null, null); - - /* check request contained expected params */ - byte[] requestBody = httpListener.getFirstRequest().requestBody.getEncoded(); - assertTrue("Verify expected params passed to authURL", (new String(requestBody, "UTF-8")).contains("test-param=test-value")); - } catch (AblyException e) { - e.printStackTrace(); - fail("auth_authURL_authParams_post: Unexpected exception instantiating library"); - } catch (UnsupportedEncodingException e) { - e.printStackTrace(); - fail("auth_authURL_authParams_post: Unexpected exception decoding request body"); - } - } - - /** - * Verify authURL is passed specified params in a GET - * Spec: RSA8c1c - */ - @Test - public void auth_authURL_urlParams_get() { - try { - DebugOptions opts = new DebugOptions(testVars.keys[0].keyStr); - fillInOptions(opts); - RawHttpTracker httpListener = new RawHttpTracker(); - opts.httpListener = httpListener; - - opts.authUrl = "http://localhost:8982/get-token-request?test-param=test-value"; - AblyRest ably = new AblyRest(opts); - - /* make a call to trigger token request */ - ably.auth.requestToken(null, null); - - /* check request contained expected params */ - assertTrue("Verify expected params passed to authURL", httpListener.getFirstRequest().url.getQuery().contains("test-param=test-value")); - } catch (AblyException e) { - e.printStackTrace(); - fail("auth_authURL_urlParams_get: Unexpected exception instantiating library"); - } - } - - /** - * Verify authURL is passed specified params in a POST - * Spec: RSA8c1c - */ - @Test - public void auth_authURL_urlParams_post() { - try { - DebugOptions opts = new DebugOptions(testVars.keys[0].keyStr); - fillInOptions(opts); - RawHttpTracker httpListener = new RawHttpTracker(); - opts.httpListener = httpListener; - - opts.authUrl = "http://localhost:8982/post-token-request?test-param=test-value"; - opts.authMethod = HttpConstants.Methods.POST; - AblyRest ably = new AblyRest(opts); - - /* make a call to trigger token request */ - ably.auth.requestToken(null, null); - - /* check request contained expected params */ - assertTrue("Verify expected params passed to authURL", httpListener.getFirstRequest().url.getQuery().contains("test-param=test-value")); - } catch (AblyException e) { - e.printStackTrace(); - fail("auth_authURL_urlParams_post: Unexpected exception instantiating library"); - } - } - - /** - * Verify authURL is passed specified params in a GET, with the specified precedence - * Spec: RSA8c1c - */ - @Test - public void auth_authURL_urlParams_get_conflict() { - try { - DebugOptions opts = new DebugOptions(testVars.keys[0].keyStr); - fillInOptions(opts); - RawHttpTracker httpListener = new RawHttpTracker(); - opts.httpListener = httpListener; - - opts.authUrl = "http://localhost:8982/get-token-request?test-param=test-value-urlParam"; - opts.authParams = new Param[]{new Param("test-param", "test-value-authParam")}; - AblyRest ably = new AblyRest(opts); - - /* make a call to trigger token request */ - ably.auth.requestToken(null, null); - - /* check request contained expected params */ - assertTrue("Verify expected params passed to authURL", httpListener.getFirstRequest().url.getQuery().contains("test-param=test-value-authParam")); - } catch (AblyException e) { - e.printStackTrace(); - fail("auth_authURL_urlParams_get: Unexpected exception instantiating library"); - } - } - - /** - * Verify tokenParams take precedence over authParams in authURL request - * Spec: RSA8c2 - */ - @Test - public void auth_authURL_authParams_get_conflict() { - try { - DebugOptions opts = new DebugOptions(testVars.keys[0].keyStr); - fillInOptions(opts); - RawHttpTracker httpListener = new RawHttpTracker(); - opts.httpListener = httpListener; - - opts.authUrl = "http://localhost:8982/get-token-request"; - opts.authParams = new Param[]{new Param("ttl", "500")}; - AblyRest ably = new AblyRest(opts); - - /* make a call to trigger token request */ - ably.auth.requestToken(new TokenParams() {{ - ttl = 300; - }}, null); - - /* check request contained expected params */ - assertTrue("Verify expected params passed to authURL", httpListener.getFirstRequest().url.getQuery().contains("ttl=300")); - } catch (AblyException e) { - e.printStackTrace(); - fail("auth_authURL_urlParams_get: Unexpected exception instantiating library"); - } - } - - /** - * Verify authURL is passed specified headers in a GET - * Spec: RSA8c3 - */ - @Test - public void auth_authURL_authHeaders_get() { - try { - DebugOptions opts = new DebugOptions(testVars.keys[0].keyStr); - fillInOptions(opts); - RawHttpTracker httpListener = new RawHttpTracker(); - opts.httpListener = httpListener; - - opts.authUrl = "http://localhost:8982/get-token-request"; - opts.authHeaders = new Param[]{new Param("test-header", "test-value")}; - AblyRest ably = new AblyRest(opts); - - /* make a call to trigger token request */ - ably.auth.requestToken(null, null); - - /* check request contained expected params */ - List headers = httpListener.getFirstRequest().requestHeaders.get("test-header"); - assertTrue("Verify expected headers passed to authURL", headers.contains("test-value")); - } catch (AblyException e) { - e.printStackTrace(); - fail("auth_authURL_authHeaders_get: Unexpected exception instantiating library"); - } - } - - /** - * Verify authURL is passed specified headers in a POST - * Spec: RSA8c3 - */ - @Test - public void auth_authURL_authHeaders_post() { - try { - DebugOptions opts = new DebugOptions(testVars.keys[0].keyStr); - fillInOptions(opts); - RawHttpTracker httpListener = new RawHttpTracker(); - opts.httpListener = httpListener; - - opts.authUrl = "http://localhost:8982/get-token-request"; - opts.authHeaders = new Param[]{new Param("test-header", "test-value")}; - AblyRest ably = new AblyRest(opts); - - /* make a call to trigger token request */ - ably.auth.requestToken(null, null); - - /* check request contained expected params */ - List headers = httpListener.getFirstRequest().requestHeaders.get("test-header"); - assertTrue("Verify expected headers passed to authURL", headers.contains("test-value")); - } catch (AblyException e) { - e.printStackTrace(); - fail("auth_authURL_authHeaders_post: Unexpected exception instantiating library"); - } - } - - /** - * Verify authCallback called and handled when returning {@code TokenRequest} - * Spec: RSA8d - */ - @Test - public void auth_authcallback_tokenrequest() { - try { - /* implement callback, using Ably instance with key */ - TokenCallback authCallback = new TokenCallback() { - private AblyRest ably = new AblyRest(createOptions(testVars.keys[0].keyStr)); - @Override - public Object getTokenRequest(TokenParams params) throws AblyException { - return ably.auth.createTokenRequest(params, null); - } - }; - - /* create Ably instance without key */ - ClientOptions opts = createOptions(); - opts.authCallback = authCallback; - AblyRest ably = new AblyRest(opts); - - /* make a call to trigger token request */ - try { - TokenDetails tokenDetails = ably.auth.requestToken(null, null); - assertNotNull("Expected token value", tokenDetails.token); - } catch (AblyException e) { - e.printStackTrace(); - fail("auth_authURL_tokenrequest: Unexpected exception requesting token"); - } - } catch (AblyException e) { - e.printStackTrace(); - fail("auth_authURL_tokenrequest: Unexpected exception instantiating library"); - } - } - - /** - * Verify authCallback called and handled when returning {@code TokenDetails} - * Spec: RSA8d - */ - @Test - public void auth_authcallback_tokendetails() { - try { - /* implement callback, using Ably instance with key */ - TokenCallback authCallback = new TokenCallback() { - private AblyRest ably = new AblyRest(createOptions(testVars.keys[0].keyStr)); - @Override - public Object getTokenRequest(TokenParams params) throws AblyException { - return ably.auth.requestToken(params, null); - } - }; - - /* create Ably instance without key */ - ClientOptions opts = createOptions(); - opts.authCallback = authCallback; - AblyRest ably = new AblyRest(opts); - - /* make a call to trigger token request */ - try { - TokenDetails tokenDetails = ably.auth.requestToken(null, null); - assertNotNull("Expected token value", tokenDetails.token); - } catch (AblyException e) { - e.printStackTrace(); - fail("auth_authURL_tokenrequest: Unexpected exception requesting token"); - } - } catch (AblyException e) { - e.printStackTrace(); - fail("auth_authURL_tokenrequest: Unexpected exception instantiating library"); - } - } - - /** - * Verify authCallback called and handled when returning token string - * Spec: RSA8d - */ - @Test - public void auth_authcallback_tokenstring() throws AblyException { - /* implement callback, using Ably instance with key */ - TokenCallback authCallback = new TokenCallback() { - private AblyRest ably = new AblyRest(createOptions(testVars.keys[0].keyStr)); - @Override - public Object getTokenRequest(TokenParams params) throws AblyException { - return ably.auth.requestToken(params, null).token; - } - }; - - /* create Ably instance without key */ - ClientOptions opts = createOptions(); - opts.authCallback = authCallback; - AblyRest ably = new AblyRest(opts); - - /* make a call to trigger token request */ - try { - TokenDetails tokenDetails = ably.auth.requestToken(null, null); - assertNotNull("Expected token value", tokenDetails.token); - } catch (AblyException e) { - e.printStackTrace(); - fail("auth_authURL_tokenrequest: Unexpected exception requesting token"); - } - } - - /** - * Verify authCallback called when token expires; Ably initialised with token - */ - @Test - public void auth_authcallback_token_expire() { - try { - ClientOptions optsForToken = createOptions(testVars.keys[0].keyStr); - final AblyRest ablyForToken = new AblyRest(optsForToken); - TokenDetails tokenDetails = ablyForToken.auth.requestToken(new TokenParams() {{ ttl = 5000L; }}, null); - assertNotNull("Expected token value", tokenDetails.token); - - /* implement callback, using Ably instance with key */ - final class TokenGenerator implements TokenCallback { - @Override - public Object getTokenRequest(TokenParams params) throws AblyException { - ++cbCount; - return ablyForToken.auth.requestToken(params, null); - } - public int getCbCount() { return cbCount; } - private int cbCount = 0; - }; - - TokenGenerator authCallback = new TokenGenerator(); - - /* create Ably instance without key */ - ClientOptions opts = createOptions(); - opts.token = tokenDetails.token; - opts.authCallback = authCallback; - AblyRest ably = new AblyRest(opts); - - /* wait until token expires */ - try { - Thread.sleep(6000L); - } catch(InterruptedException ie) {} - - /* make a request that relies on the token */ - try { - ably.stats(new Param[] { new Param("by", "hour"), new Param("limit", "1") }); - } catch (AblyException e) { - e.printStackTrace(); - fail("auth_authURL_tokenrequest: Unexpected exception requesting token"); - } - - /* verify that the auth callback was called */ - assertEquals("Expected token generator to be called", 1, authCallback.getCbCount()); - } catch (AblyException e) { - e.printStackTrace(); - fail("auth_authURL_tokenrequest: Unexpected exception instantiating library"); - } - } - - /** - * Verify authCallback called when token expires; Ably initialised with key - */ - @Test - public void auth_authcallback_key_expire() { - try { - /* create Ably instance with key */ - ClientOptions opts = createOptions(testVars.keys[0].keyStr); - opts.clientId = "testClientId"; - opts.useTokenAuth = true; - opts.defaultTokenParams.ttl = 5000L; - AblyRest ably = new AblyRest(opts); - - /* make a request that relies on the token */ - System.out.println("auth_authcallback_key_expire: making first request"); - try { - ably.stats(new Param[] { new Param("by", "hour"), new Param("limit", "1") }); - } catch (AblyException e) { - e.printStackTrace(); - fail("auth_authURL_tokenrequest: Unexpected exception requesting token"); - } - String firstToken = ably.auth.getTokenDetails().token; - - /* wait until token expires */ - System.out.println("auth_authcallback_key_expire: sleeping"); - try { - Thread.sleep(6000L); - } catch(InterruptedException ie) {} - - /* make a request that relies on the token */ - System.out.println("auth_authcallback_key_expire: making second request"); - try { - ably.stats(new Param[] { new Param("by", "hour"), new Param("limit", "1") }); - } catch (AblyException e) { - e.printStackTrace(); - fail("auth_authURL_tokenrequest: Unexpected exception requesting token"); - } - String secondToken = ably.auth.getTokenDetails().token; - - /* verify that the token was renewed */ - assertNotEquals("Verify token was renewed", firstToken, secondToken); - } catch (AblyException e) { - e.printStackTrace(); - fail("auth_authURL_tokenrequest: Unexpected exception instantiating library"); - } - } - - /** - * Verify authCallback called and handled when returning error - */ - @Test - public void auth_authcallback_err() { - try { - /* implement callback, using Ably instance with key */ - TokenCallback authCallback = new TokenCallback() { - @Override - public Object getTokenRequest(TokenParams params) throws AblyException { - throw AblyException.fromErrorInfo(new ErrorInfo("test exception", 404, 12345)); - } - }; - - /* create Ably instance without key */ - ClientOptions opts = createOptions(); - opts.authCallback = authCallback; - AblyRest ably = new AblyRest(opts); - - /* make a call to trigger token request */ - try { - ably.auth.requestToken(null, null); - fail("auth_authURL_err: Unexpected success requesting token"); - } catch (AblyException e) { - assertEquals("Expected error code", e.errorInfo.code, 80019); - assertEquals("Expected forwarded error code", ((AblyException)e.getCause()).errorInfo.code, 12345); - } - } catch (AblyException e) { - e.printStackTrace(); - fail("auth_authURL_token: Unexpected exception instantiating library"); - } - } - - /** - * Verify library throws an error on initialistion if no auth details are provided - * Spec: RSA14 - */ - @Test - public void authinit_no_auth() { - try { - ClientOptions opts = new ClientOptions(); - new AblyRest(opts); - fail("authinit_no_auth: Unexpected success instantiating library"); - } catch (AblyException e) { - assertEquals("Verify exception thrown initialising library", e.errorInfo.code, 40000); - } - } - - /** - * Verify preemptive auth occurs when an API call is made using basic auth - */ - @Test - public void auth_preemptive_auth_basic() { - try { - /* create Ably instance with key */ - DebugOptions opts = new DebugOptions(testVars.keys[0].keyStr); - fillInOptions(opts); - RawHttpTracker httpListener = new RawHttpTracker(); - opts.httpListener = httpListener; - AblyRest ably = new AblyRest(opts); - - /* make a request that relies on authentication */ - try { - ably.stats(new Param[] { new Param("by", "hour"), new Param("limit", "1") }); - } catch (AblyException e) { - e.printStackTrace(); - fail("auth_preemptive_auth_basic: Unexpected exception making API call"); - } - - /* verify that the request was sent once only with a basic auth header */ - assertEquals("Verify one request made", httpListener.size(), 1); - assertTrue("Verify request had auth header", httpListener.getFirstRequest().authHeader.startsWith("Basic")); - } catch (AblyException e) { - e.printStackTrace(); - fail("auth_preemptive_auth_basic: Unexpected exception instantiating library"); - } - } - - /** - * Verify preemptive auth occurs when an API call is on an Ably instanced initialised with a token - */ - @Test - public void auth_preemptive_auth_given_token() { - try { - ClientOptions optsForToken = createOptions(testVars.keys[0].keyStr); - optsForToken.clientId = "testClientId"; - AblyRest ablyForToken = new AblyRest(optsForToken); - TokenDetails tokenDetails = ablyForToken.auth.requestToken(null, null); - - /* create Ably instance with token */ - DebugOptions opts = new DebugOptions(tokenDetails.token); - fillInOptions(opts); - RawHttpTracker httpListener = new RawHttpTracker(); - opts.httpListener = httpListener; - AblyRest ably = new AblyRest(opts); - - /* make a request that relies on authentication */ - try { - ably.stats(new Param[] { new Param("by", "hour"), new Param("limit", "1") }); - } catch (AblyException e) { - e.printStackTrace(); - fail("auth_preemptive_auth_given_token: Unexpected exception making API call"); - } - - /* verify that the request was sent once only with a basic auth header */ - assertEquals("Verify one request made", httpListener.size(), 1); - assertTrue("Verify request had auth header", httpListener.getFirstRequest().authHeader.startsWith("Bearer")); - } catch (AblyException e) { - e.printStackTrace(); - fail("auth_preemptive_auth_given_token: Unexpected exception instantiating library"); - } - } - - /** - * Verify preemptive auth occurs when an API call is on an Ably instanced with a key but using token auth - */ - @Test - public void auth_preemptive_auth_created_token() { - try { - /* create Ably instance with key */ - DebugOptions opts = new DebugOptions(testVars.keys[0].keyStr); - fillInOptions(opts); - opts.clientId = "testClientId"; - opts.useTokenAuth = true; - RawHttpTracker httpListener = new RawHttpTracker(); - opts.httpListener = httpListener; - AblyRest ably = new AblyRest(opts); - - /* make a request that relies on authentication */ - try { - ably.stats(new Param[] { new Param("by", "hour"), new Param("limit", "1") }); - } catch (AblyException e) { - e.printStackTrace(); - fail("auth_preemptive_auth_basic: Unexpected exception making API call"); - } - - /* verify that there were two requests: one to get a token, and one to make the API call */ - assertEquals("Verify two requests made", httpListener.size(), 2); - assertTrue("Verify token request made", httpListener.getFirstRequest().url.getPath().endsWith("requestToken")); - assertTrue("Verify API request had auth header", httpListener.getLastRequest().authHeader.startsWith("Bearer")); - } catch (AblyException e) { - e.printStackTrace(); - fail("auth_preemptive_auth_basic: Unexpected exception instantiating library"); - } - } - - /** - * RSA7c: A clientId value of "*" provided in ClientOptions throws an exception - */ - @Test - public void auth_client_wildcard() { - ClientOptions opts; - try { - opts = createOptions(); - opts.clientId = "*"; - new AblyRest(opts); - } catch (AblyException e) { - assertEquals("Verify exception raised from disallowed wildcard clientId", e.errorInfo.code, 40000); - } - } - - /** - * RSA15a: Any clientId provided in ClientOptions must match any - * non wildcard ('*') clientId value in TokenDetails - * RSA15c: Following an auth request which uses a TokenDetails or TokenRequest - * object that contains an incompatible clientId, the library should ... result - * in an appropriate error response - */ - @Test - public void auth_client_match_token() { - TokenDetails tokenDetails = new TokenDetails() {{ - clientId = "token clientId"; - token = "not.really.a.token"; - }}; - ClientOptions opts; - try { - opts = createOptions(); - opts.tokenDetails = tokenDetails; - opts.clientId = "options clientId"; - new AblyRest(opts); - } catch (AblyException e) { - assertEquals("Verify exception raised from incompatible clientIds", e.errorInfo.code, 40101); - } - } - - /** - * RSA15a: Any clientId provided in ClientOptions must match any - * non wildcard ('*') clientId value in TokenDetails - * RSA15b: If the clientId from TokenDetails or connectionDetails contains - * only a wildcard string '*', then the client is permitted to be either - * unidentified or identified by providing - * a clientId when communicating with Ably - */ - @Test - public void auth_client_match_token_wildcard() { - TokenDetails tokenDetails = new TokenDetails() {{ - clientId = "*"; - token = "not.really.a.token"; - }}; - ClientOptions opts; - try { - opts = createOptions(); - opts.tokenDetails = tokenDetails; - opts.clientId = "options clientId"; - AblyRest ably = new AblyRest(opts); - assertEquals("Verify given clientId is compatible with wildcard token clientId", ably.auth.clientId, "options clientId"); - } catch (AblyException e) { - e.printStackTrace(); - fail("auth_authURL_token: Unexpected exception instantiating library"); - } - } - - /** - * RSA15b: If the clientId from TokenDetails or connectionDetails contains - * only a wildcard string '*', then the client is permitted to be either - * unidentified or identified by providing - * a clientId when communicating with Ably - */ - @Test - public void auth_client_null_match_token_wildcard() { - TokenDetails tokenDetails = new TokenDetails() {{ - clientId = "*"; - token = "not.really.a.token"; - }}; - ClientOptions opts; - try { - opts = createOptions(); - opts.tokenDetails = tokenDetails; - opts.clientId = null; - AblyRest ably = new AblyRest(opts); - assertEquals("Verify given clientId is compatible with wildcard token clientId", ably.auth.clientId, "*"); - } catch (AblyException e) { - e.printStackTrace(); - fail("auth_authURL_token: Unexpected exception instantiating library"); - } - } - - /** - * Verify token details has null client id after authenticating with null client id, - * the message gets published, and published message also does not contain a client id.
- *
- * Spec: RSA8f1 - */ - @Test - public void auth_clientid_null_success() { - try { - /* implement callback, using Ably instance with key */ - TokenCallback authCallback = new TokenCallback() { - private AblyRest ably = new AblyRest(createOptions(testVars.keys[0].keyStr)); - @Override - public Object getTokenRequest(TokenParams params) throws AblyException { - return ably.auth.requestToken(params, null); - } - }; - - /* create Ably instance without clientId */ - ClientOptions options = createOptions(); - options.clientId = null; - options.authCallback = authCallback; - AblyRest ably = new AblyRest(options); - - /* Fetch token */ - TokenDetails tokenDetails = ably.auth.requestToken(null, null); - assertEquals("Auth#clientId is expected to be null", null, ably.auth.clientId); - assertEquals("TokenDetails#clientId is expected to be null", null, tokenDetails.clientId); - - /* Publish message */ - String messageName = "clientless"; - String messageData = String.valueOf(System.currentTimeMillis()); - - Channel channel = ably.channels.get("test"); - channel.publish(messageName, messageData); - - /* Fetch published message */ - PaginatedResult result = channel.history(null); - Message[] messages = result.items(); - Message publishedMessage = null; - Message message; - - for(int i = 0; i < messages.length; i++) { - message = messages[i]; - - if(messageName.equals(message.name) && - messageData.equals(message.data)) { - publishedMessage = message; - break; - } - } - - assertNotNull("Recently published message expected to be accessible", publishedMessage); - assertEquals("Message#clientId is expected to be null", null, publishedMessage.clientId); - } catch (Exception e) { - e.printStackTrace(); - fail("auth_clientid_null_success: Unexpected exception"); - } - } - - /** - * Verify message gets rejected when there is a client id mismatch - * between token details and message
- *
- * Spec: RSA8f2 - */ - @Test - public void auth_clientid_null_mismatch() throws AblyException { - AblyRest ably = null; - - try { - /* implement callback, using Ably instance with key */ - TokenCallback authCallback = new TokenCallback() { - private AblyRest ably = new AblyRest(createOptions(testVars.keys[0].keyStr)); - @Override - public Object getTokenRequest(TokenParams params) throws AblyException { - return ably.auth.requestToken(params, null); - } - }; - - /* create Ably instance */ - ClientOptions options = createOptions(); - options.authCallback = authCallback; - ably = new AblyRest(options); - - /* Create token with null clientId */ - TokenParams tokenParams = new TokenParams() {{ clientId = null; }}; - TokenDetails tokenDetails = ably.auth.requestToken(tokenParams, null); - assertEquals("TokenDetails#clientId is expected to be null", null, tokenDetails.clientId); - assertEquals("Auth#clientId is expected to be null", null, ably.auth.clientId); - } catch (Exception e) { - e.printStackTrace(); - fail("auth_clientid_null_mismatch: Unexpected exception"); - } - - try { - /* Publish a message with mismatching client id */ - Message message = new Message( - "I", /* name */ - "will", /* data */ - "fail" /* mismatching client id */ - ); - Channel channel = ably.channels.get("test"); - channel.publish(new Message[]{ message }); - } catch(AblyException e) { - assertEquals("Verify exception is raised with expected error code", e.errorInfo.code, 40012); - } - } - - /** - * Verify message with wildcard `*` client id gets published, - * and contains null client id.
- *
- * Spec: RSA8f3, RSA7b4 - */ - @Test - public void auth_clientid_null_wildcard () { - try { - /* implement callback, using Ably instance with key */ - TokenCallback authCallback = new TokenCallback() { - private AblyRest ably = new AblyRest(createOptions(testVars.keys[0].keyStr)); - @Override - public Object getTokenRequest(TokenParams params) throws AblyException { - params.clientId = "*"; - return ably.auth.requestToken(params, null); - } - }; - - /* create Ably instance with wildcard clientId */ - ClientOptions options = createOptions(); - options.authCallback = authCallback; - AblyRest ably = new AblyRest(options); - - /* Fetch token */ - TokenDetails tokenDetails = ably.auth.authorize(null, null); - assertEquals("TokenDetails#clientId is expected to be wildcard '*'", "*", tokenDetails.clientId); - assertEquals("Auth#clientId is expected to be wildcard '*'", "*", ably.auth.clientId); - - /* Publish message */ - String messageName = "wildcard"; - String messageData = String.valueOf(System.currentTimeMillis()); - - Channel channel = ably.channels.get("test"); - channel.publish(messageName, messageData); - - /* Fetch published message */ - PaginatedResult result = channel.history(null); - Message[] messages = result.items(); - Message publishedMessage = null; - Message message; - - for(int i = 0; i < messages.length; i++) { - message = messages[i]; - - if(messageName.equals(message.name) && - messageData.equals(message.data)) { - publishedMessage = message; - break; - } - } - - assertNotNull("Recently published message expected to be accessible", publishedMessage); - assertEquals("Message#clientId is expected to be null", null, publishedMessage.clientId); - } catch (Exception e) { - e.printStackTrace(); - fail("auth_clientid_null_wildcard: Unexpected exception"); - } - } - - /** - * Verify message with explicit client id successfully gets published, - * when authenticated with wildcard '*' client id
- *
- * Spec: RSA8f4 - */ - @Test - public void auth_clientid_explicit_wildcard () { - try { - /* implement callback, using Ably instance with key */ - TokenCallback authCallback = new TokenCallback() { - private AblyRest ably = new AblyRest(createOptions(testVars.keys[0].keyStr)); - @Override - public Object getTokenRequest(TokenParams params) throws AblyException { - params.clientId = "*"; - return ably.auth.requestToken(params, null); - } - }; - - /* create Ably instance with wildcard clientId */ - ClientOptions options = createOptions(); - options.authCallback = authCallback; - AblyRest ably = new AblyRest(options); - - /* Fetch token */ - TokenDetails tokenDetails = ably.auth.authorize(null, null); - assertEquals("TokenDetails#clientId is expected to be wildcard '*'", "*", tokenDetails.clientId); - assertEquals("Auth#clientId is expected to be wildcard '*'", "*", ably.auth.clientId); - - /* Publish a message */ - Message messagePublishee = new Message( - "wildcard", /* name */ - String.valueOf(System.currentTimeMillis()), /* data */ - "brian that is called brian" /* clientId */ - ); - - Channel channel = ably.channels.get("test"); - channel.publish(new Message[] { messagePublishee }); - - /* Fetch published message */ - PaginatedResult result = channel.history(null); - Message[] messages = result.items(); - Message messagePublished = null; - Message message; - - for(int i = 0; i < messages.length; i++) { - message = messages[i]; - - if(messagePublishee.name.equals(message.name) && - messagePublishee.data.equals(message.data)) { - messagePublished = message; - break; - } - } - - assertNotNull("Recently published message expected to be accessible", messagePublished); - assertEquals("Message#clientId is expected to be same with explicitly defined clientId", messagePublishee.clientId, messagePublished.clientId); - } catch (Exception e) { - e.printStackTrace(); - fail("auth_clientid_explicit_wildcard: Unexpected exception"); - } - } - - /** - * Verify message does not have explicit client id populated - * when library is identified - * Spec: RSA7a1,RSL1g1a - */ - @Test - public void auth_clientid_publish_implicit() { - try { - String clientId = "test clientId"; - ClientOptions optsForToken = createOptions(testVars.keys[0].keyStr); - optsForToken.clientId = clientId; - AblyRest ablyForToken = new AblyRest(optsForToken); - TokenDetails tokenDetails = ablyForToken.auth.requestToken(null, null); - - final Message[] messages = new Message[1]; - - /* create Ably instance with clientId */ - DebugOptions options = new DebugOptions(testVars.keys[0].keyStr) {{ - this.httpListener = new RawHttpListener() { - @Override - public HttpCore.Response onRawHttpRequest(String id, HttpURLConnection conn, String method, String authHeader, - Map> requestHeaders, HttpCore.RequestBody requestBody) { - try { - if(testParams.useBinaryProtocol) { - messages[0] = MessageSerializer.readMsgpack(requestBody.getEncoded())[0]; - } else { - messages[0] = MessageSerializer.readMessagesFromJson(requestBody.getEncoded())[0]; - } - } catch (AblyException e) { - e.printStackTrace(); - fail("auth_clientid_publish_implicit: Unexpected exception"); - } - return null; - } - - @Override - public void onRawHttpResponse(String id, String method, HttpCore.Response response) {} - - @Override - public void onRawHttpException(String id, String method, Throwable t) {} - }; - }}; - fillInOptions(options); - options.tokenDetails = tokenDetails; - AblyRest ably = new AblyRest(options); - - /* Publish a message */ - Message messagePublishee = new Message( - "I have clientId", /* name */ - String.valueOf(System.currentTimeMillis()) /* data */ - ); - - Channel channel = ably.channels.get("test_" + testParams.name); - channel.publish(new Message[] { messagePublishee }); - - /* Get sent message */ - Message messagePublished = messages[0]; - assertNull("Published message does not contain clientId", messagePublished.clientId); - } catch (Exception e) { - e.printStackTrace(); - fail("auth_clientid_publish_implicit: Unexpected exception"); - } - } - - /** - * Verify message does have explicit client id populated - * when library is initialised as wildcard - * Spec: RSA8f4 - */ - @Test - public void auth_clientid_publish_explicit_in_message() { - try { - final String messageClientId = "test clientId"; - ClientOptions optsForToken = createOptions(testVars.keys[0].keyStr); - AblyRest ablyForToken = new AblyRest(optsForToken); - TokenDetails tokenDetails = ablyForToken.auth.requestToken(new TokenParams() {{ - this.clientId = "*"; - }}, null); - - final Message[] messages = new Message[1]; - - /* create Ably instance with clientId */ - DebugOptions options = new DebugOptions(testVars.keys[0].keyStr) {{ - this.httpListener = new RawHttpListener() { - @Override - public HttpCore.Response onRawHttpRequest(String id, HttpURLConnection conn, String method, String authHeader, - Map> requestHeaders, HttpCore.RequestBody requestBody) { - try { - if(testParams.useBinaryProtocol) { - messages[0] = MessageSerializer.readMsgpack(requestBody.getEncoded())[0]; - } else { - messages[0] = MessageSerializer.readMessagesFromJson(requestBody.getEncoded())[0]; - } - } catch (AblyException e) { - e.printStackTrace(); - fail("auth_clientid_publish_implicit: Unexpected exception"); - } - return null; - } - - @Override - public void onRawHttpResponse(String id, String method, HttpCore.Response response) {} - - @Override - public void onRawHttpException(String id, String method, Throwable t) {} - }; - }}; - fillInOptions(options); - options.tokenDetails = tokenDetails; - AblyRest ably = new AblyRest(options); - - /* Publish a message */ - Message messagePublishee = new Message( - "I have clientId", /* name */ - String.valueOf(System.currentTimeMillis()), /* data */ - messageClientId /* clientId */ - ); - - Channel channel = ably.channels.get("test_" + testParams.name); - channel.publish(new Message[] { messagePublishee }); - - /* Get sent message */ - Message messagePublished = messages[0]; - assertEquals("Published message contains clientId", messagePublished.clientId, messageClientId); - } catch (Exception e) { - e.printStackTrace(); - fail("auth_clientid_publish_explicit_in_message: Unexpected exception"); - } - } - - /** - * Verify message with wildcard `*` client id gets published, - * and contains null client id.
- *
- * Spec: RTL6e1 - */ - @Test - public void auth_clientid_basic_null_wildcard() { - try { - /* create Ably instance with basic auth and no clientId */ - ClientOptions options = createOptions(testVars.keys[0].keyStr); - AblyRest ably = new AblyRest(options); - - /* Publish message */ - String messageName = "wildcard"; - String messageData = String.valueOf(System.currentTimeMillis()); - String clientId = "message clientId"; - - Channel channel = ably.channels.get("auth_clientid_basic_null_wildcard_" + testParams.name); - Message message = new Message(messageName, messageData); - message.clientId = clientId; - channel.publish(new Message[] { message }); - - /* Fetch published message */ - PaginatedResult result = channel.history(null); - Message[] messages = result.items(); - Message publishedMessage = null; - - for(int i = 0; i < messages.length; i++) { - Message msg = messages[i]; - - if(messageName.equals(msg.name) && - messageData.equals(msg.data)) { - publishedMessage = message; - break; - } - } - - assertNotNull("Recently published message expected to be accessible", publishedMessage); - assertEquals("Message#clientId is expected to be set", clientId, publishedMessage.clientId); - } catch (Exception e) { - e.printStackTrace(); - fail("auth_clientid_basic_null_wildcard: Unexpected exception"); - } - } - - /** - * Verify client id in token is populated from defaultTokenParams - * when library is initialised without explicit clientId - * Spec: RSA7a4, RSA7d - */ - @Test - public void auth_clientid_in_defaultparams() { - try { - final String defaultClientId = "default clientId"; - - /* create Ably instance with defaultTokenParams */ - ClientOptions options = createOptions(testVars.keys[0].keyStr); - options.useTokenAuth = true; - options.defaultTokenParams = new TokenParams() {{ this.clientId = defaultClientId; }}; - AblyRest ably = new AblyRest(options); - - /* get a token with these default params */ - ably.auth.authorize(null, null); - - /* verify that clientId is set */ - assertEquals("Verify expected clientId is set", ably.auth.clientId, defaultClientId); - - } catch (Exception e) { - e.printStackTrace(); - fail("auth_clientid_in_defaultparams: Unexpected exception"); - } - } - - /** - * Verify client id in token populated from ClientOptions - * overriding any clientId in defaultTokenParams - * Spec: RSA7a4, RSA7d - */ - @Test - public void auth_clientid_in_opts_overrides_defaultparams() { - try { - final String defaultClientId = "default clientId"; - final String clientId = "options clientId"; - - /* create Ably instance with defaultTokenParams */ - ClientOptions options = createOptions(testVars.keys[0].keyStr); - options.useTokenAuth = true; - options.clientId = clientId; - options.defaultTokenParams = new TokenParams() {{ this.clientId = defaultClientId; }}; - AblyRest ably = new AblyRest(options); - - /* get a token with these default params */ - ably.auth.authorize(null, null); - - /* verify that clientId is set */ - assertEquals("Verify expected clientId is set", ably.auth.clientId, clientId); - - } catch (Exception e) { - e.printStackTrace(); - fail("auth_clientid_in_defaultparams: Unexpected exception"); - } - } - - /** - * Verify token auth used when useTokenAuth=true - */ - @Test - public void auth_useTokenAuth() { - try { - ClientOptions opts = createOptions(testVars.keys[0].keyStr); - opts.useTokenAuth = true; - AblyRest ably = new AblyRest(opts); - /* verify that we don't have a token yet. */ - assertTrue("Not expecting a token yet", ably.auth.getTokenDetails() == null); - /* make a request that relies on the token */ - try { - ably.stats(new Param[] { new Param("by", "hour"), new Param("limit", "1") }); - } catch (AblyException e) { - e.printStackTrace(); - fail("Unexpected exception requesting token"); - } - /* verify that we have a token. */ - assertTrue("Expected to use token auth", ably.auth.getTokenDetails() != null); - System.out.println("Token is " + ably.auth.getTokenDetails().token); - } catch (AblyException e) { - e.printStackTrace(); - fail("Unexpected exception instantiating library"); - } - } - - /** - * Test behaviour of queryTime parameter in ClientOpts. Time is requested from the Ably server only once, - * cached value should be used afterwards - * Spec: RSA9a - */ - @Test - public void auth_testQueryTime() { - try { - nanoHTTPD.clearRequestHistory(); - ClientOptions opts = new ClientOptions(testVars.keys[0].keyStr); - opts.tls = false; - opts.restHost = "localhost"; - opts.port = nanoHTTPD.getListeningPort(); - opts.queryTime = true; - - AblyRest ably1 = new AblyRest(opts); - @SuppressWarnings("unused") - Auth.TokenRequest tr1 = ably1.auth.createTokenRequest(null, null); - - AblyRest ably2 = new AblyRest(opts); - @SuppressWarnings("unused") - Auth.TokenRequest tr2 = ably2.auth.createTokenRequest(null, null); - - List requestHistory = nanoHTTPD.getRequestHistory(); - /* search for all /time request in the list */ - int timeRequestCount = 0; - for (String request: requestHistory) - if (request.contains("/time")) - timeRequestCount++; - - assertEquals("Verify number of time requests", timeRequestCount, 2); - } catch (AblyException e) { - e.printStackTrace(); - fail("Unexpected exception"); - } - } - - /** - * Verify JSON serialisation and deserialisation of basic types - * Spec: TE6, TD7 - */ - @Test - public void auth_json_interop() { - /* create a token request */ - AblyRest ably; - TokenRequest tokenRequest; - try { - ClientOptions opts = createOptions(testVars.keys[0].keyStr); - opts.clientId = "Test client id"; - ably = new AblyRest(opts); - tokenRequest = ably.auth.createTokenRequest(new TokenParams() {{ - ttl = 10000; - capability = "{\"*\": [\"*\"]}"; - }}, null); - String serialisedTokenRequest = tokenRequest.asJson(); - TokenRequest deserialisedTokenRequest = TokenRequest.fromJson(serialisedTokenRequest); - assertEquals("Verify token request is serialised and deserialised successfully", tokenRequest, deserialisedTokenRequest); - } catch (AblyException e) { - e.printStackTrace(); - fail("Unexpected exception"); - return; - } - /* create a token details */ - try { - TokenDetails tokenDetails = ably.auth.requestToken(tokenRequest, null); - String serialisedTokenDetails = tokenDetails.asJson(); - TokenDetails deserialisedTokenDetails = TokenDetails.fromJson(serialisedTokenDetails); - assertEquals("Verify token details is serialised and deserialised successfully", tokenDetails, deserialisedTokenDetails); - } catch (AblyException e) { - e.printStackTrace(); - fail("Unexpected exception"); - } - } - - /** - * Verify TokenRequest as JSON TTL and capability are omitted if not explicitly set to nonzero. - * Spec: RSA5, RSA6 - */ - @Test - public void auth_token_request_json_omitted_defaults() { - AblyRest ably; - TokenRequest tokenRequest; - try { - for (final String cap : new String[] {null, ""}) { - ClientOptions opts = createOptions(testVars.keys[0].keyStr); - opts.clientId = "Test client id"; - ably = new AblyRest(opts); - tokenRequest = ably.auth.createTokenRequest(new TokenParams() {{ - capability = cap; - }}, null); - String serialisedTokenRequest = tokenRequest.asJson(); - assertTrue("Verify token request has no ttl", !serialisedTokenRequest.contains("ttl")); - assertTrue("Verify token request has no capability", !serialisedTokenRequest.contains("capability")); - } - } catch (AblyException e) { - e.printStackTrace(); - fail("Unexpected exception"); - } - } - - /** - * Verify that renewing the token when useTokenAuth is true doesn't use the old (expired) token. - */ - @Test - public void auth_renew_token_bearer_auth() { - try { - ClientOptions opts = createOptions(testVars.keys[0].keyStr); - opts.useTokenAuth = true; - opts.defaultTokenParams = new TokenParams() {{ - ttl = 500; - }}; - AblyRest ably = new AblyRest(opts); - - // Any request will issue a new token with the defaultTokenParams and use it. - - ably.channels.get("test").history(null); - TokenDetails oldToken = ably.auth.getTokenDetails(); - - // Sleep until old token expires, then ensure it did. - - Thread.sleep(1000); - ClientOptions optsWithOldToken = createOptions(); - optsWithOldToken.tokenDetails = oldToken; - AblyRest ablyWithOldToken = new AblyRest(optsWithOldToken); - try { - ablyWithOldToken.channels.get("test").history(null); - fail("expected old token to be expired already"); - } catch(AblyException e) {} - - // The library should now renew the token using the key. - - ably.channels.get("test").history(null); - TokenDetails newToken = ably.auth.getTokenDetails(); - - assertNotEquals(oldToken.token, newToken.token); - } catch (Exception e) { - e.printStackTrace(); - fail("Unexpected exception"); - } - } - - /** - * Verify that a local token validity check is made if queryTime == true - * and local time is in sync with server - * Spec: RSA4b1 - */ - @Test - public void auth_local_token_expiry_check_sync() { - try { - /* get a TokenDetails and allow to expire */ - final String testKey = testVars.keys[0].keyStr; - ClientOptions optsForToken = createOptions(testKey); - optsForToken.queryTime = true; - AblyRest ablyForToken = new AblyRest(optsForToken); - - TokenDetails tokenDetails = ablyForToken.auth.requestToken(new TokenParams(){{ ttl = 100L; }}, null); - - /* create Ably instance with token details */ - DebugOptions opts = new DebugOptions(); - fillInOptions(opts); - opts.queryTime = true; - opts.tokenDetails = tokenDetails; - RawHttpTracker httpListener = new RawHttpTracker(); - opts.httpListener = httpListener; - AblyRest ably = new AblyRest(opts); - - /* sync this library instance to server by creating a token request */ - ably.auth.createTokenRequest(null, new Auth.AuthOptions() {{ key = testKey; queryTime = true; }}); - - /* wait for the token to expire */ - try { Thread.sleep(200L); } catch(InterruptedException ie) {} - - /* make a request that relies on authentication */ - try { - ably.stats(new Param[] { new Param("by", "hour"), new Param("limit", "1") }); - fail("auth_local_token_expiry_check_sync: API call unexpectedly succeeded"); - return; - } catch (AblyException e) { - assertEquals("Verify that API request failed with credentials error", e.errorInfo.code, 40106); - for(Helpers.RawHttpRequest req : httpListener.values()) { - assertFalse("Verify no API request attempted", req.url.getPath().contains("stats")); - } - } - } catch (AblyException e) { - e.printStackTrace(); - fail("auth_local_token_expiry_check_sync: Unexpected exception instantiating library"); - } - } - - /** - * Verify that a local token validity check is not made if queryTime == false - * and local time is not in sync with server - * Spec: RSA4b1 - */ - @Test - public void auth_local_token_expiry_check_nosync() { - try { - /* get a TokenDetails and allow to expire */ - final String testKey = testVars.keys[0].keyStr; - ClientOptions optsForToken = createOptions(testKey); - optsForToken.queryTime = true; - AblyRest ablyForToken = new AblyRest(optsForToken); - - TokenDetails tokenDetails = ablyForToken.auth.requestToken(new TokenParams(){{ ttl = 100L; }}, null); - - /* create Ably instance with token details */ - DebugOptions opts = new DebugOptions(); - fillInOptions(opts); - opts.queryTime = false; - opts.tokenDetails = tokenDetails; - RawHttpTracker httpListener = new RawHttpTracker(); - opts.httpListener = httpListener; - AblyRest ably = new AblyRest(opts); - - /* wait for the token to expire */ - try { Thread.sleep(200L); } catch(InterruptedException ie) {} - - /* make a request that relies on authentication */ - try { - ably.stats(new Param[] { new Param("by", "hour"), new Param("limit", "1") }); - fail("auth_local_token_expiry_check_nosync: API call unexpectedly succeeded"); - return; - } catch (AblyException e) { - assertEquals("Verify API request attempted", httpListener.size(), 1); - assertEquals("Verify API request failed with token expiry error", httpListener.getFirstRequest().response.headers.get("x-ably-errorcode").get(0), "40142"); - assertEquals("Verify that API request failed with credentials error", e.errorInfo.code, 40106); - } - } catch (AblyException e) { - e.printStackTrace(); - fail("auth_local_token_expiry_check_nosync: Unexpected exception instantiating library"); - } - } - - /** - * Verify that if a local token validity check suppressed because queryTime == false - * this does not prevent token renewal by an auth callback when a request fails - * Spec: RSA4b1 - */ - @Test - public void auth_local_token_expiry_check_nosync_retried() { - try { - /* get a TokenDetails and allow to expire */ - final String testKey = testVars.keys[0].keyStr; - ClientOptions optsForToken = createOptions(testKey); - optsForToken.queryTime = false; - final AblyRest ablyForToken = new AblyRest(optsForToken); - - TokenDetails tokenDetails = ablyForToken.auth.requestToken(new TokenParams(){{ ttl = 100L; }}, null); - - /* create Ably instance with token details */ - DebugOptions opts = new DebugOptions(); - fillInOptions(opts); - opts.queryTime = false; - opts.tokenDetails = tokenDetails; - opts.authCallback = new TokenCallback() { - @Override - public Object getTokenRequest(TokenParams params) throws AblyException { - return ablyForToken.auth.createTokenRequest(params, null); - } - }; - - RawHttpTracker httpListener = new RawHttpTracker(); - opts.httpListener = httpListener; - AblyRest ably = new AblyRest(opts); - - /* wait for the token to expire */ - try { Thread.sleep(200L); } catch(InterruptedException ie) {} - - /* make a request that relies on authentication */ - try { - ably.stats(new Param[] { new Param("by", "hour"), new Param("limit", "1") }); - } catch (AblyException e) { - fail("auth_local_token_expiry_check_nosync: API call unexpectedly failed"); - return; - } - assertEquals("Verify API request attempted", httpListener.size(), 3); - for(Helpers.RawHttpRequest x : httpListener.values()) { - System.out.println(x.url.toString()); - } - assertEquals("Verify API request failed with token expiry error", httpListener.getFirstRequest().response.headers.get("x-ably-errorcode").get(0), "40142"); - } catch (AblyException e) { - e.printStackTrace(); - fail("auth_local_token_expiry_check_nosync: Unexpected exception instantiating library"); - } - } - - private static TokenServer tokenServer; - private static SessionHandlerNanoHTTPD nanoHTTPD; - - private static class SessionHandlerNanoHTTPD extends RouterNanoHTTPD { - private final ArrayList requestHistory = new ArrayList<>(); - - public SessionHandlerNanoHTTPD(int port) { - super(port); - } - - @Override - public Response serve(IHTTPSession session) { - synchronized (requestHistory) { - requestHistory.add(session.getUri()); - } - /* the only request supported here is /time */ - return newFixedLengthResponse(String.format(Locale.US, "[%d]", System.currentTimeMillis())); - } - - public void clearRequestHistory() { requestHistory.clear(); } - - public List getRequestHistory() { return requestHistory; } - } + @Rule + public Timeout testTimeout = Timeout.seconds(40); + + @Rule + public ExpectedException thrown = ExpectedException.none(); + + /** + * Init token server + */ + @BeforeClass + public static void auth_start_tokenserver() { + try { + ClientOptions opts = testVars.createOptions(testVars.keys[0].keyStr); + AblyRest ably = new AblyRest(opts); + tokenServer = new TokenServer(ably, 8982); + tokenServer.start(); + + nanoHTTPD = new SessionHandlerNanoHTTPD(27335); + nanoHTTPD.start(NanoHTTPD.SOCKET_READ_TIMEOUT, true); + + while (!nanoHTTPD.wasStarted()) { + try { + Thread.sleep(100); + } catch (InterruptedException e) { + e.printStackTrace(); + } + } + } catch (IOException e) { + e.printStackTrace(); + fail("auth_start_tokenserver: Unexpected exception starting server"); + } catch (AblyException e) { + e.printStackTrace(); + fail("auth_start_tokenserver: Unexpected exception starting server"); + } + } + + /** + * Kill token server + */ + @AfterClass + public static void auth_stop_tokenserver() { + if(tokenServer != null) + tokenServer.stop(); + if (nanoHTTPD != null) + nanoHTTPD.stop(); + } + + /** + * Init library with a key only + */ + @Test + public void authinit0() { + try { + AblyRest ably = new AblyRest(testVars.keys[0].keyStr); + assertEquals("Unexpected Auth method mismatch", ably.auth.getAuthMethod(), AuthMethod.basic); + assertEquals("Unexpected clientId mismatch", ably.auth.clientId, "*"); + } catch (AblyException e) { + e.printStackTrace(); + fail("authinit0: Unexpected exception instantiating library"); + } + } + + /** + * Init library with a key and tls=false results in an error + * Spec: RSC18 + */ + @Test + public void auth_basic_nontls() { + try { + ClientOptions opts = createOptions(testVars.keys[0].keyStr); + opts.tls = false; + AblyRest ably = new AblyRest(opts); + ably.stats(null); + fail("Unexpected success calling with Basic auth over httpCore"); + } catch (AblyException e) { + e.printStackTrace(); + assertEquals("Verify expected error code", e.errorInfo.statusCode, 401); + } + } + + /** + * Init library with useTokenAuth set + */ + @Test + public void authinit0_useTokenAuth() { + try { + ClientOptions opts = createOptions(testVars.keys[0].keyStr); + opts.useTokenAuth = true; + AblyRest ably = new AblyRest(opts); + assertEquals("Unexpected Auth method mismatch", ably.auth.getAuthMethod(), AuthMethod.token); + /* Spec: RSA12a */ + assertEquals("Unexpected clientId mismatch", ably.auth.clientId, null); + } catch (AblyException e) { + e.printStackTrace(); + fail("authinit0point5: Unexpected exception instantiating library"); + } + } + + /** + * Init library with a token only + */ + @Test + public void authinit1() { + try { + ClientOptions opts = new ClientOptions(); + opts.token = "this_is_not_really_a_token"; + AblyRest ably = new AblyRest(opts); + assertEquals("Unexpected Auth method mismatch", ably.auth.getAuthMethod(), AuthMethod.token); + /* Spec: RSA12a */ + assertEquals("Unexpected clientId mismatch", ably.auth.clientId, null); + } catch (AblyException e) { + e.printStackTrace(); + fail("authinit1: Unexpected exception instantiating library"); + } + } + + /** + * Init library with a token callback + */ + private boolean authinit2_cbCalled; + @Test + public void authinit2() { + try { + ClientOptions opts = createOptions(); + opts.restHost = testVars.restHost; + opts.environment = testVars.environment; + opts.port = testVars.port; + opts.tlsPort = testVars.tlsPort; + opts.tls = testVars.tls; + opts.authCallback = new TokenCallback() { + @Override + public String getTokenRequest(TokenParams params) throws AblyException { + authinit2_cbCalled = true; + return "this_is_not_really_a_token_request"; + }}; + AblyRest ably = new AblyRest(opts); + /* make a call to trigger token request */ + try { + ably.stats(null); + } catch(Throwable t) {} + assertEquals("Unexpected Auth method mismatch", ably.auth.getAuthMethod(), AuthMethod.token); + /* Spec: RSA12a */ + assertEquals("Unexpected clientId mismatch", ably.auth.clientId, null); + assertTrue("Token callback not called", authinit2_cbCalled); + } catch (AblyException e) { + e.printStackTrace(); + fail("authinit2: Unexpected exception instantiating library"); + } + } + + /** + * Init library with a key and clientId; expect token auth to be chosen + * Spec: RSA4, RSC17, RSA7b1 + */ + @Test + public void authinit_clientId_implies_token() { + try { + ClientOptions opts = createOptions(testVars.keys[0].keyStr); + opts.clientId = "testClientId"; + AblyRest ably = new AblyRest(opts); + assertEquals("Unexpected Auth method mismatch", ably.auth.getAuthMethod(), AuthMethod.token); + assertEquals("Unexpected clientId mismatch", ably.auth.clientId, "testClientId"); + } catch (AblyException e) { + e.printStackTrace(); + fail("authinit_clientId_implies_token: Unexpected exception instantiating library"); + } + } + + /** + * Init library with a key and token; verify Auth.clientId is null before + * authorization and set following auth + * Spec: RSA12b, RSA7b2 + */ + @Test + public void auth_clientid_null_before_auth() { + try { + final String defaultClientId = "default clientId"; + ClientOptions opts = createOptions(testVars.keys[0].keyStr); + opts.useTokenAuth = true; + opts.defaultTokenParams = new TokenParams() {{ this.clientId = defaultClientId; }}; + AblyRest ably = new AblyRest(opts); + assertEquals("Unexpected Auth method mismatch", ably.auth.getAuthMethod(), AuthMethod.token); + assertEquals("Unexpected clientId mismatch", ably.auth.clientId, null); + ably.auth.authorize(null, null); + assertEquals("Unexpected clientId mismatch", ably.auth.clientId, defaultClientId); + } catch (AblyException e) { + e.printStackTrace(); + fail("authinit_token_implies_token: Unexpected exception instantiating library"); + } + } + + /** + * Init library with a key and token; expect token auth to be chosen + * Spec: RSA4 + */ + @Test + public void authinit_token_implies_token() { + try { + ClientOptions opts = createOptions(testVars.keys[0].keyStr); + opts.token = testVars.keys[0].keyStr; + AblyRest ably = new AblyRest(opts); + assertEquals("Unexpected Auth method mismatch", ably.auth.getAuthMethod(), AuthMethod.token); + assertEquals("Unexpected clientId mismatch", ably.auth.clientId, null); + } catch (AblyException e) { + e.printStackTrace(); + fail("authinit_token_implies_token: Unexpected exception instantiating library"); + } + } + + /** + * Init library with a key and tokenDetails; expect token auth to be chosen + * Spec: RSA4 + */ + @Test + public void authinit_tokendetails_implies_token() { + try { + ClientOptions opts = createOptions(testVars.keys[0].keyStr); + opts.tokenDetails = new TokenDetails() {{ token = testVars.keys[0].keyStr; }}; + AblyRest ably = new AblyRest(opts); + assertEquals("Unexpected Auth method mismatch", ably.auth.getAuthMethod(), AuthMethod.token); + assertEquals("Unexpected clientId mismatch", ably.auth.clientId, null); + } catch (AblyException e) { + e.printStackTrace(); + fail("authinit_token_implies_token: Unexpected exception instantiating library"); + } + } + + /** + * Init library with a key and authCallback; expect token auth to be chosen + * Spec: RSA4 + */ + @Test + public void authinit_authcallback_implies_token() { + try { + ClientOptions opts = createOptions(testVars.keys[0].keyStr); + opts.authCallback = new TokenCallback() { + @Override + public Object getTokenRequest(TokenParams params) throws AblyException { + return null; + }}; + AblyRest ably = new AblyRest(opts); + assertEquals("Unexpected Auth method mismatch", ably.auth.getAuthMethod(), AuthMethod.token); + assertEquals("Unexpected clientId mismatch", ably.auth.clientId, null); + } catch (AblyException e) { + e.printStackTrace(); + fail("authinit_token_implies_token: Unexpected exception instantiating library"); + } + } + + /** + * Init library with a key and authUrl; expect token auth to be chosen + * Spec: RSA4 + */ + @Test + public void authinit_authurl_implies_token() { + try { + ClientOptions opts = createOptions(testVars.keys[0].keyStr); + opts.authUrl = "http://auth.url"; + AblyRest ably = new AblyRest(opts); + assertEquals("Unexpected Auth method mismatch", ably.auth.getAuthMethod(), AuthMethod.token); + } catch (AblyException e) { + e.printStackTrace(); + fail("authinit_token_implies_token: Unexpected exception instantiating library"); + } + } + + /** + * Init library with a token + */ + @Test + public void authinit4() { + try { + ClientOptions optsForToken = createOptions(testVars.keys[0].keyStr); + AblyRest ablyForToken = new AblyRest(optsForToken); + TokenDetails tokenDetails = ablyForToken.auth.requestToken(null, null); + assertNotNull("Expected token value", tokenDetails.token); + ClientOptions opts = new ClientOptions(); + opts.token = tokenDetails.token; + opts.environment = testVars.environment; + AblyRest ably = new AblyRest(opts); + assertEquals("Unexpected Auth method mismatch", ably.auth.getAuthMethod(), AuthMethod.token); + assertEquals("Unexpected clientId mismatch", ably.auth.clientId, null); + } catch (AblyException e) { + e.printStackTrace(); + fail("authinit3: Unexpected exception instantiating library"); + } + } + + /** + * Verify authURL called and handled when returning token request + * Spec: RSA8c + */ + @Test + public void auth_authURL_tokenrequest() { + try { + ClientOptions opts = createOptions(); + opts.environment = testVars.environment; + opts.authUrl = "http://localhost:8982/get-token-request"; + AblyRest ably = new AblyRest(opts); + /* make a call to trigger token request */ + try { + TokenDetails tokenDetails = ably.auth.requestToken(null, null); + assertNotNull("Expected token value", tokenDetails.token); + } catch (AblyException e) { + e.printStackTrace(); + fail("auth_authURL_tokenrequest: Unexpected exception requesting token"); + } + } catch (AblyException e) { + e.printStackTrace(); + fail("auth_authURL_tokenrequest: Unexpected exception instantiating library"); + } + } + + /** + * Verify authURL called and handled when returning token request, use POST method + * Spec: RSA8c1b + */ + @Test + public void auth_authURL_tokenrequest_post() { + try { + ClientOptions opts = createOptions(); + opts.environment = testVars.environment; + opts.authUrl = "http://localhost:8982/post-token-request"; + opts.authMethod = HttpConstants.Methods.POST; + AblyRest ably = new AblyRest(opts); + /* make a call to trigger token request */ + try { + TokenDetails tokenDetails = ably.auth.requestToken(null, null); + assertNotNull("Expected token value", tokenDetails.token); + } catch (AblyException e) { + e.printStackTrace(); + fail("auth_authURL_tokenrequest: Unexpected exception requesting token"); + } + } catch (AblyException e) { + e.printStackTrace(); + fail("auth_authURL_tokenrequest: Unexpected exception instantiating library"); + } + } + + + /** + * Verify authURL called and handled when returning token + * Spec: RSA8c + */ + @Test + public void auth_authURL_token() { + try { + ClientOptions opts = createOptions(); + opts.environment = testVars.environment; + opts.authUrl = "http://localhost:8982/get-token"; + AblyRest ably = new AblyRest(opts); + /* make a call to trigger token request */ + try { + TokenDetails tokenDetails = ably.auth.requestToken(null, null); + assertNotNull("Expected token value", tokenDetails.token); + } catch (AblyException e) { + e.printStackTrace(); + fail("auth_authURL_token: Unexpected exception requesting token"); + } + } catch (AblyException e) { + e.printStackTrace(); + fail("auth_authURL_token: Unexpected exception instantiating library"); + } + } + + /** + * Verify authURL called and handled when returning error + * Spec: RSA8c + */ + @Test + public void auth_authURL_err() { + try { + ClientOptions opts = createOptions(); + opts.environment = testVars.environment; + opts.authUrl = "http://localhost:8982/404"; + AblyRest ably = new AblyRest(opts); + /* make a call to trigger token request */ + try { + ably.auth.requestToken(null, null); + fail("auth_authURL_err: Unexpected success requesting token"); + } catch (AblyException e) { + assertEquals("Expected error code", e.errorInfo.code, 80019); + assertEquals("Expected forwarded error code", ((AblyException)e.getCause()).errorInfo.statusCode, 404); + } + } catch (AblyException e) { + e.printStackTrace(); + fail("auth_authURL_token: Unexpected exception instantiating library"); + } + } + + /** + * Verify authURL called and handled when timing out + * Spec: RSA8c, RSA4c + */ + @Test + public void auth_authURL_timeout() { + try { + ClientOptions opts = createOptions(); + opts.environment = testVars.environment; + opts.authUrl = "http://localhost:8982/wait?delay=6000"; + opts.httpRequestTimeout = 5000; + AblyRest ably = new AblyRest(opts); + /* make a call to trigger token request */ + try { + ably.auth.requestToken(null, null); + fail("auth_authURL_err: Unexpected success requesting token"); + } catch (AblyException e) { + assertEquals("Expected error code", e.errorInfo.code, 80019); + assertEquals("Expected forwarded error code", ((AblyException)e.getCause()).errorInfo.statusCode, 500); + assertTrue("Expected forwarded forwarded exception", (e.getCause().getCause()) instanceof SocketTimeoutException); + } + } catch (AblyException e) { + e.printStackTrace(); + fail("auth_authURL_timeout: Unexpected exception instantiating library"); + } + } + + /** + * Verify authURL is passed specified params in a GET + * Spec: RSA8c1a + */ + @Test + public void auth_authURL_authParams_get() { + try { + DebugOptions opts = new DebugOptions(testVars.keys[0].keyStr); + fillInOptions(opts); + RawHttpTracker httpListener = new RawHttpTracker(); + opts.httpListener = httpListener; + + opts.authUrl = "http://localhost:8982/get-token-request"; + opts.authParams = new Param[]{new Param("test-param", "test-value")}; + AblyRest ably = new AblyRest(opts); + + /* make a call to trigger token request */ + ably.auth.requestToken(null, null); + + /* check request contained expected params */ + assertTrue("Verify expected params passed to authURL", httpListener.getFirstRequest().url.getQuery().contains("test-param=test-value")); + } catch (AblyException e) { + e.printStackTrace(); + fail("auth_authURL_authParams_get: Unexpected exception instantiating library"); + } + } + + /** + * Verify authURL is passed specified params in a POST body + * Spec: RSA8c1b + */ + @Test + public void auth_authURL_authParams_post() { + try { + DebugOptions opts = new DebugOptions(testVars.keys[0].keyStr); + fillInOptions(opts); + RawHttpTracker httpListener = new RawHttpTracker(); + opts.httpListener = httpListener; + + opts.authUrl = "http://localhost:8982/post-token-request"; + opts.authMethod = HttpConstants.Methods.POST; + opts.authParams = new Param[]{new Param("test-param", "test-value")}; + AblyRest ably = new AblyRest(opts); + + /* make a call to trigger token request */ + ably.auth.requestToken(null, null); + + /* check request contained expected params */ + byte[] requestBody = httpListener.getFirstRequest().requestBody.getEncoded(); + assertTrue("Verify expected params passed to authURL", (new String(requestBody, "UTF-8")).contains("test-param=test-value")); + } catch (AblyException e) { + e.printStackTrace(); + fail("auth_authURL_authParams_post: Unexpected exception instantiating library"); + } catch (UnsupportedEncodingException e) { + e.printStackTrace(); + fail("auth_authURL_authParams_post: Unexpected exception decoding request body"); + } + } + + /** + * Verify authURL is passed specified params in a GET + * Spec: RSA8c1c + */ + @Test + public void auth_authURL_urlParams_get() { + try { + DebugOptions opts = new DebugOptions(testVars.keys[0].keyStr); + fillInOptions(opts); + RawHttpTracker httpListener = new RawHttpTracker(); + opts.httpListener = httpListener; + + opts.authUrl = "http://localhost:8982/get-token-request?test-param=test-value"; + AblyRest ably = new AblyRest(opts); + + /* make a call to trigger token request */ + ably.auth.requestToken(null, null); + + /* check request contained expected params */ + assertTrue("Verify expected params passed to authURL", httpListener.getFirstRequest().url.getQuery().contains("test-param=test-value")); + } catch (AblyException e) { + e.printStackTrace(); + fail("auth_authURL_urlParams_get: Unexpected exception instantiating library"); + } + } + + /** + * Verify authURL is passed specified params in a POST + * Spec: RSA8c1c + */ + @Test + public void auth_authURL_urlParams_post() { + try { + DebugOptions opts = new DebugOptions(testVars.keys[0].keyStr); + fillInOptions(opts); + RawHttpTracker httpListener = new RawHttpTracker(); + opts.httpListener = httpListener; + + opts.authUrl = "http://localhost:8982/post-token-request?test-param=test-value"; + opts.authMethod = HttpConstants.Methods.POST; + AblyRest ably = new AblyRest(opts); + + /* make a call to trigger token request */ + ably.auth.requestToken(null, null); + + /* check request contained expected params */ + assertTrue("Verify expected params passed to authURL", httpListener.getFirstRequest().url.getQuery().contains("test-param=test-value")); + } catch (AblyException e) { + e.printStackTrace(); + fail("auth_authURL_urlParams_post: Unexpected exception instantiating library"); + } + } + + /** + * Verify authURL is passed specified params in a GET, with the specified precedence + * Spec: RSA8c1c + */ + @Test + public void auth_authURL_urlParams_get_conflict() { + try { + DebugOptions opts = new DebugOptions(testVars.keys[0].keyStr); + fillInOptions(opts); + RawHttpTracker httpListener = new RawHttpTracker(); + opts.httpListener = httpListener; + + opts.authUrl = "http://localhost:8982/get-token-request?test-param=test-value-urlParam"; + opts.authParams = new Param[]{new Param("test-param", "test-value-authParam")}; + AblyRest ably = new AblyRest(opts); + + /* make a call to trigger token request */ + ably.auth.requestToken(null, null); + + /* check request contained expected params */ + assertTrue("Verify expected params passed to authURL", httpListener.getFirstRequest().url.getQuery().contains("test-param=test-value-authParam")); + } catch (AblyException e) { + e.printStackTrace(); + fail("auth_authURL_urlParams_get: Unexpected exception instantiating library"); + } + } + + /** + * Verify tokenParams take precedence over authParams in authURL request + * Spec: RSA8c2 + */ + @Test + public void auth_authURL_authParams_get_conflict() { + try { + DebugOptions opts = new DebugOptions(testVars.keys[0].keyStr); + fillInOptions(opts); + RawHttpTracker httpListener = new RawHttpTracker(); + opts.httpListener = httpListener; + + opts.authUrl = "http://localhost:8982/get-token-request"; + opts.authParams = new Param[]{new Param("ttl", "500")}; + AblyRest ably = new AblyRest(opts); + + /* make a call to trigger token request */ + ably.auth.requestToken(new TokenParams() {{ + ttl = 300; + }}, null); + + /* check request contained expected params */ + assertTrue("Verify expected params passed to authURL", httpListener.getFirstRequest().url.getQuery().contains("ttl=300")); + } catch (AblyException e) { + e.printStackTrace(); + fail("auth_authURL_urlParams_get: Unexpected exception instantiating library"); + } + } + + /** + * Verify authURL is passed specified headers in a GET + * Spec: RSA8c3 + */ + @Test + public void auth_authURL_authHeaders_get() { + try { + DebugOptions opts = new DebugOptions(testVars.keys[0].keyStr); + fillInOptions(opts); + RawHttpTracker httpListener = new RawHttpTracker(); + opts.httpListener = httpListener; + + opts.authUrl = "http://localhost:8982/get-token-request"; + opts.authHeaders = new Param[]{new Param("test-header", "test-value")}; + AblyRest ably = new AblyRest(opts); + + /* make a call to trigger token request */ + ably.auth.requestToken(null, null); + + /* check request contained expected params */ + List headers = httpListener.getFirstRequest().requestHeaders.get("test-header"); + assertTrue("Verify expected headers passed to authURL", headers.contains("test-value")); + } catch (AblyException e) { + e.printStackTrace(); + fail("auth_authURL_authHeaders_get: Unexpected exception instantiating library"); + } + } + + /** + * Verify authURL is passed specified headers in a POST + * Spec: RSA8c3 + */ + @Test + public void auth_authURL_authHeaders_post() { + try { + DebugOptions opts = new DebugOptions(testVars.keys[0].keyStr); + fillInOptions(opts); + RawHttpTracker httpListener = new RawHttpTracker(); + opts.httpListener = httpListener; + + opts.authUrl = "http://localhost:8982/get-token-request"; + opts.authHeaders = new Param[]{new Param("test-header", "test-value")}; + AblyRest ably = new AblyRest(opts); + + /* make a call to trigger token request */ + ably.auth.requestToken(null, null); + + /* check request contained expected params */ + List headers = httpListener.getFirstRequest().requestHeaders.get("test-header"); + assertTrue("Verify expected headers passed to authURL", headers.contains("test-value")); + } catch (AblyException e) { + e.printStackTrace(); + fail("auth_authURL_authHeaders_post: Unexpected exception instantiating library"); + } + } + + /** + * Verify authCallback called and handled when returning {@code TokenRequest} + * Spec: RSA8d + */ + @Test + public void auth_authcallback_tokenrequest() { + try { + /* implement callback, using Ably instance with key */ + TokenCallback authCallback = new TokenCallback() { + private AblyRest ably = new AblyRest(createOptions(testVars.keys[0].keyStr)); + @Override + public Object getTokenRequest(TokenParams params) throws AblyException { + return ably.auth.createTokenRequest(params, null); + } + }; + + /* create Ably instance without key */ + ClientOptions opts = createOptions(); + opts.authCallback = authCallback; + AblyRest ably = new AblyRest(opts); + + /* make a call to trigger token request */ + try { + TokenDetails tokenDetails = ably.auth.requestToken(null, null); + assertNotNull("Expected token value", tokenDetails.token); + } catch (AblyException e) { + e.printStackTrace(); + fail("auth_authURL_tokenrequest: Unexpected exception requesting token"); + } + } catch (AblyException e) { + e.printStackTrace(); + fail("auth_authURL_tokenrequest: Unexpected exception instantiating library"); + } + } + + /** + * Verify authCallback called and handled when returning {@code TokenDetails} + * Spec: RSA8d + */ + @Test + public void auth_authcallback_tokendetails() { + try { + /* implement callback, using Ably instance with key */ + TokenCallback authCallback = new TokenCallback() { + private AblyRest ably = new AblyRest(createOptions(testVars.keys[0].keyStr)); + @Override + public Object getTokenRequest(TokenParams params) throws AblyException { + return ably.auth.requestToken(params, null); + } + }; + + /* create Ably instance without key */ + ClientOptions opts = createOptions(); + opts.authCallback = authCallback; + AblyRest ably = new AblyRest(opts); + + /* make a call to trigger token request */ + try { + TokenDetails tokenDetails = ably.auth.requestToken(null, null); + assertNotNull("Expected token value", tokenDetails.token); + } catch (AblyException e) { + e.printStackTrace(); + fail("auth_authURL_tokenrequest: Unexpected exception requesting token"); + } + } catch (AblyException e) { + e.printStackTrace(); + fail("auth_authURL_tokenrequest: Unexpected exception instantiating library"); + } + } + + /** + * Verify authCallback called and handled when returning token string + * Spec: RSA8d + */ + @Test + public void auth_authcallback_tokenstring() throws AblyException { + /* implement callback, using Ably instance with key */ + TokenCallback authCallback = new TokenCallback() { + private AblyRest ably = new AblyRest(createOptions(testVars.keys[0].keyStr)); + @Override + public Object getTokenRequest(TokenParams params) throws AblyException { + return ably.auth.requestToken(params, null).token; + } + }; + + /* create Ably instance without key */ + ClientOptions opts = createOptions(); + opts.authCallback = authCallback; + AblyRest ably = new AblyRest(opts); + + /* make a call to trigger token request */ + try { + TokenDetails tokenDetails = ably.auth.requestToken(null, null); + assertNotNull("Expected token value", tokenDetails.token); + } catch (AblyException e) { + e.printStackTrace(); + fail("auth_authURL_tokenrequest: Unexpected exception requesting token"); + } + } + + /** + * Verify authCallback called when token expires; Ably initialised with token + */ + @Test + public void auth_authcallback_token_expire() { + try { + ClientOptions optsForToken = createOptions(testVars.keys[0].keyStr); + final AblyRest ablyForToken = new AblyRest(optsForToken); + TokenDetails tokenDetails = ablyForToken.auth.requestToken(new TokenParams() {{ ttl = 5000L; }}, null); + assertNotNull("Expected token value", tokenDetails.token); + + /* implement callback, using Ably instance with key */ + final class TokenGenerator implements TokenCallback { + @Override + public Object getTokenRequest(TokenParams params) throws AblyException { + ++cbCount; + return ablyForToken.auth.requestToken(params, null); + } + public int getCbCount() { return cbCount; } + private int cbCount = 0; + }; + + TokenGenerator authCallback = new TokenGenerator(); + + /* create Ably instance without key */ + ClientOptions opts = createOptions(); + opts.token = tokenDetails.token; + opts.authCallback = authCallback; + AblyRest ably = new AblyRest(opts); + + /* wait until token expires */ + try { + Thread.sleep(6000L); + } catch(InterruptedException ie) {} + + /* make a request that relies on the token */ + try { + ably.stats(new Param[] { new Param("by", "hour"), new Param("limit", "1") }); + } catch (AblyException e) { + e.printStackTrace(); + fail("auth_authURL_tokenrequest: Unexpected exception requesting token"); + } + + /* verify that the auth callback was called */ + assertEquals("Expected token generator to be called", 1, authCallback.getCbCount()); + } catch (AblyException e) { + e.printStackTrace(); + fail("auth_authURL_tokenrequest: Unexpected exception instantiating library"); + } + } + + /** + * Verify authCallback called when token expires; Ably initialised with key + */ + @Test + public void auth_authcallback_key_expire() { + try { + /* create Ably instance with key */ + ClientOptions opts = createOptions(testVars.keys[0].keyStr); + opts.clientId = "testClientId"; + opts.useTokenAuth = true; + opts.defaultTokenParams.ttl = 5000L; + AblyRest ably = new AblyRest(opts); + + /* make a request that relies on the token */ + System.out.println("auth_authcallback_key_expire: making first request"); + try { + ably.stats(new Param[] { new Param("by", "hour"), new Param("limit", "1") }); + } catch (AblyException e) { + e.printStackTrace(); + fail("auth_authURL_tokenrequest: Unexpected exception requesting token"); + } + String firstToken = ably.auth.getTokenDetails().token; + + /* wait until token expires */ + System.out.println("auth_authcallback_key_expire: sleeping"); + try { + Thread.sleep(6000L); + } catch(InterruptedException ie) {} + + /* make a request that relies on the token */ + System.out.println("auth_authcallback_key_expire: making second request"); + try { + ably.stats(new Param[] { new Param("by", "hour"), new Param("limit", "1") }); + } catch (AblyException e) { + e.printStackTrace(); + fail("auth_authURL_tokenrequest: Unexpected exception requesting token"); + } + String secondToken = ably.auth.getTokenDetails().token; + + /* verify that the token was renewed */ + assertNotEquals("Verify token was renewed", firstToken, secondToken); + } catch (AblyException e) { + e.printStackTrace(); + fail("auth_authURL_tokenrequest: Unexpected exception instantiating library"); + } + } + + /** + * Verify authCallback called and handled when returning error + */ + @Test + public void auth_authcallback_err() { + try { + /* implement callback, using Ably instance with key */ + TokenCallback authCallback = new TokenCallback() { + @Override + public Object getTokenRequest(TokenParams params) throws AblyException { + throw AblyException.fromErrorInfo(new ErrorInfo("test exception", 404, 12345)); + } + }; + + /* create Ably instance without key */ + ClientOptions opts = createOptions(); + opts.authCallback = authCallback; + AblyRest ably = new AblyRest(opts); + + /* make a call to trigger token request */ + try { + ably.auth.requestToken(null, null); + fail("auth_authURL_err: Unexpected success requesting token"); + } catch (AblyException e) { + assertEquals("Expected error code", e.errorInfo.code, 80019); + assertEquals("Expected forwarded error code", ((AblyException)e.getCause()).errorInfo.code, 12345); + } + } catch (AblyException e) { + e.printStackTrace(); + fail("auth_authURL_token: Unexpected exception instantiating library"); + } + } + + /** + * Verify library throws an error on initialistion if no auth details are provided + * Spec: RSA14 + */ + @Test + public void authinit_no_auth() { + try { + ClientOptions opts = new ClientOptions(); + new AblyRest(opts); + fail("authinit_no_auth: Unexpected success instantiating library"); + } catch (AblyException e) { + assertEquals("Verify exception thrown initialising library", e.errorInfo.code, 40000); + } + } + + /** + * Verify preemptive auth occurs when an API call is made using basic auth + */ + @Test + public void auth_preemptive_auth_basic() { + try { + /* create Ably instance with key */ + DebugOptions opts = new DebugOptions(testVars.keys[0].keyStr); + fillInOptions(opts); + RawHttpTracker httpListener = new RawHttpTracker(); + opts.httpListener = httpListener; + AblyRest ably = new AblyRest(opts); + + /* make a request that relies on authentication */ + try { + ably.stats(new Param[] { new Param("by", "hour"), new Param("limit", "1") }); + } catch (AblyException e) { + e.printStackTrace(); + fail("auth_preemptive_auth_basic: Unexpected exception making API call"); + } + + /* verify that the request was sent once only with a basic auth header */ + assertEquals("Verify one request made", httpListener.size(), 1); + assertTrue("Verify request had auth header", httpListener.getFirstRequest().authHeader.startsWith("Basic")); + } catch (AblyException e) { + e.printStackTrace(); + fail("auth_preemptive_auth_basic: Unexpected exception instantiating library"); + } + } + + /** + * Verify preemptive auth occurs when an API call is on an Ably instanced initialised with a token + */ + @Test + public void auth_preemptive_auth_given_token() { + try { + ClientOptions optsForToken = createOptions(testVars.keys[0].keyStr); + optsForToken.clientId = "testClientId"; + AblyRest ablyForToken = new AblyRest(optsForToken); + TokenDetails tokenDetails = ablyForToken.auth.requestToken(null, null); + + /* create Ably instance with token */ + DebugOptions opts = new DebugOptions(tokenDetails.token); + fillInOptions(opts); + RawHttpTracker httpListener = new RawHttpTracker(); + opts.httpListener = httpListener; + AblyRest ably = new AblyRest(opts); + + /* make a request that relies on authentication */ + try { + ably.stats(new Param[] { new Param("by", "hour"), new Param("limit", "1") }); + } catch (AblyException e) { + e.printStackTrace(); + fail("auth_preemptive_auth_given_token: Unexpected exception making API call"); + } + + /* verify that the request was sent once only with a basic auth header */ + assertEquals("Verify one request made", httpListener.size(), 1); + assertTrue("Verify request had auth header", httpListener.getFirstRequest().authHeader.startsWith("Bearer")); + } catch (AblyException e) { + e.printStackTrace(); + fail("auth_preemptive_auth_given_token: Unexpected exception instantiating library"); + } + } + + /** + * Verify preemptive auth occurs when an API call is on an Ably instanced with a key but using token auth + */ + @Test + public void auth_preemptive_auth_created_token() { + try { + /* create Ably instance with key */ + DebugOptions opts = new DebugOptions(testVars.keys[0].keyStr); + fillInOptions(opts); + opts.clientId = "testClientId"; + opts.useTokenAuth = true; + RawHttpTracker httpListener = new RawHttpTracker(); + opts.httpListener = httpListener; + AblyRest ably = new AblyRest(opts); + + /* make a request that relies on authentication */ + try { + ably.stats(new Param[] { new Param("by", "hour"), new Param("limit", "1") }); + } catch (AblyException e) { + e.printStackTrace(); + fail("auth_preemptive_auth_basic: Unexpected exception making API call"); + } + + /* verify that there were two requests: one to get a token, and one to make the API call */ + assertEquals("Verify two requests made", httpListener.size(), 2); + assertTrue("Verify token request made", httpListener.getFirstRequest().url.getPath().endsWith("requestToken")); + assertTrue("Verify API request had auth header", httpListener.getLastRequest().authHeader.startsWith("Bearer")); + } catch (AblyException e) { + e.printStackTrace(); + fail("auth_preemptive_auth_basic: Unexpected exception instantiating library"); + } + } + + /** + * RSA7c: A clientId value of "*" provided in ClientOptions throws an exception + */ + @Test + public void auth_client_wildcard() { + ClientOptions opts; + try { + opts = createOptions(); + opts.clientId = "*"; + new AblyRest(opts); + } catch (AblyException e) { + assertEquals("Verify exception raised from disallowed wildcard clientId", e.errorInfo.code, 40000); + } + } + + /** + * RSA15a: Any clientId provided in ClientOptions must match any + * non wildcard ('*') clientId value in TokenDetails + * RSA15c: Following an auth request which uses a TokenDetails or TokenRequest + * object that contains an incompatible clientId, the library should ... result + * in an appropriate error response + */ + @Test + public void auth_client_match_token() { + TokenDetails tokenDetails = new TokenDetails() {{ + clientId = "token clientId"; + token = "not.really.a.token"; + }}; + ClientOptions opts; + try { + opts = createOptions(); + opts.tokenDetails = tokenDetails; + opts.clientId = "options clientId"; + new AblyRest(opts); + } catch (AblyException e) { + assertEquals("Verify exception raised from incompatible clientIds", e.errorInfo.code, 40101); + } + } + + /** + * RSA15a: Any clientId provided in ClientOptions must match any + * non wildcard ('*') clientId value in TokenDetails + * RSA15b: If the clientId from TokenDetails or connectionDetails contains + * only a wildcard string '*', then the client is permitted to be either + * unidentified or identified by providing + * a clientId when communicating with Ably + */ + @Test + public void auth_client_match_token_wildcard() { + TokenDetails tokenDetails = new TokenDetails() {{ + clientId = "*"; + token = "not.really.a.token"; + }}; + ClientOptions opts; + try { + opts = createOptions(); + opts.tokenDetails = tokenDetails; + opts.clientId = "options clientId"; + AblyRest ably = new AblyRest(opts); + assertEquals("Verify given clientId is compatible with wildcard token clientId", ably.auth.clientId, "options clientId"); + } catch (AblyException e) { + e.printStackTrace(); + fail("auth_authURL_token: Unexpected exception instantiating library"); + } + } + + /** + * RSA15b: If the clientId from TokenDetails or connectionDetails contains + * only a wildcard string '*', then the client is permitted to be either + * unidentified or identified by providing + * a clientId when communicating with Ably + */ + @Test + public void auth_client_null_match_token_wildcard() { + TokenDetails tokenDetails = new TokenDetails() {{ + clientId = "*"; + token = "not.really.a.token"; + }}; + ClientOptions opts; + try { + opts = createOptions(); + opts.tokenDetails = tokenDetails; + opts.clientId = null; + AblyRest ably = new AblyRest(opts); + assertEquals("Verify given clientId is compatible with wildcard token clientId", ably.auth.clientId, "*"); + } catch (AblyException e) { + e.printStackTrace(); + fail("auth_authURL_token: Unexpected exception instantiating library"); + } + } + + /** + * Verify token details has null client id after authenticating with null client id, + * the message gets published, and published message also does not contain a client id.
+ *
+ * Spec: RSA8f1 + */ + @Test + public void auth_clientid_null_success() { + try { + /* implement callback, using Ably instance with key */ + TokenCallback authCallback = new TokenCallback() { + private AblyRest ably = new AblyRest(createOptions(testVars.keys[0].keyStr)); + @Override + public Object getTokenRequest(TokenParams params) throws AblyException { + return ably.auth.requestToken(params, null); + } + }; + + /* create Ably instance without clientId */ + ClientOptions options = createOptions(); + options.clientId = null; + options.authCallback = authCallback; + AblyRest ably = new AblyRest(options); + + /* Fetch token */ + TokenDetails tokenDetails = ably.auth.requestToken(null, null); + assertEquals("Auth#clientId is expected to be null", null, ably.auth.clientId); + assertEquals("TokenDetails#clientId is expected to be null", null, tokenDetails.clientId); + + /* Publish message */ + String messageName = "clientless"; + String messageData = String.valueOf(System.currentTimeMillis()); + + Channel channel = ably.channels.get("test"); + channel.publish(messageName, messageData); + + /* Fetch published message */ + PaginatedResult result = channel.history(null); + Message[] messages = result.items(); + Message publishedMessage = null; + Message message; + + for(int i = 0; i < messages.length; i++) { + message = messages[i]; + + if(messageName.equals(message.name) && + messageData.equals(message.data)) { + publishedMessage = message; + break; + } + } + + assertNotNull("Recently published message expected to be accessible", publishedMessage); + assertEquals("Message#clientId is expected to be null", null, publishedMessage.clientId); + } catch (Exception e) { + e.printStackTrace(); + fail("auth_clientid_null_success: Unexpected exception"); + } + } + + /** + * Verify message gets rejected when there is a client id mismatch + * between token details and message
+ *
+ * Spec: RSA8f2 + */ + @Test + public void auth_clientid_null_mismatch() throws AblyException { + AblyRest ably = null; + + try { + /* implement callback, using Ably instance with key */ + TokenCallback authCallback = new TokenCallback() { + private AblyRest ably = new AblyRest(createOptions(testVars.keys[0].keyStr)); + @Override + public Object getTokenRequest(TokenParams params) throws AblyException { + return ably.auth.requestToken(params, null); + } + }; + + /* create Ably instance */ + ClientOptions options = createOptions(); + options.authCallback = authCallback; + ably = new AblyRest(options); + + /* Create token with null clientId */ + TokenParams tokenParams = new TokenParams() {{ clientId = null; }}; + TokenDetails tokenDetails = ably.auth.requestToken(tokenParams, null); + assertEquals("TokenDetails#clientId is expected to be null", null, tokenDetails.clientId); + assertEquals("Auth#clientId is expected to be null", null, ably.auth.clientId); + } catch (Exception e) { + e.printStackTrace(); + fail("auth_clientid_null_mismatch: Unexpected exception"); + } + + try { + /* Publish a message with mismatching client id */ + Message message = new Message( + "I", /* name */ + "will", /* data */ + "fail" /* mismatching client id */ + ); + Channel channel = ably.channels.get("test"); + channel.publish(new Message[]{ message }); + } catch(AblyException e) { + assertEquals("Verify exception is raised with expected error code", e.errorInfo.code, 40012); + } + } + + /** + * Verify message with wildcard `*` client id gets published, + * and contains null client id.
+ *
+ * Spec: RSA8f3, RSA7b4 + */ + @Test + public void auth_clientid_null_wildcard () { + try { + /* implement callback, using Ably instance with key */ + TokenCallback authCallback = new TokenCallback() { + private AblyRest ably = new AblyRest(createOptions(testVars.keys[0].keyStr)); + @Override + public Object getTokenRequest(TokenParams params) throws AblyException { + params.clientId = "*"; + return ably.auth.requestToken(params, null); + } + }; + + /* create Ably instance with wildcard clientId */ + ClientOptions options = createOptions(); + options.authCallback = authCallback; + AblyRest ably = new AblyRest(options); + + /* Fetch token */ + TokenDetails tokenDetails = ably.auth.authorize(null, null); + assertEquals("TokenDetails#clientId is expected to be wildcard '*'", "*", tokenDetails.clientId); + assertEquals("Auth#clientId is expected to be wildcard '*'", "*", ably.auth.clientId); + + /* Publish message */ + String messageName = "wildcard"; + String messageData = String.valueOf(System.currentTimeMillis()); + + Channel channel = ably.channels.get("test"); + channel.publish(messageName, messageData); + + /* Fetch published message */ + PaginatedResult result = channel.history(null); + Message[] messages = result.items(); + Message publishedMessage = null; + Message message; + + for(int i = 0; i < messages.length; i++) { + message = messages[i]; + + if(messageName.equals(message.name) && + messageData.equals(message.data)) { + publishedMessage = message; + break; + } + } + + assertNotNull("Recently published message expected to be accessible", publishedMessage); + assertEquals("Message#clientId is expected to be null", null, publishedMessage.clientId); + } catch (Exception e) { + e.printStackTrace(); + fail("auth_clientid_null_wildcard: Unexpected exception"); + } + } + + /** + * Verify message with explicit client id successfully gets published, + * when authenticated with wildcard '*' client id
+ *
+ * Spec: RSA8f4 + */ + @Test + public void auth_clientid_explicit_wildcard () { + try { + /* implement callback, using Ably instance with key */ + TokenCallback authCallback = new TokenCallback() { + private AblyRest ably = new AblyRest(createOptions(testVars.keys[0].keyStr)); + @Override + public Object getTokenRequest(TokenParams params) throws AblyException { + params.clientId = "*"; + return ably.auth.requestToken(params, null); + } + }; + + /* create Ably instance with wildcard clientId */ + ClientOptions options = createOptions(); + options.authCallback = authCallback; + AblyRest ably = new AblyRest(options); + + /* Fetch token */ + TokenDetails tokenDetails = ably.auth.authorize(null, null); + assertEquals("TokenDetails#clientId is expected to be wildcard '*'", "*", tokenDetails.clientId); + assertEquals("Auth#clientId is expected to be wildcard '*'", "*", ably.auth.clientId); + + /* Publish a message */ + Message messagePublishee = new Message( + "wildcard", /* name */ + String.valueOf(System.currentTimeMillis()), /* data */ + "brian that is called brian" /* clientId */ + ); + + Channel channel = ably.channels.get("test"); + channel.publish(new Message[] { messagePublishee }); + + /* Fetch published message */ + PaginatedResult result = channel.history(null); + Message[] messages = result.items(); + Message messagePublished = null; + Message message; + + for(int i = 0; i < messages.length; i++) { + message = messages[i]; + + if(messagePublishee.name.equals(message.name) && + messagePublishee.data.equals(message.data)) { + messagePublished = message; + break; + } + } + + assertNotNull("Recently published message expected to be accessible", messagePublished); + assertEquals("Message#clientId is expected to be same with explicitly defined clientId", messagePublishee.clientId, messagePublished.clientId); + } catch (Exception e) { + e.printStackTrace(); + fail("auth_clientid_explicit_wildcard: Unexpected exception"); + } + } + + /** + * Verify message does not have explicit client id populated + * when library is identified + * Spec: RSA7a1,RSL1g1a + */ + @Test + public void auth_clientid_publish_implicit() { + try { + String clientId = "test clientId"; + ClientOptions optsForToken = createOptions(testVars.keys[0].keyStr); + optsForToken.clientId = clientId; + AblyRest ablyForToken = new AblyRest(optsForToken); + TokenDetails tokenDetails = ablyForToken.auth.requestToken(null, null); + + final Message[] messages = new Message[1]; + + /* create Ably instance with clientId */ + DebugOptions options = new DebugOptions(testVars.keys[0].keyStr) {{ + this.httpListener = new RawHttpListener() { + @Override + public HttpCore.Response onRawHttpRequest(String id, HttpURLConnection conn, String method, String authHeader, + Map> requestHeaders, HttpCore.RequestBody requestBody) { + try { + if(testParams.useBinaryProtocol) { + messages[0] = MessageSerializer.readMsgpack(requestBody.getEncoded())[0]; + } else { + messages[0] = MessageSerializer.readMessagesFromJson(requestBody.getEncoded())[0]; + } + } catch (AblyException e) { + e.printStackTrace(); + fail("auth_clientid_publish_implicit: Unexpected exception"); + } + return null; + } + + @Override + public void onRawHttpResponse(String id, String method, HttpCore.Response response) {} + + @Override + public void onRawHttpException(String id, String method, Throwable t) {} + }; + }}; + fillInOptions(options); + options.tokenDetails = tokenDetails; + AblyRest ably = new AblyRest(options); + + /* Publish a message */ + Message messagePublishee = new Message( + "I have clientId", /* name */ + String.valueOf(System.currentTimeMillis()) /* data */ + ); + + Channel channel = ably.channels.get("test_" + testParams.name); + channel.publish(new Message[] { messagePublishee }); + + /* Get sent message */ + Message messagePublished = messages[0]; + assertNull("Published message does not contain clientId", messagePublished.clientId); + } catch (Exception e) { + e.printStackTrace(); + fail("auth_clientid_publish_implicit: Unexpected exception"); + } + } + + /** + * Verify message does have explicit client id populated + * when library is initialised as wildcard + * Spec: RSA8f4 + */ + @Test + public void auth_clientid_publish_explicit_in_message() { + try { + final String messageClientId = "test clientId"; + ClientOptions optsForToken = createOptions(testVars.keys[0].keyStr); + AblyRest ablyForToken = new AblyRest(optsForToken); + TokenDetails tokenDetails = ablyForToken.auth.requestToken(new TokenParams() {{ + this.clientId = "*"; + }}, null); + + final Message[] messages = new Message[1]; + + /* create Ably instance with clientId */ + DebugOptions options = new DebugOptions(testVars.keys[0].keyStr) {{ + this.httpListener = new RawHttpListener() { + @Override + public HttpCore.Response onRawHttpRequest(String id, HttpURLConnection conn, String method, String authHeader, + Map> requestHeaders, HttpCore.RequestBody requestBody) { + try { + if(testParams.useBinaryProtocol) { + messages[0] = MessageSerializer.readMsgpack(requestBody.getEncoded())[0]; + } else { + messages[0] = MessageSerializer.readMessagesFromJson(requestBody.getEncoded())[0]; + } + } catch (AblyException e) { + e.printStackTrace(); + fail("auth_clientid_publish_implicit: Unexpected exception"); + } + return null; + } + + @Override + public void onRawHttpResponse(String id, String method, HttpCore.Response response) {} + + @Override + public void onRawHttpException(String id, String method, Throwable t) {} + }; + }}; + fillInOptions(options); + options.tokenDetails = tokenDetails; + AblyRest ably = new AblyRest(options); + + /* Publish a message */ + Message messagePublishee = new Message( + "I have clientId", /* name */ + String.valueOf(System.currentTimeMillis()), /* data */ + messageClientId /* clientId */ + ); + + Channel channel = ably.channels.get("test_" + testParams.name); + channel.publish(new Message[] { messagePublishee }); + + /* Get sent message */ + Message messagePublished = messages[0]; + assertEquals("Published message contains clientId", messagePublished.clientId, messageClientId); + } catch (Exception e) { + e.printStackTrace(); + fail("auth_clientid_publish_explicit_in_message: Unexpected exception"); + } + } + + /** + * Verify message with wildcard `*` client id gets published, + * and contains null client id.
+ *
+ * Spec: RTL6e1 + */ + @Test + public void auth_clientid_basic_null_wildcard() { + try { + /* create Ably instance with basic auth and no clientId */ + ClientOptions options = createOptions(testVars.keys[0].keyStr); + AblyRest ably = new AblyRest(options); + + /* Publish message */ + String messageName = "wildcard"; + String messageData = String.valueOf(System.currentTimeMillis()); + String clientId = "message clientId"; + + Channel channel = ably.channels.get("auth_clientid_basic_null_wildcard_" + testParams.name); + Message message = new Message(messageName, messageData); + message.clientId = clientId; + channel.publish(new Message[] { message }); + + /* Fetch published message */ + PaginatedResult result = channel.history(null); + Message[] messages = result.items(); + Message publishedMessage = null; + + for(int i = 0; i < messages.length; i++) { + Message msg = messages[i]; + + if(messageName.equals(msg.name) && + messageData.equals(msg.data)) { + publishedMessage = message; + break; + } + } + + assertNotNull("Recently published message expected to be accessible", publishedMessage); + assertEquals("Message#clientId is expected to be set", clientId, publishedMessage.clientId); + } catch (Exception e) { + e.printStackTrace(); + fail("auth_clientid_basic_null_wildcard: Unexpected exception"); + } + } + + /** + * Verify client id in token is populated from defaultTokenParams + * when library is initialised without explicit clientId + * Spec: RSA7a4, RSA7d + */ + @Test + public void auth_clientid_in_defaultparams() { + try { + final String defaultClientId = "default clientId"; + + /* create Ably instance with defaultTokenParams */ + ClientOptions options = createOptions(testVars.keys[0].keyStr); + options.useTokenAuth = true; + options.defaultTokenParams = new TokenParams() {{ this.clientId = defaultClientId; }}; + AblyRest ably = new AblyRest(options); + + /* get a token with these default params */ + ably.auth.authorize(null, null); + + /* verify that clientId is set */ + assertEquals("Verify expected clientId is set", ably.auth.clientId, defaultClientId); + + } catch (Exception e) { + e.printStackTrace(); + fail("auth_clientid_in_defaultparams: Unexpected exception"); + } + } + + /** + * Verify client id in token populated from ClientOptions + * overriding any clientId in defaultTokenParams + * Spec: RSA7a4, RSA7d + */ + @Test + public void auth_clientid_in_opts_overrides_defaultparams() { + try { + final String defaultClientId = "default clientId"; + final String clientId = "options clientId"; + + /* create Ably instance with defaultTokenParams */ + ClientOptions options = createOptions(testVars.keys[0].keyStr); + options.useTokenAuth = true; + options.clientId = clientId; + options.defaultTokenParams = new TokenParams() {{ this.clientId = defaultClientId; }}; + AblyRest ably = new AblyRest(options); + + /* get a token with these default params */ + ably.auth.authorize(null, null); + + /* verify that clientId is set */ + assertEquals("Verify expected clientId is set", ably.auth.clientId, clientId); + + } catch (Exception e) { + e.printStackTrace(); + fail("auth_clientid_in_defaultparams: Unexpected exception"); + } + } + + /** + * Verify token auth used when useTokenAuth=true + */ + @Test + public void auth_useTokenAuth() { + try { + ClientOptions opts = createOptions(testVars.keys[0].keyStr); + opts.useTokenAuth = true; + AblyRest ably = new AblyRest(opts); + /* verify that we don't have a token yet. */ + assertTrue("Not expecting a token yet", ably.auth.getTokenDetails() == null); + /* make a request that relies on the token */ + try { + ably.stats(new Param[] { new Param("by", "hour"), new Param("limit", "1") }); + } catch (AblyException e) { + e.printStackTrace(); + fail("Unexpected exception requesting token"); + } + /* verify that we have a token. */ + assertTrue("Expected to use token auth", ably.auth.getTokenDetails() != null); + System.out.println("Token is " + ably.auth.getTokenDetails().token); + } catch (AblyException e) { + e.printStackTrace(); + fail("Unexpected exception instantiating library"); + } + } + + /** + * Test behaviour of queryTime parameter in ClientOpts. Time is requested from the Ably server only once, + * cached value should be used afterwards + * Spec: RSA9a + */ + @Test + public void auth_testQueryTime() { + try { + nanoHTTPD.clearRequestHistory(); + ClientOptions opts = new ClientOptions(testVars.keys[0].keyStr); + opts.tls = false; + opts.restHost = "localhost"; + opts.port = nanoHTTPD.getListeningPort(); + opts.queryTime = true; + + AblyRest ably1 = new AblyRest(opts); + @SuppressWarnings("unused") + Auth.TokenRequest tr1 = ably1.auth.createTokenRequest(null, null); + + AblyRest ably2 = new AblyRest(opts); + @SuppressWarnings("unused") + Auth.TokenRequest tr2 = ably2.auth.createTokenRequest(null, null); + + List requestHistory = nanoHTTPD.getRequestHistory(); + /* search for all /time request in the list */ + int timeRequestCount = 0; + for (String request: requestHistory) + if (request.contains("/time")) + timeRequestCount++; + + assertEquals("Verify number of time requests", timeRequestCount, 2); + } catch (AblyException e) { + e.printStackTrace(); + fail("Unexpected exception"); + } + } + + /** + * Verify JSON serialisation and deserialisation of basic types + * Spec: TE6, TD7 + */ + @Test + public void auth_json_interop() { + /* create a token request */ + AblyRest ably; + TokenRequest tokenRequest; + try { + ClientOptions opts = createOptions(testVars.keys[0].keyStr); + opts.clientId = "Test client id"; + ably = new AblyRest(opts); + tokenRequest = ably.auth.createTokenRequest(new TokenParams() {{ + ttl = 10000; + capability = "{\"*\": [\"*\"]}"; + }}, null); + String serialisedTokenRequest = tokenRequest.asJson(); + TokenRequest deserialisedTokenRequest = TokenRequest.fromJson(serialisedTokenRequest); + assertEquals("Verify token request is serialised and deserialised successfully", tokenRequest, deserialisedTokenRequest); + } catch (AblyException e) { + e.printStackTrace(); + fail("Unexpected exception"); + return; + } + /* create a token details */ + try { + TokenDetails tokenDetails = ably.auth.requestToken(tokenRequest, null); + String serialisedTokenDetails = tokenDetails.asJson(); + TokenDetails deserialisedTokenDetails = TokenDetails.fromJson(serialisedTokenDetails); + assertEquals("Verify token details is serialised and deserialised successfully", tokenDetails, deserialisedTokenDetails); + } catch (AblyException e) { + e.printStackTrace(); + fail("Unexpected exception"); + } + } + + /** + * Verify TokenRequest as JSON TTL and capability are omitted if not explicitly set to nonzero. + * Spec: RSA5, RSA6 + */ + @Test + public void auth_token_request_json_omitted_defaults() { + AblyRest ably; + TokenRequest tokenRequest; + try { + for (final String cap : new String[] {null, ""}) { + ClientOptions opts = createOptions(testVars.keys[0].keyStr); + opts.clientId = "Test client id"; + ably = new AblyRest(opts); + tokenRequest = ably.auth.createTokenRequest(new TokenParams() {{ + capability = cap; + }}, null); + String serialisedTokenRequest = tokenRequest.asJson(); + assertTrue("Verify token request has no ttl", !serialisedTokenRequest.contains("ttl")); + assertTrue("Verify token request has no capability", !serialisedTokenRequest.contains("capability")); + } + } catch (AblyException e) { + e.printStackTrace(); + fail("Unexpected exception"); + } + } + + /** + * Verify that renewing the token when useTokenAuth is true doesn't use the old (expired) token. + */ + @Test + public void auth_renew_token_bearer_auth() { + try { + ClientOptions opts = createOptions(testVars.keys[0].keyStr); + opts.useTokenAuth = true; + opts.defaultTokenParams = new TokenParams() {{ + ttl = 500; + }}; + AblyRest ably = new AblyRest(opts); + + // Any request will issue a new token with the defaultTokenParams and use it. + + ably.channels.get("test").history(null); + TokenDetails oldToken = ably.auth.getTokenDetails(); + + // Sleep until old token expires, then ensure it did. + + Thread.sleep(1000); + ClientOptions optsWithOldToken = createOptions(); + optsWithOldToken.tokenDetails = oldToken; + AblyRest ablyWithOldToken = new AblyRest(optsWithOldToken); + try { + ablyWithOldToken.channels.get("test").history(null); + fail("expected old token to be expired already"); + } catch(AblyException e) {} + + // The library should now renew the token using the key. + + ably.channels.get("test").history(null); + TokenDetails newToken = ably.auth.getTokenDetails(); + + assertNotEquals(oldToken.token, newToken.token); + } catch (Exception e) { + e.printStackTrace(); + fail("Unexpected exception"); + } + } + + /** + * Verify that a local token validity check is made if queryTime == true + * and local time is in sync with server + * Spec: RSA4b1 + */ + @Test + public void auth_local_token_expiry_check_sync() { + try { + /* get a TokenDetails and allow to expire */ + final String testKey = testVars.keys[0].keyStr; + ClientOptions optsForToken = createOptions(testKey); + optsForToken.queryTime = true; + AblyRest ablyForToken = new AblyRest(optsForToken); + + TokenDetails tokenDetails = ablyForToken.auth.requestToken(new TokenParams(){{ ttl = 100L; }}, null); + + /* create Ably instance with token details */ + DebugOptions opts = new DebugOptions(); + fillInOptions(opts); + opts.queryTime = true; + opts.tokenDetails = tokenDetails; + RawHttpTracker httpListener = new RawHttpTracker(); + opts.httpListener = httpListener; + AblyRest ably = new AblyRest(opts); + + /* sync this library instance to server by creating a token request */ + ably.auth.createTokenRequest(null, new Auth.AuthOptions() {{ key = testKey; queryTime = true; }}); + + /* wait for the token to expire */ + try { Thread.sleep(200L); } catch(InterruptedException ie) {} + + /* make a request that relies on authentication */ + try { + ably.stats(new Param[] { new Param("by", "hour"), new Param("limit", "1") }); + fail("auth_local_token_expiry_check_sync: API call unexpectedly succeeded"); + return; + } catch (AblyException e) { + assertEquals("Verify that API request failed with credentials error", e.errorInfo.code, 40106); + for(Helpers.RawHttpRequest req : httpListener.values()) { + assertFalse("Verify no API request attempted", req.url.getPath().contains("stats")); + } + } + } catch (AblyException e) { + e.printStackTrace(); + fail("auth_local_token_expiry_check_sync: Unexpected exception instantiating library"); + } + } + + /** + * Verify that a local token validity check is not made if queryTime == false + * and local time is not in sync with server + * Spec: RSA4b1 + */ + @Test + public void auth_local_token_expiry_check_nosync() { + try { + /* get a TokenDetails and allow to expire */ + final String testKey = testVars.keys[0].keyStr; + ClientOptions optsForToken = createOptions(testKey); + optsForToken.queryTime = true; + AblyRest ablyForToken = new AblyRest(optsForToken); + + TokenDetails tokenDetails = ablyForToken.auth.requestToken(new TokenParams(){{ ttl = 100L; }}, null); + + /* create Ably instance with token details */ + DebugOptions opts = new DebugOptions(); + fillInOptions(opts); + opts.queryTime = false; + opts.tokenDetails = tokenDetails; + RawHttpTracker httpListener = new RawHttpTracker(); + opts.httpListener = httpListener; + AblyRest ably = new AblyRest(opts); + + /* wait for the token to expire */ + try { Thread.sleep(200L); } catch(InterruptedException ie) {} + + /* make a request that relies on authentication */ + try { + ably.stats(new Param[] { new Param("by", "hour"), new Param("limit", "1") }); + fail("auth_local_token_expiry_check_nosync: API call unexpectedly succeeded"); + return; + } catch (AblyException e) { + assertEquals("Verify API request attempted", httpListener.size(), 1); + assertEquals("Verify API request failed with token expiry error", httpListener.getFirstRequest().response.headers.get("x-ably-errorcode").get(0), "40142"); + assertEquals("Verify that API request failed with credentials error", e.errorInfo.code, 40106); + } + } catch (AblyException e) { + e.printStackTrace(); + fail("auth_local_token_expiry_check_nosync: Unexpected exception instantiating library"); + } + } + + /** + * Verify that if a local token validity check suppressed because queryTime == false + * this does not prevent token renewal by an auth callback when a request fails + * Spec: RSA4b1 + */ + @Test + public void auth_local_token_expiry_check_nosync_retried() { + try { + /* get a TokenDetails and allow to expire */ + final String testKey = testVars.keys[0].keyStr; + ClientOptions optsForToken = createOptions(testKey); + optsForToken.queryTime = false; + final AblyRest ablyForToken = new AblyRest(optsForToken); + + TokenDetails tokenDetails = ablyForToken.auth.requestToken(new TokenParams(){{ ttl = 100L; }}, null); + + /* create Ably instance with token details */ + DebugOptions opts = new DebugOptions(); + fillInOptions(opts); + opts.queryTime = false; + opts.tokenDetails = tokenDetails; + opts.authCallback = new TokenCallback() { + @Override + public Object getTokenRequest(TokenParams params) throws AblyException { + return ablyForToken.auth.createTokenRequest(params, null); + } + }; + + RawHttpTracker httpListener = new RawHttpTracker(); + opts.httpListener = httpListener; + AblyRest ably = new AblyRest(opts); + + /* wait for the token to expire */ + try { Thread.sleep(200L); } catch(InterruptedException ie) {} + + /* make a request that relies on authentication */ + try { + ably.stats(new Param[] { new Param("by", "hour"), new Param("limit", "1") }); + } catch (AblyException e) { + fail("auth_local_token_expiry_check_nosync: API call unexpectedly failed"); + return; + } + assertEquals("Verify API request attempted", httpListener.size(), 3); + for(Helpers.RawHttpRequest x : httpListener.values()) { + System.out.println(x.url.toString()); + } + assertEquals("Verify API request failed with token expiry error", httpListener.getFirstRequest().response.headers.get("x-ably-errorcode").get(0), "40142"); + } catch (AblyException e) { + e.printStackTrace(); + fail("auth_local_token_expiry_check_nosync: Unexpected exception instantiating library"); + } + } + + private static TokenServer tokenServer; + private static SessionHandlerNanoHTTPD nanoHTTPD; + + private static class SessionHandlerNanoHTTPD extends RouterNanoHTTPD { + private final ArrayList requestHistory = new ArrayList<>(); + + public SessionHandlerNanoHTTPD(int port) { + super(port); + } + + @Override + public Response serve(IHTTPSession session) { + synchronized (requestHistory) { + requestHistory.add(session.getUri()); + } + /* the only request supported here is /time */ + return newFixedLengthResponse(String.format(Locale.US, "[%d]", System.currentTimeMillis())); + } + + public void clearRequestHistory() { requestHistory.clear(); } + + public List getRequestHistory() { return requestHistory; } + } } diff --git a/lib/src/test/java/io/ably/lib/test/rest/RestCapabilityTest.java b/lib/src/test/java/io/ably/lib/test/rest/RestCapabilityTest.java index 717aa23b9..fbaa61e96 100644 --- a/lib/src/test/java/io/ably/lib/test/rest/RestCapabilityTest.java +++ b/lib/src/test/java/io/ably/lib/test/rest/RestCapabilityTest.java @@ -19,283 +19,283 @@ public class RestCapabilityTest extends ParameterizedTest { - private AblyRest ably; + private AblyRest ably; - @Before - public void setUpBefore() throws Exception { - ClientOptions opts = createOptions(testVars.keys[0].keyStr); - ably = new AblyRest(opts); - } + @Before + public void setUpBefore() throws Exception { + ClientOptions opts = createOptions(testVars.keys[0].keyStr); + ably = new AblyRest(opts); + } - /** - * Blanket intersection with specified key - */ - @Test - public void authcapability0() { - try { - Key key = testVars.keys[1]; - AuthOptions authOptions = new AuthOptions(); - authOptions.key = key.keyStr; - TokenDetails tokenDetails = ably.auth.requestToken(null, authOptions); - assertNotNull("Expected token value", tokenDetails.token); - assertEquals("Unexpected capability", tokenDetails.capability, key.capability); - } catch (AblyException e) { - e.printStackTrace(); - fail("authcapability0: Unexpected exception"); - } - } + /** + * Blanket intersection with specified key + */ + @Test + public void authcapability0() { + try { + Key key = testVars.keys[1]; + AuthOptions authOptions = new AuthOptions(); + authOptions.key = key.keyStr; + TokenDetails tokenDetails = ably.auth.requestToken(null, authOptions); + assertNotNull("Expected token value", tokenDetails.token); + assertEquals("Unexpected capability", tokenDetails.capability, key.capability); + } catch (AblyException e) { + e.printStackTrace(); + fail("authcapability0: Unexpected exception"); + } + } - /** - * Equal intersection with specified key - */ - @Test - public void authcapability1() { - try { - Key key = testVars.keys[1]; - AuthOptions authOptions = new AuthOptions(); - authOptions.key = key.keyStr; - TokenParams tokenParams = new TokenParams(); - tokenParams.capability = key.capability; - TokenDetails tokenDetails = ably.auth.requestToken(tokenParams, authOptions); - assertNotNull("Expected token value", tokenDetails.token); - assertEquals("Unexpected capability", tokenDetails.capability, key.capability); - } catch (AblyException e) { - e.printStackTrace(); - fail("authcapability1: Unexpected exception"); - } - } + /** + * Equal intersection with specified key + */ + @Test + public void authcapability1() { + try { + Key key = testVars.keys[1]; + AuthOptions authOptions = new AuthOptions(); + authOptions.key = key.keyStr; + TokenParams tokenParams = new TokenParams(); + tokenParams.capability = key.capability; + TokenDetails tokenDetails = ably.auth.requestToken(tokenParams, authOptions); + assertNotNull("Expected token value", tokenDetails.token); + assertEquals("Unexpected capability", tokenDetails.capability, key.capability); + } catch (AblyException e) { + e.printStackTrace(); + fail("authcapability1: Unexpected exception"); + } + } - /** - * Empty ops intersection - */ - @Test - public void authcapability2() { - Key key = testVars.keys[1]; - AuthOptions authOptions = new AuthOptions(); - authOptions.key = key.keyStr; - TokenParams tokenParams = new TokenParams(); - Capability capability = new Capability(); - capability.addResource("testchannel", "subscribe"); - tokenParams.capability = capability.toString(); - try { - ably.auth.requestToken(tokenParams, authOptions); - fail("Invalid capability, expected rejection"); - } catch(AblyException e) { - assertEquals("Unexpected error code", e.errorInfo.code, 40160); - } - } + /** + * Empty ops intersection + */ + @Test + public void authcapability2() { + Key key = testVars.keys[1]; + AuthOptions authOptions = new AuthOptions(); + authOptions.key = key.keyStr; + TokenParams tokenParams = new TokenParams(); + Capability capability = new Capability(); + capability.addResource("testchannel", "subscribe"); + tokenParams.capability = capability.toString(); + try { + ably.auth.requestToken(tokenParams, authOptions); + fail("Invalid capability, expected rejection"); + } catch(AblyException e) { + assertEquals("Unexpected error code", e.errorInfo.code, 40160); + } + } - /** - * Empty paths intersection - */ - @Test - public void authcapability3() { - Key key = testVars.keys[1]; - AuthOptions authOptions = new AuthOptions(); - authOptions.key = key.keyStr; - TokenParams tokenParams = new TokenParams(); - Capability capability = new Capability(); - capability.addResource("testchannelx", "publish"); - tokenParams.capability = capability.toString(); - try { - ably.auth.requestToken(tokenParams, authOptions); - fail("Invalid capability, expected rejection"); - } catch(AblyException e) { - assertEquals("Unexpected error code", e.errorInfo.code, 40160); - } - } + /** + * Empty paths intersection + */ + @Test + public void authcapability3() { + Key key = testVars.keys[1]; + AuthOptions authOptions = new AuthOptions(); + authOptions.key = key.keyStr; + TokenParams tokenParams = new TokenParams(); + Capability capability = new Capability(); + capability.addResource("testchannelx", "publish"); + tokenParams.capability = capability.toString(); + try { + ably.auth.requestToken(tokenParams, authOptions); + fail("Invalid capability, expected rejection"); + } catch(AblyException e) { + assertEquals("Unexpected error code", e.errorInfo.code, 40160); + } + } - /** - * Non-empty ops intersection - */ - @Test - public void authcapability4() { - try { - Key key = testVars.keys[4]; - AuthOptions authOptions = new AuthOptions(); - authOptions.key = key.keyStr; - TokenParams tokenParams = new TokenParams(); - Capability requestedCapability = new Capability(); - requestedCapability.addResource("channel2", new String[]{"presence", "subscribe"}); - tokenParams.capability = requestedCapability.toString(); - TokenDetails tokenDetails = ably.auth.requestToken(tokenParams, authOptions); - Capability expectedCapability = new Capability(); - expectedCapability.addResource("channel2", "subscribe"); - assertNotNull("Expected token value", tokenDetails.token); - assertEquals("Unexpected capability", tokenDetails.capability, expectedCapability.toString()); - } catch (AblyException e) { - e.printStackTrace(); - fail("authcapability4: Unexpected exception"); - } - } + /** + * Non-empty ops intersection + */ + @Test + public void authcapability4() { + try { + Key key = testVars.keys[4]; + AuthOptions authOptions = new AuthOptions(); + authOptions.key = key.keyStr; + TokenParams tokenParams = new TokenParams(); + Capability requestedCapability = new Capability(); + requestedCapability.addResource("channel2", new String[]{"presence", "subscribe"}); + tokenParams.capability = requestedCapability.toString(); + TokenDetails tokenDetails = ably.auth.requestToken(tokenParams, authOptions); + Capability expectedCapability = new Capability(); + expectedCapability.addResource("channel2", "subscribe"); + assertNotNull("Expected token value", tokenDetails.token); + assertEquals("Unexpected capability", tokenDetails.capability, expectedCapability.toString()); + } catch (AblyException e) { + e.printStackTrace(); + fail("authcapability4: Unexpected exception"); + } + } - /** - * Non-empty paths intersection - */ - @Test - public void authcapability5() { - try { - Key key = testVars.keys[4]; - AuthOptions authOptions = new AuthOptions(); - authOptions.key = key.keyStr; - TokenParams tokenParams = new TokenParams(); - Capability requestedCapability = new Capability(); - requestedCapability.addResource("channel2", new String[]{"presence", "subscribe"}); - requestedCapability.addResource("channelx", new String[]{"presence", "subscribe"}); - tokenParams.capability = requestedCapability.toString(); - TokenDetails tokenDetails = ably.auth.requestToken(tokenParams, authOptions); - Capability expectedCapability = new Capability(); - expectedCapability.addResource("channel2", "subscribe"); - assertNotNull("Expected token value", tokenDetails.token); - assertEquals("Unexpected capability", tokenDetails.capability, expectedCapability.toString()); - } catch (AblyException e) { - e.printStackTrace(); - fail("authcapability5: Unexpected exception"); - } - } + /** + * Non-empty paths intersection + */ + @Test + public void authcapability5() { + try { + Key key = testVars.keys[4]; + AuthOptions authOptions = new AuthOptions(); + authOptions.key = key.keyStr; + TokenParams tokenParams = new TokenParams(); + Capability requestedCapability = new Capability(); + requestedCapability.addResource("channel2", new String[]{"presence", "subscribe"}); + requestedCapability.addResource("channelx", new String[]{"presence", "subscribe"}); + tokenParams.capability = requestedCapability.toString(); + TokenDetails tokenDetails = ably.auth.requestToken(tokenParams, authOptions); + Capability expectedCapability = new Capability(); + expectedCapability.addResource("channel2", "subscribe"); + assertNotNull("Expected token value", tokenDetails.token); + assertEquals("Unexpected capability", tokenDetails.capability, expectedCapability.toString()); + } catch (AblyException e) { + e.printStackTrace(); + fail("authcapability5: Unexpected exception"); + } + } - /** - * Wildcard ops intersection - */ - @Test - public void authcapability6() { - try { - Key key = testVars.keys[4]; - AuthOptions authOptions = new AuthOptions(); - authOptions.key = key.keyStr; - TokenParams tokenParams = new TokenParams(); - Capability requestedCapability = new Capability(); - requestedCapability.addResource("channel2", "*"); - tokenParams.capability = requestedCapability.toString(); - TokenDetails tokenDetails = ably.auth.requestToken(tokenParams, authOptions); - Capability expectedCapability = new Capability(); - expectedCapability.addResource("channel2", new String[]{"publish", "subscribe"}); - assertNotNull("Expected token value", tokenDetails.token); - assertEquals("Unexpected capability", tokenDetails.capability, expectedCapability.toString()); - } catch (AblyException e) { - e.printStackTrace(); - fail("authcapability6: Unexpected exception"); - } - } - @Test - public void authcapability7() { - try { - Key key = testVars.keys[4]; - AuthOptions authOptions = new AuthOptions(); - authOptions.key = key.keyStr; - TokenParams tokenParams = new TokenParams(); - Capability requestedCapability = new Capability(); - requestedCapability.addResource("channel6", new String[]{"publish", "subscribe"}); - tokenParams.capability = requestedCapability.toString(); - TokenDetails tokenDetails = ably.auth.requestToken(tokenParams, authOptions); - Capability expectedCapability = new Capability(); - expectedCapability.addResource("channel6", new String[]{"publish", "subscribe"}); - assertNotNull("Expected token value", tokenDetails.token); - assertEquals("Unexpected capability", tokenDetails.capability, expectedCapability.toString()); - } catch (AblyException e) { - e.printStackTrace(); - fail("authcapability7: Unexpected exception"); - } - } + /** + * Wildcard ops intersection + */ + @Test + public void authcapability6() { + try { + Key key = testVars.keys[4]; + AuthOptions authOptions = new AuthOptions(); + authOptions.key = key.keyStr; + TokenParams tokenParams = new TokenParams(); + Capability requestedCapability = new Capability(); + requestedCapability.addResource("channel2", "*"); + tokenParams.capability = requestedCapability.toString(); + TokenDetails tokenDetails = ably.auth.requestToken(tokenParams, authOptions); + Capability expectedCapability = new Capability(); + expectedCapability.addResource("channel2", new String[]{"publish", "subscribe"}); + assertNotNull("Expected token value", tokenDetails.token); + assertEquals("Unexpected capability", tokenDetails.capability, expectedCapability.toString()); + } catch (AblyException e) { + e.printStackTrace(); + fail("authcapability6: Unexpected exception"); + } + } + @Test + public void authcapability7() { + try { + Key key = testVars.keys[4]; + AuthOptions authOptions = new AuthOptions(); + authOptions.key = key.keyStr; + TokenParams tokenParams = new TokenParams(); + Capability requestedCapability = new Capability(); + requestedCapability.addResource("channel6", new String[]{"publish", "subscribe"}); + tokenParams.capability = requestedCapability.toString(); + TokenDetails tokenDetails = ably.auth.requestToken(tokenParams, authOptions); + Capability expectedCapability = new Capability(); + expectedCapability.addResource("channel6", new String[]{"publish", "subscribe"}); + assertNotNull("Expected token value", tokenDetails.token); + assertEquals("Unexpected capability", tokenDetails.capability, expectedCapability.toString()); + } catch (AblyException e) { + e.printStackTrace(); + fail("authcapability7: Unexpected exception"); + } + } - /** - * Wildcard resources intersection - */ - @Test - public void authcapability8() { - try { - Key key = testVars.keys[2]; - AuthOptions authOptions = new AuthOptions(); - authOptions.key = key.keyStr; - TokenParams tokenParams = new TokenParams(); - Capability requestedCapability = new Capability(); - requestedCapability.addResource("cansubscribe", "subscribe"); - tokenParams.capability = requestedCapability.toString(); - TokenDetails tokenDetails = ably.auth.requestToken(tokenParams, authOptions); - assertNotNull("Expected token value", tokenDetails.token); - assertEquals("Unexpected capability", tokenDetails.capability, requestedCapability.toString()); - } catch (AblyException e) { - e.printStackTrace(); - fail("authcapability8: Unexpected exception"); - } - } - @Test - public void authcapability9() { - try { - Key key = testVars.keys[2]; - AuthOptions authOptions = new AuthOptions(); - authOptions.key = key.keyStr; - TokenParams tokenParams = new TokenParams(); - Capability requestedCapability = new Capability(); - requestedCapability.addResource("canpublish:check", "publish"); - tokenParams.capability = requestedCapability.toString(); - TokenDetails tokenDetails = ably.auth.requestToken(tokenParams, authOptions); - assertNotNull("Expected token value", tokenDetails.token); - assertEquals("Unexpected capability", tokenDetails.capability, requestedCapability.toString()); - } catch (AblyException e) { - e.printStackTrace(); - fail("authcapability9: Unexpected exception"); - } - } - @Test - public void authcapability10() { - try { - Key key = testVars.keys[2]; - AuthOptions authOptions = new AuthOptions(); - authOptions.key = key.keyStr; - TokenParams tokenParams = new TokenParams(); - Capability requestedCapability = new Capability(); - requestedCapability.addResource("cansubscribe:*", "subscribe"); - tokenParams.capability = requestedCapability.toString(); - TokenDetails tokenDetails = ably.auth.requestToken(tokenParams, authOptions); - assertNotNull("Expected token value", tokenDetails.token); - assertEquals("Unexpected capability", tokenDetails.capability, requestedCapability.toString()); - } catch (AblyException e) { - e.printStackTrace(); - fail("authcapability10: Unexpected exception"); - } - } + /** + * Wildcard resources intersection + */ + @Test + public void authcapability8() { + try { + Key key = testVars.keys[2]; + AuthOptions authOptions = new AuthOptions(); + authOptions.key = key.keyStr; + TokenParams tokenParams = new TokenParams(); + Capability requestedCapability = new Capability(); + requestedCapability.addResource("cansubscribe", "subscribe"); + tokenParams.capability = requestedCapability.toString(); + TokenDetails tokenDetails = ably.auth.requestToken(tokenParams, authOptions); + assertNotNull("Expected token value", tokenDetails.token); + assertEquals("Unexpected capability", tokenDetails.capability, requestedCapability.toString()); + } catch (AblyException e) { + e.printStackTrace(); + fail("authcapability8: Unexpected exception"); + } + } + @Test + public void authcapability9() { + try { + Key key = testVars.keys[2]; + AuthOptions authOptions = new AuthOptions(); + authOptions.key = key.keyStr; + TokenParams tokenParams = new TokenParams(); + Capability requestedCapability = new Capability(); + requestedCapability.addResource("canpublish:check", "publish"); + tokenParams.capability = requestedCapability.toString(); + TokenDetails tokenDetails = ably.auth.requestToken(tokenParams, authOptions); + assertNotNull("Expected token value", tokenDetails.token); + assertEquals("Unexpected capability", tokenDetails.capability, requestedCapability.toString()); + } catch (AblyException e) { + e.printStackTrace(); + fail("authcapability9: Unexpected exception"); + } + } + @Test + public void authcapability10() { + try { + Key key = testVars.keys[2]; + AuthOptions authOptions = new AuthOptions(); + authOptions.key = key.keyStr; + TokenParams tokenParams = new TokenParams(); + Capability requestedCapability = new Capability(); + requestedCapability.addResource("cansubscribe:*", "subscribe"); + tokenParams.capability = requestedCapability.toString(); + TokenDetails tokenDetails = ably.auth.requestToken(tokenParams, authOptions); + assertNotNull("Expected token value", tokenDetails.token); + assertEquals("Unexpected capability", tokenDetails.capability, requestedCapability.toString()); + } catch (AblyException e) { + e.printStackTrace(); + fail("authcapability10: Unexpected exception"); + } + } - /** - * Invalid capabilities - */ - @Test - public void authinvalid0() { - TokenParams tokenParams = new TokenParams(); - Capability invalidCapability = new Capability(); - invalidCapability.addResource("channel0", "publish_"); - tokenParams.capability = invalidCapability.toString(); - try { - ably.auth.requestToken(tokenParams, null); - fail("Invalid capability, expected rejection"); - } catch(AblyException e) { - assertEquals("Unexpected error code", e.errorInfo.code, 40000); - } - } - @Test - public void authinvalid1() { - TokenParams tokenParams = new TokenParams(); - Capability invalidCapability = new Capability(); - invalidCapability.addResource("channel0", new String[]{"*", "publish"}); - tokenParams.capability = invalidCapability.toString(); - try { - ably.auth.requestToken(tokenParams, null); - fail("Invalid capability, expected rejection"); - } catch(AblyException e) { - assertEquals("Unexpected error code", e.errorInfo.code, 40000); - } - } - @Test - public void authinvalid2() { - TokenParams tokenParams = new TokenParams(); - Capability invalidCapability = new Capability(); - invalidCapability.addResource("channel0", new String[0]); - tokenParams.capability = invalidCapability.toString(); - try { - ably.auth.requestToken(tokenParams, null); - fail("Invalid capability, expected rejection"); - } catch(AblyException e) { - assertEquals("Unexpected error code", e.errorInfo.code, 40000); - } - } + /** + * Invalid capabilities + */ + @Test + public void authinvalid0() { + TokenParams tokenParams = new TokenParams(); + Capability invalidCapability = new Capability(); + invalidCapability.addResource("channel0", "publish_"); + tokenParams.capability = invalidCapability.toString(); + try { + ably.auth.requestToken(tokenParams, null); + fail("Invalid capability, expected rejection"); + } catch(AblyException e) { + assertEquals("Unexpected error code", e.errorInfo.code, 40000); + } + } + @Test + public void authinvalid1() { + TokenParams tokenParams = new TokenParams(); + Capability invalidCapability = new Capability(); + invalidCapability.addResource("channel0", new String[]{"*", "publish"}); + tokenParams.capability = invalidCapability.toString(); + try { + ably.auth.requestToken(tokenParams, null); + fail("Invalid capability, expected rejection"); + } catch(AblyException e) { + assertEquals("Unexpected error code", e.errorInfo.code, 40000); + } + } + @Test + public void authinvalid2() { + TokenParams tokenParams = new TokenParams(); + Capability invalidCapability = new Capability(); + invalidCapability.addResource("channel0", new String[0]); + tokenParams.capability = invalidCapability.toString(); + try { + ably.auth.requestToken(tokenParams, null); + fail("Invalid capability, expected rejection"); + } catch(AblyException e) { + assertEquals("Unexpected error code", e.errorInfo.code, 40000); + } + } } diff --git a/lib/src/test/java/io/ably/lib/test/rest/RestChannelBulkPublishTest.java b/lib/src/test/java/io/ably/lib/test/rest/RestChannelBulkPublishTest.java index 8b3d592f0..62db2a0ff 100644 --- a/lib/src/test/java/io/ably/lib/test/rest/RestChannelBulkPublishTest.java +++ b/lib/src/test/java/io/ably/lib/test/rest/RestChannelBulkPublishTest.java @@ -16,203 +16,203 @@ public class RestChannelBulkPublishTest extends ParameterizedTest { - /** - * Publish a single message on multiple channels - * - * The payload constructed has the form - * [ - * { - * channel: [ , , ... ], - * message: [{ data: }] - * } - * ] - * - * It publishes the given message on all of the given channels. - */ - @Test - public void bulk_publish_multiple_channels_simple() { - try { - /* setup library instance */ - ClientOptions opts = createOptions(testVars.keys[0].keyStr); - AblyRest ably = new AblyRest(opts); - - /* first, publish some messages */ - int channelCount = 5; - ArrayList channelIds = new ArrayList(); - for(int i = 0; i < channelCount; i++) { - channelIds.add("persisted:" + randomString()); - } - - Message message = new Message(null, "bulk_publish_multiple_channels_simple"); - String messageId = message.id = randomString(); - Message.Batch payload = new Message.Batch(channelIds, Collections.singleton(message)); - - PublishResponse[] result = ably.publishBatch(new Message.Batch[] { payload }, null); - for(PublishResponse response : result) { - assertEquals("Verify expected response id", response.messageId, messageId); - assertTrue("Verify expected channel name", channelIds.contains(response.channelId)); - assertNull("Verify no publish error", response.error); - } - - /* get the history for this channel */ - for(String channel : channelIds) { - PaginatedResult messages = ably.channels.get(channel).history(null); - assertNotNull("Expected non-null messages", messages); - assertEquals("Expected 1 message", messages.items().length, 1); - /* verify message contents */ - assertEquals("Expect message data to be expected String", messages.items()[0].data, message.data); - } - } catch (AblyException e) { - e.printStackTrace(); - fail("bulkpublish_multiple_channels_simple: Unexpected exception"); - return; - } - } - - /** - * Publish a multiple messages on multiple channels - * - * The payload constructed has the form - * [ - * { - * channel: [ ], - * message: [ - * { data: }, - * { data: }, - * { data: }, - * ... - * ] - * }, - * { - * channel: [ ], - * message: [ - * { data: }, - * { data: }, - * { data: }, - * ... - * ] - * }, - * ... - * ] - * - * It publishes the given messages on the associated channels. - */ - @Test - public void bulk_publish_multiple_channels_multiple_messages() { - try { - /* setup library instance */ - ClientOptions opts = createOptions(testVars.keys[0].keyStr); - opts.idempotentRestPublishing = true; - AblyRest ably = new AblyRest(opts); - - /* first, publish some messages */ - int channelCount = 5; - int messageCount = 6; - String baseMessageText = "bulk_publish_multiple_channels_multiple_messages"; - ArrayList payload = new ArrayList(); - - ArrayList rndMessageTexts = new ArrayList(); - for(int i = 0; i < messageCount; i++) { - rndMessageTexts.add(randomString()); - } - - ArrayList channelIds = new ArrayList(); - for(int i = 0; i < channelCount; i++) { - String channel = "persisted:" + randomString(); - channelIds.add(channel); - ArrayList messages = new ArrayList(); - for(int j = 0; j < messageCount; j++) { - messages.add(new Message(null, baseMessageText + '-' + channel + '-' + rndMessageTexts.get(j))); - } - payload.add(new Message.Batch(Collections.singleton(channel), messages)); - } - - PublishResponse[] result = ably.publishBatch(payload.toArray(new Message.Batch[payload.size()]), null); - for(PublishResponse response : result) { - assertNotNull("Verify expected response id", response.messageId); - assertTrue("Verify expected channel name", channelIds.contains(response.channelId)); - assertNull("Verify no publish error", response.error); - } - - /* get the history for this channel */ - for(String channel : channelIds) { - PaginatedResult messages = ably.channels.get(channel).history(new Param[] {new Param("direction", "forwards")}); - assertNotNull("Expected non-null messages", messages); - assertEquals("Expected correct number of messages", messages.items().length, messageCount); - /* verify message contents */ - for(int i = 0; i < messageCount; i++) { - assertEquals("Expect message data to be expected String", messages.items()[i].data, baseMessageText + '-' + channel + '-' + rndMessageTexts.get(i)); - } - } - } catch (AblyException e) { - e.printStackTrace(); - fail("bulk_publish_multiple_channels_multiple_messages: Unexpected exception"); - return; - } - } - - /** - * Publish a single message on multiple channels, using credentials - * that are only able to publish to a subset of the channels - * - * The payload constructed has the form - * [ - * { - * channel: [ , , ... ], - * message: [{ data: }] - * } - * ] - * - * It attempts to publish the given message on all of the given channels. - */ - @Ignore // awaiting channel member in error responses - @Test - public void bulk_publish_multiple_channels_partial_error() { - try { - /* setup library instance */ - ClientOptions opts = createOptions(testVars.keys[6].keyStr); - AblyRest ably = new AblyRest(opts); - - /* first, publish some messages */ - String baseChannelName = "persisted:" + testParams.name + ":channel"; - int channelCount = 5; - ArrayList channelIds = new ArrayList(); - for(int i = 0; i < channelCount; i++) { - channelIds.add(baseChannelName + i); - } - - Message message = new Message(null, "bulk_publish_multiple_channels_partial_error"); - String messageId = message.id = randomString(); - Message.Batch payload = new Message.Batch(channelIds, Collections.singleton(message)); - - PublishResponse[] result = ably.publishBatch(new Message.Batch[] { payload }, null); - for(PublishResponse response : result) { - if((baseChannelName + "1").compareTo(response.channelId) >= 0) { - assertEquals("Verify expected response id", response.messageId, messageId); - assertTrue("Verify expected channel name", channelIds.contains(response.channelId)); - assertNull("Verify no publish error", response.error); - } else { - assertNotNull("Verify expected publish error", response.error); - assertEquals("Verify expected publish error code", response.error.code, 40160); - } - } - - /* get the history for this channel */ - for(String channel : channelIds) { - if((baseChannelName + "1").compareTo(channel) >= 0) { - PaginatedResult messages = ably.channels.get(channel).history(null); - assertNotNull("Expected non-null messages", messages); - assertEquals("Expected 1 message", messages.items().length, 1); - /* verify message contents */ - assertEquals("Expect message data to be expected String", messages.items()[0].data, message.data); - } - } - } catch (AblyException e) { - e.printStackTrace(); - fail("bulkpublish_multiple_channels_simple: Unexpected exception"); - return; - } - } - - private String randomString() { return String.valueOf(new Random().nextDouble()).substring(2); } + /** + * Publish a single message on multiple channels + * + * The payload constructed has the form + * [ + * { + * channel: [ , , ... ], + * message: [{ data: }] + * } + * ] + * + * It publishes the given message on all of the given channels. + */ + @Test + public void bulk_publish_multiple_channels_simple() { + try { + /* setup library instance */ + ClientOptions opts = createOptions(testVars.keys[0].keyStr); + AblyRest ably = new AblyRest(opts); + + /* first, publish some messages */ + int channelCount = 5; + ArrayList channelIds = new ArrayList(); + for(int i = 0; i < channelCount; i++) { + channelIds.add("persisted:" + randomString()); + } + + Message message = new Message(null, "bulk_publish_multiple_channels_simple"); + String messageId = message.id = randomString(); + Message.Batch payload = new Message.Batch(channelIds, Collections.singleton(message)); + + PublishResponse[] result = ably.publishBatch(new Message.Batch[] { payload }, null); + for(PublishResponse response : result) { + assertEquals("Verify expected response id", response.messageId, messageId); + assertTrue("Verify expected channel name", channelIds.contains(response.channelId)); + assertNull("Verify no publish error", response.error); + } + + /* get the history for this channel */ + for(String channel : channelIds) { + PaginatedResult messages = ably.channels.get(channel).history(null); + assertNotNull("Expected non-null messages", messages); + assertEquals("Expected 1 message", messages.items().length, 1); + /* verify message contents */ + assertEquals("Expect message data to be expected String", messages.items()[0].data, message.data); + } + } catch (AblyException e) { + e.printStackTrace(); + fail("bulkpublish_multiple_channels_simple: Unexpected exception"); + return; + } + } + + /** + * Publish a multiple messages on multiple channels + * + * The payload constructed has the form + * [ + * { + * channel: [ ], + * message: [ + * { data: }, + * { data: }, + * { data: }, + * ... + * ] + * }, + * { + * channel: [ ], + * message: [ + * { data: }, + * { data: }, + * { data: }, + * ... + * ] + * }, + * ... + * ] + * + * It publishes the given messages on the associated channels. + */ + @Test + public void bulk_publish_multiple_channels_multiple_messages() { + try { + /* setup library instance */ + ClientOptions opts = createOptions(testVars.keys[0].keyStr); + opts.idempotentRestPublishing = true; + AblyRest ably = new AblyRest(opts); + + /* first, publish some messages */ + int channelCount = 5; + int messageCount = 6; + String baseMessageText = "bulk_publish_multiple_channels_multiple_messages"; + ArrayList payload = new ArrayList(); + + ArrayList rndMessageTexts = new ArrayList(); + for(int i = 0; i < messageCount; i++) { + rndMessageTexts.add(randomString()); + } + + ArrayList channelIds = new ArrayList(); + for(int i = 0; i < channelCount; i++) { + String channel = "persisted:" + randomString(); + channelIds.add(channel); + ArrayList messages = new ArrayList(); + for(int j = 0; j < messageCount; j++) { + messages.add(new Message(null, baseMessageText + '-' + channel + '-' + rndMessageTexts.get(j))); + } + payload.add(new Message.Batch(Collections.singleton(channel), messages)); + } + + PublishResponse[] result = ably.publishBatch(payload.toArray(new Message.Batch[payload.size()]), null); + for(PublishResponse response : result) { + assertNotNull("Verify expected response id", response.messageId); + assertTrue("Verify expected channel name", channelIds.contains(response.channelId)); + assertNull("Verify no publish error", response.error); + } + + /* get the history for this channel */ + for(String channel : channelIds) { + PaginatedResult messages = ably.channels.get(channel).history(new Param[] {new Param("direction", "forwards")}); + assertNotNull("Expected non-null messages", messages); + assertEquals("Expected correct number of messages", messages.items().length, messageCount); + /* verify message contents */ + for(int i = 0; i < messageCount; i++) { + assertEquals("Expect message data to be expected String", messages.items()[i].data, baseMessageText + '-' + channel + '-' + rndMessageTexts.get(i)); + } + } + } catch (AblyException e) { + e.printStackTrace(); + fail("bulk_publish_multiple_channels_multiple_messages: Unexpected exception"); + return; + } + } + + /** + * Publish a single message on multiple channels, using credentials + * that are only able to publish to a subset of the channels + * + * The payload constructed has the form + * [ + * { + * channel: [ , , ... ], + * message: [{ data: }] + * } + * ] + * + * It attempts to publish the given message on all of the given channels. + */ + @Ignore // awaiting channel member in error responses + @Test + public void bulk_publish_multiple_channels_partial_error() { + try { + /* setup library instance */ + ClientOptions opts = createOptions(testVars.keys[6].keyStr); + AblyRest ably = new AblyRest(opts); + + /* first, publish some messages */ + String baseChannelName = "persisted:" + testParams.name + ":channel"; + int channelCount = 5; + ArrayList channelIds = new ArrayList(); + for(int i = 0; i < channelCount; i++) { + channelIds.add(baseChannelName + i); + } + + Message message = new Message(null, "bulk_publish_multiple_channels_partial_error"); + String messageId = message.id = randomString(); + Message.Batch payload = new Message.Batch(channelIds, Collections.singleton(message)); + + PublishResponse[] result = ably.publishBatch(new Message.Batch[] { payload }, null); + for(PublishResponse response : result) { + if((baseChannelName + "1").compareTo(response.channelId) >= 0) { + assertEquals("Verify expected response id", response.messageId, messageId); + assertTrue("Verify expected channel name", channelIds.contains(response.channelId)); + assertNull("Verify no publish error", response.error); + } else { + assertNotNull("Verify expected publish error", response.error); + assertEquals("Verify expected publish error code", response.error.code, 40160); + } + } + + /* get the history for this channel */ + for(String channel : channelIds) { + if((baseChannelName + "1").compareTo(channel) >= 0) { + PaginatedResult messages = ably.channels.get(channel).history(null); + assertNotNull("Expected non-null messages", messages); + assertEquals("Expected 1 message", messages.items().length, 1); + /* verify message contents */ + assertEquals("Expect message data to be expected String", messages.items()[0].data, message.data); + } + } + } catch (AblyException e) { + e.printStackTrace(); + fail("bulkpublish_multiple_channels_simple: Unexpected exception"); + return; + } + } + + private String randomString() { return String.valueOf(new Random().nextDouble()).substring(2); } } diff --git a/lib/src/test/java/io/ably/lib/test/rest/RestChannelHistoryTest.java b/lib/src/test/java/io/ably/lib/test/rest/RestChannelHistoryTest.java index ad2b0caf1..b133773e7 100644 --- a/lib/src/test/java/io/ably/lib/test/rest/RestChannelHistoryTest.java +++ b/lib/src/test/java/io/ably/lib/test/rest/RestChannelHistoryTest.java @@ -21,593 +21,593 @@ public class RestChannelHistoryTest extends ParameterizedTest { - private AblyRest ably; - private long timeOffset; - - @Rule - public Timeout testTimeout = Timeout.seconds(300); - - @Before - public void setUpBefore() throws Exception { - ClientOptions opts = createOptions(testVars.keys[0].keyStr); - opts.useBinaryProtocol = false; - ably = new AblyRest(opts); - long timeFromService = ably.time(); - timeOffset = timeFromService - System.currentTimeMillis(); - } - - /** - * Publish events with data of various datatypes - */ - @Test - public void channelhistory_types() { - /* first, publish some messages */ - Channel history0 = ably.channels.get("persisted:channelhistory_types_" + UUID.randomUUID().toString() + "_" + testParams.name); - try { - history0.publish("history0", "This is a string message payload"); - history0.publish("history1", "This is a byte[] message payload".getBytes()); - } catch(AblyException e) { - e.printStackTrace(); - fail("channelhistory0: Unexpected exception"); - return; - } - - /* get the history for this channel */ - try { - PaginatedResult messages = history0.history(null); - assertNotNull("Expected non-null messages", messages); - assertEquals("Expected 2 messages", messages.items().length, 2); - HashMap messageContents = new HashMap(); - /* verify message contents */ - for(Message message : messages.items()) - messageContents.put(message.name, message); - assertEquals("Expect history0 to be expected String", messageContents.get("history0").data, "This is a string message payload"); - assertEquals("Expect history1 to be expected byte[]", new String((byte[])messageContents.get("history1").data), "This is a byte[] message payload"); - /* verify message order */ - Message[] expectedMessageHistory = new Message[]{ - messageContents.get("history1"), - messageContents.get("history0") - }; - Assert.assertArrayEquals("Expect messages in reverse order", messages.items(), expectedMessageHistory); - } catch (AblyException e) { - e.printStackTrace(); - fail("channelhistory0: Unexpected exception"); - return; - } - } - - /** - * Publish events and check expected order (forwards) - */ - @Test - public void channelhistory_multi_50_f() { - /* first, publish some messages */ - Channel history1 = ably.channels.get("persisted:channelhistory_multi_50_f_" + testParams.name); - for(int i = 0; i < 50; i++) - try { - history1.publish("history" + i, String.valueOf(i)); - } catch(AblyException e) { - e.printStackTrace(); - fail("channelhistory1: Unexpected exception"); - return; - } - - /* get the history for this channel */ - try { - PaginatedResult messages = history1.history(new Param[] { new Param("direction", "forwards") }); - assertNotNull("Expected non-null messages", messages); - assertEquals("Expected 50 messages", messages.items().length, 50); - HashMap messageContents = new HashMap(); - for(Message message : messages.items()) - messageContents.put(message.name, message); - /* verify message order */ - Message[] expectedMessageHistory = new Message[50]; - for(int i = 0; i < 50; i++) - expectedMessageHistory[i] = messageContents.get("history" + i); - Assert.assertArrayEquals("Expect messages in forward order", messages.items(), expectedMessageHistory); - } catch (AblyException e) { - e.printStackTrace(); - fail("channelhistory1: Unexpected exception"); - return; - } - } - - /** - * Publish events and check expected order (backwards) - */ - @Test - public void channelhistory_multi_50_b() { - /* first, publish some messages */ - Channel history2 = ably.channels.get("persisted:channelhistory_multi_50_b_" + testParams.name); - for(int i = 0; i < 50; i++) - try { - history2.publish("history" + i, String.valueOf(i)); - } catch(AblyException e) { - e.printStackTrace(); - fail("channelhistory2: Unexpected exception"); - return; - } - - /* get the history for this channel */ - try { - PaginatedResult messages = history2.history(new Param[] { new Param("direction", "backwards") }); - assertNotNull("Expected non-null messages", messages); - assertEquals("Expected 50 messages", messages.items().length, 50); - HashMap messageContents = new HashMap(); - for(Message message : messages.items()) - messageContents.put(message.name, message); - /* verify message order */ - Message[] expectedMessageHistory = new Message[50]; - for(int i = 0; i < 50; i++) - expectedMessageHistory[i] = messageContents.get("history" + (49 - i)); - Assert.assertArrayEquals("Expect messages in reverse order", messages.items(), expectedMessageHistory); - } catch (AblyException e) { - e.printStackTrace(); - fail("channelhistory2: Unexpected exception"); - return; - } - } - - /** - * Publish events, get limited history and check expected order (forwards) - */ - @Test - public void channelhistory_limit_f() { - /* first, publish some messages */ - Channel history3 = ably.channels.get("persisted:channelhistory_limit_f_" + testParams.name); - for(int i = 0; i < 50; i++) - try { - history3.publish("history" + i, String.valueOf(i)); - } catch(AblyException e) { - e.printStackTrace(); - fail("channelhistory3: Unexpected exception"); - return; - } - - /* get the history for this channel */ - try { - PaginatedResult messages = history3.history(new Param[] { new Param("direction", "forwards"), new Param("limit", "25") }); - assertNotNull("Expected non-null messages", messages); - assertEquals("Expected 25 messages", messages.items().length, 25); - HashMap messageContents = new HashMap(); - for(Message message : messages.items()) - messageContents.put(message.name, message); - /* verify message order */ - Message[] expectedMessageHistory = new Message[25]; - for(int i = 0; i < 25; i++) - expectedMessageHistory[i] = messageContents.get("history" + i); - Assert.assertArrayEquals("Expect messages in forward order", messages.items(), expectedMessageHistory); - } catch (AblyException e) { - e.printStackTrace(); - fail("channelhistory3: Unexpected exception"); - return; - } - } - - /** - * Publish events, get limited history and check expected order (backwards) - */ - @Test - public void channelhistory_limit_b() { - /* first, publish some messages */ - Channel history4 = ably.channels.get("persisted:channelhistory_limit_b_" + testParams.name); - for(int i = 0; i < 50; i++) - try { - history4.publish("history" + i, String.valueOf(i)); - } catch(AblyException e) { - e.printStackTrace(); - fail("channelhistory4: Unexpected exception"); - return; - } - - /* get the history for this channel */ - try { - PaginatedResult messages = history4.history(new Param[] { new Param("direction", "backwards"), new Param("limit", "25") }); - assertNotNull("Expected non-null messages", messages); - assertEquals("Expected 25 messages", messages.items().length, 25); - HashMap messageContents = new HashMap(); - for(Message message : messages.items()) - messageContents.put(message.name, message); - /* verify message order */ - Message[] expectedMessageHistory = new Message[25]; - for(int i = 0; i < 25; i++) - expectedMessageHistory[i] = messageContents.get("history" + (49 - i)); - Assert.assertArrayEquals("Expect messages in forward order", messages.items(), expectedMessageHistory); - } catch (AblyException e) { - e.printStackTrace(); - fail("channelhistory4: Unexpected exception"); - return; - } - } - - /** - * Publish events and check expected history based on time slice (forwards) - */ - @Test - @Ignore("Fails in sandbox due to timing issues") - public void channelhistory_time_f() { - /* first, publish some messages */ - long intervalStart = 0, intervalEnd = 0; - Channel history5 = ably.channels.get("persisted:channelhistory_time_f_" + UUID.randomUUID().toString() + "_" + testParams.name); - /* send batches of messages with short inter-message delay */ - try { - for(int i = 0; i < 20; i++) { - history5.publish("history" + i, String.valueOf(i)); - Thread.sleep(100L); - } - Thread.sleep(1000L); - intervalStart = timeOffset + System.currentTimeMillis(); - for(int i = 20; i < 40; i++) { - history5.publish("history" + i, String.valueOf(i)); - Thread.sleep(100L); - } - intervalEnd = timeOffset + System.currentTimeMillis() - 1; - Thread.sleep(1000L); - for(int i = 40; i < 60; i++) { - history5.publish("history" + i, String.valueOf(i)); - Thread.sleep(100L); - } - } catch(AblyException e) { - e.printStackTrace(); - fail("channelhistory1: Unexpected exception"); - return; - } catch (InterruptedException e) { - e.printStackTrace(); - fail("channelhistory1: Unexpected exception"); - } - - /* get the history for this channel */ - try { - PaginatedResult messages = history5.history(new Param[] { - new Param("direction", "forwards"), - new Param("start", String.valueOf(intervalStart - 500)), - new Param("end", String.valueOf(intervalEnd + 500)) - }); - assertNotNull("Expected non-null messages", messages); - assertEquals("Expected 20 messages", messages.items().length, 20); - HashMap messageContents = new HashMap(); - for(Message message : messages.items()) - messageContents.put(message.name, message); - /* verify message order */ - Message[] expectedMessageHistory = new Message[20]; - for(int i = 20; i < 40; i++) - expectedMessageHistory[i - 20] = messageContents.get("history" + i); - Assert.assertArrayEquals("Expect messages in forward order", messages.items(), expectedMessageHistory); - } catch (AblyException e) { - e.printStackTrace(); - fail("channelhistory5: Unexpected exception"); - return; - } - } - - /** - * Publish events and check expected history based on time slice (backwards) - */ - @Test - public void channelhistory_time_b() { - /* first, publish some messages */ - long intervalStart = 0, intervalEnd = 0; - Channel history6 = ably.channels.get("persisted:channelhistory_time_b_" + testParams.name); - /* send batches of messages with shprt inter-message delay */ - try { - for(int i = 0; i < 20; i++) { - history6.publish("history" + i, String.valueOf(i)); - Thread.sleep(100L); - } - Thread.sleep(1000L); - intervalStart = timeOffset + System.currentTimeMillis(); - for(int i = 20; i < 40; i++) { - history6.publish("history" + i, String.valueOf(i)); - Thread.sleep(100L); - } - intervalEnd = timeOffset + System.currentTimeMillis() - 1; - Thread.sleep(1000L); - for(int i = 40; i < 60; i++) { - history6.publish("history" + i, String.valueOf(i)); - Thread.sleep(100L); - } - } catch(AblyException e) { - e.printStackTrace(); - fail("channelhistory6: Unexpected exception"); - return; - } catch (InterruptedException e) { - e.printStackTrace(); - fail("channelhistory6: Unexpected exception"); - } - - /* get the history for this channel */ - try { - PaginatedResult messages = history6.history(new Param[] { - new Param("direction", "backwards"), - new Param("start", String.valueOf(intervalStart - 500)), - new Param("end", String.valueOf(intervalEnd + 500)) - }); - assertNotNull("Expected non-null messages", messages); - assertEquals("Expected 20 messages", messages.items().length, 20); - HashMap messageContents = new HashMap(); - for(Message message : messages.items()) - messageContents.put(message.name, message); - /* verify message order */ - Message[] expectedMessageHistory = new Message[20]; - for(int i = 20; i < 40; i++) - expectedMessageHistory[i - 20] = messageContents.get("history" + (59 - i)); - Assert.assertArrayEquals("Expect messages in backwards order", messages.items(), expectedMessageHistory); - } catch (AblyException e) { - e.printStackTrace(); - fail("channelhistory6: Unexpected exception"); - return; - } - } - - /** - * Check query pagination (forwards) - */ - @Test - public void channelhistory_paginate_f() { - /* first, publish some messages */ - Channel history3 = ably.channels.get("persisted:channelhistory_paginate_f_" + testParams.name); - for(int i = 0; i < 50; i++) - try { - history3.publish("history" + i, String.valueOf(i)); - } catch(AblyException e) { - e.printStackTrace(); - fail("channelhistory3: Unexpected exception"); - return; - } - - /* get the history for this channel */ - try { - PaginatedResult messages = history3.history(new Param[] { new Param("direction", "forwards"), new Param("limit", "10") }); - assertNotNull("Expected non-null messages", messages); - assertEquals("Expected 10 messages", messages.items().length, 10); - - /* log all messages */ - HashMap messageContents = new HashMap(); - for(Message message : messages.items()) - messageContents.put(message.name, message); - - /* verify message order */ - Message[] expectedMessageHistory = new Message[10]; - for(int i = 0; i < 10; i++) - expectedMessageHistory[i] = messageContents.get("history" + i); - Assert.assertArrayEquals("Expect messages in forward order", messages.items(), expectedMessageHistory); - - /* get next page */ - messages = messages.next(); - assertNotNull("Expected non-null messages", messages); - assertEquals("Expected 10 messages", messages.items().length, 10); - - /* log all messages */ - for(Message message : messages.items()) - messageContents.put(message.name, message); - - /* verify message order */ - for(int i = 0; i < 10; i++) - expectedMessageHistory[i] = messageContents.get("history" + String.valueOf(i + 10)); - Assert.assertArrayEquals("Expect messages in forward order", messages.items(), expectedMessageHistory); - - /* get next page */ - messages = messages.next(); - assertNotNull("Expected non-null messages", messages); - assertEquals("Expected 10 messages", messages.items().length, 10); - - /* log all messages */ - for(Message message : messages.items()) - messageContents.put(message.name, message); - - /* verify message order */ - for(int i = 0; i < 10; i++) - expectedMessageHistory[i] = messageContents.get("history" + String.valueOf(i + 20)); - Assert.assertArrayEquals("Expect messages in forward order", messages.items(), expectedMessageHistory); - - } catch (AblyException e) { - e.printStackTrace(); - fail("channelhistory3: Unexpected exception"); - return; - } - } - - /** - * Check query pagination (backwards) - */ - @Test - public void channelhistory_paginate_b() { - /* first, publish some messages */ - Channel history3 = ably.channels.get("persisted:channelhistory_paginate_b_" + testParams.name); - for(int i = 0; i < 50; i++) - try { - history3.publish("history" + i, String.valueOf(i)); - } catch(AblyException e) { - e.printStackTrace(); - fail("channelhistory3: Unexpected exception"); - return; - } - - /* get the history for this channel */ - try { - PaginatedResult messages = history3.history(new Param[] { new Param("direction", "backwards"), new Param("limit", "10") }); - assertNotNull("Expected non-null messages", messages); - assertEquals("Expected 10 messages", messages.items().length, 10); - - /* log all messages */ - HashMap messageContents = new HashMap(); - for(Message message : messages.items()) - messageContents.put(message.name, message); - - /* verify message order */ - Message[] expectedMessageHistory = new Message[10]; - for(int i = 0; i < 10; i++) - expectedMessageHistory[i] = messageContents.get("history" + String.valueOf(49 - i)); - Assert.assertArrayEquals("Expect messages in forward order", messages.items(), expectedMessageHistory); - - /* get next page */ - messages = messages.next(); - assertNotNull("Expected non-null messages", messages); - assertEquals("Expected 10 messages", messages.items().length, 10); - - /* log all messages */ - for(Message message : messages.items()) - messageContents.put(message.name, message); - - /* verify message order */ - for(int i = 0; i < 10; i++) - expectedMessageHistory[i] = messageContents.get("history" + String.valueOf(39 - i)); - Assert.assertArrayEquals("Expect messages in forward order", messages.items(), expectedMessageHistory); - - /* get next page */ - messages = messages.next(); - assertNotNull("Expected non-null messages", messages); - assertEquals("Expected 10 messages", messages.items().length, 10); - - /* log all messages */ - for(Message message : messages.items()) - messageContents.put(message.name, message); - - /* verify message order */ - for(int i = 0; i < 10; i++) - expectedMessageHistory[i] = messageContents.get("history" + String.valueOf(29 - i)); - Assert.assertArrayEquals("Expect messages in forward order", messages.items(), expectedMessageHistory); - - } catch (AblyException e) { - e.printStackTrace(); - fail("channelhistory3: Unexpected exception"); - return; - } - } - - /** - * Check query pagination "rel=first" (forwards) - */ - @Test - public void channelhistory_paginate_first_f() { - /* first, publish some messages */ - Channel history3 = ably.channels.get("persisted:channelhistory_paginate_first_f_" + testParams.name); - for(int i = 0; i < 50; i++) - try { - history3.publish("history" + i, String.valueOf(i)); - } catch(AblyException e) { - e.printStackTrace(); - fail("channelhistory3: Unexpected exception"); - return; - } - - /* get the history for this channel */ - try { - PaginatedResult messages = history3.history(new Param[] { new Param("direction", "forwards"), new Param("limit", "10") }); - assertNotNull("Expected non-null messages", messages); - assertEquals("Expected 10 messages", messages.items().length, 10); - - /* log all messages */ - HashMap messageContents = new HashMap(); - for(Message message : messages.items()) - messageContents.put(message.name, message); - - /* verify message order */ - Message[] expectedMessageHistory = new Message[10]; - for(int i = 0; i < 10; i++) - expectedMessageHistory[i] = messageContents.get("history" + i); - Assert.assertArrayEquals("Expect messages in forward order", messages.items(), expectedMessageHistory); - - /* get next page */ - messages = messages.next(); - assertNotNull("Expected non-null messages", messages); - assertEquals("Expected 10 messages", messages.items().length, 10); - - /* log all messages */ - for(Message message : messages.items()) - messageContents.put(message.name, message); - - /* verify message order */ - for(int i = 0; i < 10; i++) - expectedMessageHistory[i] = messageContents.get("history" + String.valueOf(i + 10)); - Assert.assertArrayEquals("Expect messages in forward order", messages.items(), expectedMessageHistory); - - /* get first page */ - messages = messages.first(); - assertNotNull("Expected non-null messages", messages); - assertEquals("Expected 10 messages", messages.items().length, 10); - - /* log all messages */ - for(Message message : messages.items()) - messageContents.put(message.name, message); - - /* verify message order */ - for(int i = 0; i < 10; i++) - expectedMessageHistory[i] = messageContents.get("history" + String.valueOf(i)); - Assert.assertArrayEquals("Expect messages in forward order", messages.items(), expectedMessageHistory); - - } catch (AblyException e) { - e.printStackTrace(); - fail("channelhistory3: Unexpected exception"); - return; - } - } - - /** - * Check query pagination "rel=first" (backwards) - */ - @Test - public void channelhistory_paginate_first_b() { - /* first, publish some messages */ - Channel history3 = ably.channels.get("persisted:channelhistory_paginate_first_b_" + testParams.name); - for(int i = 0; i < 50; i++) - try { - history3.publish("history" + i, String.valueOf(i)); - } catch(AblyException e) { - e.printStackTrace(); - fail("channelhistory3: Unexpected exception"); - return; - } - - /* get the history for this channel */ - try { - PaginatedResult messages = history3.history(new Param[] { new Param("direction", "backwards"), new Param("limit", "10") }); - assertNotNull("Expected non-null messages", messages); - assertEquals("Expected 10 messages", messages.items().length, 10); - - /* log all messages */ - HashMap messageContents = new HashMap(); - for(Message message : messages.items()) - messageContents.put(message.name, message); - - /* verify message order */ - Message[] expectedMessageHistory = new Message[10]; - for(int i = 0; i < 10; i++) - expectedMessageHistory[i] = messageContents.get("history" + String.valueOf(49 - i)); - Assert.assertArrayEquals("Expect messages in forward order", messages.items(), expectedMessageHistory); - - /* get next page */ - messages = messages.next(); - assertNotNull("Expected non-null messages", messages); - assertEquals("Expected 10 messages", messages.items().length, 10); - - /* log all messages */ - for(Message message : messages.items()) - messageContents.put(message.name, message); - - /* verify message order */ - for(int i = 0; i < 10; i++) - expectedMessageHistory[i] = messageContents.get("history" + String.valueOf(39 - i)); - Assert.assertArrayEquals("Expect messages in forward order", messages.items(), expectedMessageHistory); - - /* get first page */ - messages = messages.first(); - assertNotNull("Expected non-null messages", messages); - assertEquals("Expected 10 messages", messages.items().length, 10); - - /* log all messages */ - for(Message message : messages.items()) - messageContents.put(message.name, message); - - /* verify message order */ - for(int i = 0; i < 10; i++) - expectedMessageHistory[i] = messageContents.get("history" + String.valueOf(49 - i)); - Assert.assertArrayEquals("Expect messages in forward order", messages.items(), expectedMessageHistory); - - } catch (AblyException e) { - e.printStackTrace(); - fail("channelhistory3: Unexpected exception"); - return; - } - } + private AblyRest ably; + private long timeOffset; + + @Rule + public Timeout testTimeout = Timeout.seconds(300); + + @Before + public void setUpBefore() throws Exception { + ClientOptions opts = createOptions(testVars.keys[0].keyStr); + opts.useBinaryProtocol = false; + ably = new AblyRest(opts); + long timeFromService = ably.time(); + timeOffset = timeFromService - System.currentTimeMillis(); + } + + /** + * Publish events with data of various datatypes + */ + @Test + public void channelhistory_types() { + /* first, publish some messages */ + Channel history0 = ably.channels.get("persisted:channelhistory_types_" + UUID.randomUUID().toString() + "_" + testParams.name); + try { + history0.publish("history0", "This is a string message payload"); + history0.publish("history1", "This is a byte[] message payload".getBytes()); + } catch(AblyException e) { + e.printStackTrace(); + fail("channelhistory0: Unexpected exception"); + return; + } + + /* get the history for this channel */ + try { + PaginatedResult messages = history0.history(null); + assertNotNull("Expected non-null messages", messages); + assertEquals("Expected 2 messages", messages.items().length, 2); + HashMap messageContents = new HashMap(); + /* verify message contents */ + for(Message message : messages.items()) + messageContents.put(message.name, message); + assertEquals("Expect history0 to be expected String", messageContents.get("history0").data, "This is a string message payload"); + assertEquals("Expect history1 to be expected byte[]", new String((byte[])messageContents.get("history1").data), "This is a byte[] message payload"); + /* verify message order */ + Message[] expectedMessageHistory = new Message[]{ + messageContents.get("history1"), + messageContents.get("history0") + }; + Assert.assertArrayEquals("Expect messages in reverse order", messages.items(), expectedMessageHistory); + } catch (AblyException e) { + e.printStackTrace(); + fail("channelhistory0: Unexpected exception"); + return; + } + } + + /** + * Publish events and check expected order (forwards) + */ + @Test + public void channelhistory_multi_50_f() { + /* first, publish some messages */ + Channel history1 = ably.channels.get("persisted:channelhistory_multi_50_f_" + testParams.name); + for(int i = 0; i < 50; i++) + try { + history1.publish("history" + i, String.valueOf(i)); + } catch(AblyException e) { + e.printStackTrace(); + fail("channelhistory1: Unexpected exception"); + return; + } + + /* get the history for this channel */ + try { + PaginatedResult messages = history1.history(new Param[] { new Param("direction", "forwards") }); + assertNotNull("Expected non-null messages", messages); + assertEquals("Expected 50 messages", messages.items().length, 50); + HashMap messageContents = new HashMap(); + for(Message message : messages.items()) + messageContents.put(message.name, message); + /* verify message order */ + Message[] expectedMessageHistory = new Message[50]; + for(int i = 0; i < 50; i++) + expectedMessageHistory[i] = messageContents.get("history" + i); + Assert.assertArrayEquals("Expect messages in forward order", messages.items(), expectedMessageHistory); + } catch (AblyException e) { + e.printStackTrace(); + fail("channelhistory1: Unexpected exception"); + return; + } + } + + /** + * Publish events and check expected order (backwards) + */ + @Test + public void channelhistory_multi_50_b() { + /* first, publish some messages */ + Channel history2 = ably.channels.get("persisted:channelhistory_multi_50_b_" + testParams.name); + for(int i = 0; i < 50; i++) + try { + history2.publish("history" + i, String.valueOf(i)); + } catch(AblyException e) { + e.printStackTrace(); + fail("channelhistory2: Unexpected exception"); + return; + } + + /* get the history for this channel */ + try { + PaginatedResult messages = history2.history(new Param[] { new Param("direction", "backwards") }); + assertNotNull("Expected non-null messages", messages); + assertEquals("Expected 50 messages", messages.items().length, 50); + HashMap messageContents = new HashMap(); + for(Message message : messages.items()) + messageContents.put(message.name, message); + /* verify message order */ + Message[] expectedMessageHistory = new Message[50]; + for(int i = 0; i < 50; i++) + expectedMessageHistory[i] = messageContents.get("history" + (49 - i)); + Assert.assertArrayEquals("Expect messages in reverse order", messages.items(), expectedMessageHistory); + } catch (AblyException e) { + e.printStackTrace(); + fail("channelhistory2: Unexpected exception"); + return; + } + } + + /** + * Publish events, get limited history and check expected order (forwards) + */ + @Test + public void channelhistory_limit_f() { + /* first, publish some messages */ + Channel history3 = ably.channels.get("persisted:channelhistory_limit_f_" + testParams.name); + for(int i = 0; i < 50; i++) + try { + history3.publish("history" + i, String.valueOf(i)); + } catch(AblyException e) { + e.printStackTrace(); + fail("channelhistory3: Unexpected exception"); + return; + } + + /* get the history for this channel */ + try { + PaginatedResult messages = history3.history(new Param[] { new Param("direction", "forwards"), new Param("limit", "25") }); + assertNotNull("Expected non-null messages", messages); + assertEquals("Expected 25 messages", messages.items().length, 25); + HashMap messageContents = new HashMap(); + for(Message message : messages.items()) + messageContents.put(message.name, message); + /* verify message order */ + Message[] expectedMessageHistory = new Message[25]; + for(int i = 0; i < 25; i++) + expectedMessageHistory[i] = messageContents.get("history" + i); + Assert.assertArrayEquals("Expect messages in forward order", messages.items(), expectedMessageHistory); + } catch (AblyException e) { + e.printStackTrace(); + fail("channelhistory3: Unexpected exception"); + return; + } + } + + /** + * Publish events, get limited history and check expected order (backwards) + */ + @Test + public void channelhistory_limit_b() { + /* first, publish some messages */ + Channel history4 = ably.channels.get("persisted:channelhistory_limit_b_" + testParams.name); + for(int i = 0; i < 50; i++) + try { + history4.publish("history" + i, String.valueOf(i)); + } catch(AblyException e) { + e.printStackTrace(); + fail("channelhistory4: Unexpected exception"); + return; + } + + /* get the history for this channel */ + try { + PaginatedResult messages = history4.history(new Param[] { new Param("direction", "backwards"), new Param("limit", "25") }); + assertNotNull("Expected non-null messages", messages); + assertEquals("Expected 25 messages", messages.items().length, 25); + HashMap messageContents = new HashMap(); + for(Message message : messages.items()) + messageContents.put(message.name, message); + /* verify message order */ + Message[] expectedMessageHistory = new Message[25]; + for(int i = 0; i < 25; i++) + expectedMessageHistory[i] = messageContents.get("history" + (49 - i)); + Assert.assertArrayEquals("Expect messages in forward order", messages.items(), expectedMessageHistory); + } catch (AblyException e) { + e.printStackTrace(); + fail("channelhistory4: Unexpected exception"); + return; + } + } + + /** + * Publish events and check expected history based on time slice (forwards) + */ + @Test + @Ignore("Fails in sandbox due to timing issues") + public void channelhistory_time_f() { + /* first, publish some messages */ + long intervalStart = 0, intervalEnd = 0; + Channel history5 = ably.channels.get("persisted:channelhistory_time_f_" + UUID.randomUUID().toString() + "_" + testParams.name); + /* send batches of messages with short inter-message delay */ + try { + for(int i = 0; i < 20; i++) { + history5.publish("history" + i, String.valueOf(i)); + Thread.sleep(100L); + } + Thread.sleep(1000L); + intervalStart = timeOffset + System.currentTimeMillis(); + for(int i = 20; i < 40; i++) { + history5.publish("history" + i, String.valueOf(i)); + Thread.sleep(100L); + } + intervalEnd = timeOffset + System.currentTimeMillis() - 1; + Thread.sleep(1000L); + for(int i = 40; i < 60; i++) { + history5.publish("history" + i, String.valueOf(i)); + Thread.sleep(100L); + } + } catch(AblyException e) { + e.printStackTrace(); + fail("channelhistory1: Unexpected exception"); + return; + } catch (InterruptedException e) { + e.printStackTrace(); + fail("channelhistory1: Unexpected exception"); + } + + /* get the history for this channel */ + try { + PaginatedResult messages = history5.history(new Param[] { + new Param("direction", "forwards"), + new Param("start", String.valueOf(intervalStart - 500)), + new Param("end", String.valueOf(intervalEnd + 500)) + }); + assertNotNull("Expected non-null messages", messages); + assertEquals("Expected 20 messages", messages.items().length, 20); + HashMap messageContents = new HashMap(); + for(Message message : messages.items()) + messageContents.put(message.name, message); + /* verify message order */ + Message[] expectedMessageHistory = new Message[20]; + for(int i = 20; i < 40; i++) + expectedMessageHistory[i - 20] = messageContents.get("history" + i); + Assert.assertArrayEquals("Expect messages in forward order", messages.items(), expectedMessageHistory); + } catch (AblyException e) { + e.printStackTrace(); + fail("channelhistory5: Unexpected exception"); + return; + } + } + + /** + * Publish events and check expected history based on time slice (backwards) + */ + @Test + public void channelhistory_time_b() { + /* first, publish some messages */ + long intervalStart = 0, intervalEnd = 0; + Channel history6 = ably.channels.get("persisted:channelhistory_time_b_" + testParams.name); + /* send batches of messages with shprt inter-message delay */ + try { + for(int i = 0; i < 20; i++) { + history6.publish("history" + i, String.valueOf(i)); + Thread.sleep(100L); + } + Thread.sleep(1000L); + intervalStart = timeOffset + System.currentTimeMillis(); + for(int i = 20; i < 40; i++) { + history6.publish("history" + i, String.valueOf(i)); + Thread.sleep(100L); + } + intervalEnd = timeOffset + System.currentTimeMillis() - 1; + Thread.sleep(1000L); + for(int i = 40; i < 60; i++) { + history6.publish("history" + i, String.valueOf(i)); + Thread.sleep(100L); + } + } catch(AblyException e) { + e.printStackTrace(); + fail("channelhistory6: Unexpected exception"); + return; + } catch (InterruptedException e) { + e.printStackTrace(); + fail("channelhistory6: Unexpected exception"); + } + + /* get the history for this channel */ + try { + PaginatedResult messages = history6.history(new Param[] { + new Param("direction", "backwards"), + new Param("start", String.valueOf(intervalStart - 500)), + new Param("end", String.valueOf(intervalEnd + 500)) + }); + assertNotNull("Expected non-null messages", messages); + assertEquals("Expected 20 messages", messages.items().length, 20); + HashMap messageContents = new HashMap(); + for(Message message : messages.items()) + messageContents.put(message.name, message); + /* verify message order */ + Message[] expectedMessageHistory = new Message[20]; + for(int i = 20; i < 40; i++) + expectedMessageHistory[i - 20] = messageContents.get("history" + (59 - i)); + Assert.assertArrayEquals("Expect messages in backwards order", messages.items(), expectedMessageHistory); + } catch (AblyException e) { + e.printStackTrace(); + fail("channelhistory6: Unexpected exception"); + return; + } + } + + /** + * Check query pagination (forwards) + */ + @Test + public void channelhistory_paginate_f() { + /* first, publish some messages */ + Channel history3 = ably.channels.get("persisted:channelhistory_paginate_f_" + testParams.name); + for(int i = 0; i < 50; i++) + try { + history3.publish("history" + i, String.valueOf(i)); + } catch(AblyException e) { + e.printStackTrace(); + fail("channelhistory3: Unexpected exception"); + return; + } + + /* get the history for this channel */ + try { + PaginatedResult messages = history3.history(new Param[] { new Param("direction", "forwards"), new Param("limit", "10") }); + assertNotNull("Expected non-null messages", messages); + assertEquals("Expected 10 messages", messages.items().length, 10); + + /* log all messages */ + HashMap messageContents = new HashMap(); + for(Message message : messages.items()) + messageContents.put(message.name, message); + + /* verify message order */ + Message[] expectedMessageHistory = new Message[10]; + for(int i = 0; i < 10; i++) + expectedMessageHistory[i] = messageContents.get("history" + i); + Assert.assertArrayEquals("Expect messages in forward order", messages.items(), expectedMessageHistory); + + /* get next page */ + messages = messages.next(); + assertNotNull("Expected non-null messages", messages); + assertEquals("Expected 10 messages", messages.items().length, 10); + + /* log all messages */ + for(Message message : messages.items()) + messageContents.put(message.name, message); + + /* verify message order */ + for(int i = 0; i < 10; i++) + expectedMessageHistory[i] = messageContents.get("history" + String.valueOf(i + 10)); + Assert.assertArrayEquals("Expect messages in forward order", messages.items(), expectedMessageHistory); + + /* get next page */ + messages = messages.next(); + assertNotNull("Expected non-null messages", messages); + assertEquals("Expected 10 messages", messages.items().length, 10); + + /* log all messages */ + for(Message message : messages.items()) + messageContents.put(message.name, message); + + /* verify message order */ + for(int i = 0; i < 10; i++) + expectedMessageHistory[i] = messageContents.get("history" + String.valueOf(i + 20)); + Assert.assertArrayEquals("Expect messages in forward order", messages.items(), expectedMessageHistory); + + } catch (AblyException e) { + e.printStackTrace(); + fail("channelhistory3: Unexpected exception"); + return; + } + } + + /** + * Check query pagination (backwards) + */ + @Test + public void channelhistory_paginate_b() { + /* first, publish some messages */ + Channel history3 = ably.channels.get("persisted:channelhistory_paginate_b_" + testParams.name); + for(int i = 0; i < 50; i++) + try { + history3.publish("history" + i, String.valueOf(i)); + } catch(AblyException e) { + e.printStackTrace(); + fail("channelhistory3: Unexpected exception"); + return; + } + + /* get the history for this channel */ + try { + PaginatedResult messages = history3.history(new Param[] { new Param("direction", "backwards"), new Param("limit", "10") }); + assertNotNull("Expected non-null messages", messages); + assertEquals("Expected 10 messages", messages.items().length, 10); + + /* log all messages */ + HashMap messageContents = new HashMap(); + for(Message message : messages.items()) + messageContents.put(message.name, message); + + /* verify message order */ + Message[] expectedMessageHistory = new Message[10]; + for(int i = 0; i < 10; i++) + expectedMessageHistory[i] = messageContents.get("history" + String.valueOf(49 - i)); + Assert.assertArrayEquals("Expect messages in forward order", messages.items(), expectedMessageHistory); + + /* get next page */ + messages = messages.next(); + assertNotNull("Expected non-null messages", messages); + assertEquals("Expected 10 messages", messages.items().length, 10); + + /* log all messages */ + for(Message message : messages.items()) + messageContents.put(message.name, message); + + /* verify message order */ + for(int i = 0; i < 10; i++) + expectedMessageHistory[i] = messageContents.get("history" + String.valueOf(39 - i)); + Assert.assertArrayEquals("Expect messages in forward order", messages.items(), expectedMessageHistory); + + /* get next page */ + messages = messages.next(); + assertNotNull("Expected non-null messages", messages); + assertEquals("Expected 10 messages", messages.items().length, 10); + + /* log all messages */ + for(Message message : messages.items()) + messageContents.put(message.name, message); + + /* verify message order */ + for(int i = 0; i < 10; i++) + expectedMessageHistory[i] = messageContents.get("history" + String.valueOf(29 - i)); + Assert.assertArrayEquals("Expect messages in forward order", messages.items(), expectedMessageHistory); + + } catch (AblyException e) { + e.printStackTrace(); + fail("channelhistory3: Unexpected exception"); + return; + } + } + + /** + * Check query pagination "rel=first" (forwards) + */ + @Test + public void channelhistory_paginate_first_f() { + /* first, publish some messages */ + Channel history3 = ably.channels.get("persisted:channelhistory_paginate_first_f_" + testParams.name); + for(int i = 0; i < 50; i++) + try { + history3.publish("history" + i, String.valueOf(i)); + } catch(AblyException e) { + e.printStackTrace(); + fail("channelhistory3: Unexpected exception"); + return; + } + + /* get the history for this channel */ + try { + PaginatedResult messages = history3.history(new Param[] { new Param("direction", "forwards"), new Param("limit", "10") }); + assertNotNull("Expected non-null messages", messages); + assertEquals("Expected 10 messages", messages.items().length, 10); + + /* log all messages */ + HashMap messageContents = new HashMap(); + for(Message message : messages.items()) + messageContents.put(message.name, message); + + /* verify message order */ + Message[] expectedMessageHistory = new Message[10]; + for(int i = 0; i < 10; i++) + expectedMessageHistory[i] = messageContents.get("history" + i); + Assert.assertArrayEquals("Expect messages in forward order", messages.items(), expectedMessageHistory); + + /* get next page */ + messages = messages.next(); + assertNotNull("Expected non-null messages", messages); + assertEquals("Expected 10 messages", messages.items().length, 10); + + /* log all messages */ + for(Message message : messages.items()) + messageContents.put(message.name, message); + + /* verify message order */ + for(int i = 0; i < 10; i++) + expectedMessageHistory[i] = messageContents.get("history" + String.valueOf(i + 10)); + Assert.assertArrayEquals("Expect messages in forward order", messages.items(), expectedMessageHistory); + + /* get first page */ + messages = messages.first(); + assertNotNull("Expected non-null messages", messages); + assertEquals("Expected 10 messages", messages.items().length, 10); + + /* log all messages */ + for(Message message : messages.items()) + messageContents.put(message.name, message); + + /* verify message order */ + for(int i = 0; i < 10; i++) + expectedMessageHistory[i] = messageContents.get("history" + String.valueOf(i)); + Assert.assertArrayEquals("Expect messages in forward order", messages.items(), expectedMessageHistory); + + } catch (AblyException e) { + e.printStackTrace(); + fail("channelhistory3: Unexpected exception"); + return; + } + } + + /** + * Check query pagination "rel=first" (backwards) + */ + @Test + public void channelhistory_paginate_first_b() { + /* first, publish some messages */ + Channel history3 = ably.channels.get("persisted:channelhistory_paginate_first_b_" + testParams.name); + for(int i = 0; i < 50; i++) + try { + history3.publish("history" + i, String.valueOf(i)); + } catch(AblyException e) { + e.printStackTrace(); + fail("channelhistory3: Unexpected exception"); + return; + } + + /* get the history for this channel */ + try { + PaginatedResult messages = history3.history(new Param[] { new Param("direction", "backwards"), new Param("limit", "10") }); + assertNotNull("Expected non-null messages", messages); + assertEquals("Expected 10 messages", messages.items().length, 10); + + /* log all messages */ + HashMap messageContents = new HashMap(); + for(Message message : messages.items()) + messageContents.put(message.name, message); + + /* verify message order */ + Message[] expectedMessageHistory = new Message[10]; + for(int i = 0; i < 10; i++) + expectedMessageHistory[i] = messageContents.get("history" + String.valueOf(49 - i)); + Assert.assertArrayEquals("Expect messages in forward order", messages.items(), expectedMessageHistory); + + /* get next page */ + messages = messages.next(); + assertNotNull("Expected non-null messages", messages); + assertEquals("Expected 10 messages", messages.items().length, 10); + + /* log all messages */ + for(Message message : messages.items()) + messageContents.put(message.name, message); + + /* verify message order */ + for(int i = 0; i < 10; i++) + expectedMessageHistory[i] = messageContents.get("history" + String.valueOf(39 - i)); + Assert.assertArrayEquals("Expect messages in forward order", messages.items(), expectedMessageHistory); + + /* get first page */ + messages = messages.first(); + assertNotNull("Expected non-null messages", messages); + assertEquals("Expected 10 messages", messages.items().length, 10); + + /* log all messages */ + for(Message message : messages.items()) + messageContents.put(message.name, message); + + /* verify message order */ + for(int i = 0; i < 10; i++) + expectedMessageHistory[i] = messageContents.get("history" + String.valueOf(49 - i)); + Assert.assertArrayEquals("Expect messages in forward order", messages.items(), expectedMessageHistory); + + } catch (AblyException e) { + e.printStackTrace(); + fail("channelhistory3: Unexpected exception"); + return; + } + } } diff --git a/lib/src/test/java/io/ably/lib/test/rest/RestChannelPublishTest.java b/lib/src/test/java/io/ably/lib/test/rest/RestChannelPublishTest.java index 3ef1ce3ae..e6984797b 100644 --- a/lib/src/test/java/io/ably/lib/test/rest/RestChannelPublishTest.java +++ b/lib/src/test/java/io/ably/lib/test/rest/RestChannelPublishTest.java @@ -29,405 +29,405 @@ public class RestChannelPublishTest extends ParameterizedTest { - private AblyRest ably; - - @Before - public void setUpBefore() throws Exception { - ClientOptions opts = createOptions(testVars.keys[0].keyStr); - ably = new AblyRest(opts); - } - - /** - * Publish events with data of various datatypes using text protocol - */ - @Test - public void channelpublish() { - /* first, publish some messages */ - String channelName = "persisted:channelpublish_" + testParams.name; - Channel pubChannel = ably.channels.get(channelName); - try { - pubChannel.publish("pub_text", "This is a string message payload"); - pubChannel.publish("pub_binary", "This is a byte[] message payload".getBytes()); - } catch(AblyException e) { - e.printStackTrace(); - fail("channelpublish_text: Unexpected exception"); - return; - } - - /* get the history for this channel */ - try { - PaginatedResult messages = pubChannel.history(null); - assertNotNull("Expected non-null messages", messages); - assertEquals("Expected 2 messages", messages.items().length, 2); - HashMap messageContents = new HashMap(); - /* verify message contents */ - for(Message message : messages.items()) - messageContents.put(message.name, message.data); - assertEquals("Expect pub_text to be expected String", messageContents.get("pub_text"), "This is a string message payload"); - assertEquals("Expect pub_binary to be expected byte[]", new String((byte[])messageContents.get("pub_binary")), "This is a byte[] message payload"); - } catch (AblyException e) { - e.printStackTrace(); - fail("channelpublish_text: Unexpected exception"); - return; - } - } - - /** - * Publish events using the async publish API - */ - @Test - public void channelpublish_async() { - /* first, publish some messages */ - String channelName = "persisted:channelpublish_async_" + testParams.name; - String textPayload = "This is a string message payload"; - byte[] binaryPayload = "This is a byte[] message payload".getBytes(); - Channel pubChannel = ably.channels.get(channelName); - CompletionSet pubComplete = new CompletionSet(); - - pubChannel.publishAsync(new Message[] {new Message("pub_text", textPayload)}, pubComplete.add()); - pubChannel.publishAsync(new Message[] {new Message("pub_binary", binaryPayload)}, pubComplete.add()); - - ErrorInfo[] pubErrors = pubComplete.waitFor(); - if(pubErrors != null && pubErrors.length > 0) { - fail("channelpublish_async: Unexpected errors from publish"); - return; - } - - /* get the history for this channel */ - AsyncWaiter> callback = new AsyncWaiter>(); - pubChannel.historyAsync(null, callback); - callback.waitFor(); - - if(callback.error != null) { - fail("channelpublish_async: Unexpected errors from history: " + callback.error); - return; - } - - AsyncPaginatedResult messages = callback.result; - assertNotNull("Expected non-null messages", messages); - assertEquals("Expected 2 messages", messages.items().length, 2); - HashMap messageContents = new HashMap(); - /* verify message contents */ - for(Message message : messages.items()) - messageContents.put(message.name, message.data); - assertEquals("Expect pub_text to be expected String", messageContents.get("pub_text"), textPayload); - assertThat("Expect pub_binary to be expected byte[]", (byte[])messageContents.get("pub_binary"), equalTo(binaryPayload)); - } - - /** - * Verify processing of a client-supplied message id - */ - @Test - public void channel_idempotent_publish_client_generated_single() { - - String channelName = "persisted:channel_idempotent_publish_client_generated_single_" + testParams.name; - Channel pubChannel; - final Message messageWithId = new Message("name_withId", "data_withId"); - messageWithId.id = "client_generated_id"; - try { - DebugOptions opts = new DebugOptions(testVars.keys[0].keyStr); - fillInOptions(opts); - opts.useBinaryProtocol = true; - opts.httpListener = new DebugOptions.RawHttpListener() { - @Override - public HttpCore.Response onRawHttpRequest(String id, HttpURLConnection conn, String method, String authHeader, Map> requestHeaders, HttpCore.RequestBody requestBody) { - /* verify request body contains the supplied ids */ - try { - if(method.equalsIgnoreCase("POST")) { - Message[] requestedMessages = MessageSerializer.readMsgpack(requestBody.getEncoded()); - assertEquals(requestedMessages[0].id, messageWithId.id); - } - } catch (AblyException e) { - e.printStackTrace(); - } - return null; - } - - @Override - public void onRawHttpResponse(String id, String method, HttpCore.Response response) {} - - @Override - public void onRawHttpException(String id, String method, Throwable t) {} - }; - AblyRest ably = new AblyRest(opts); - - /* first, publish messages */ - pubChannel = ably.channels.get(channelName); - pubChannel.publish(new Message[]{messageWithId}); - } catch(AblyException e) { - e.printStackTrace(); - fail("channel_idempotent_publish_client_generated_single: Unexpected exception"); - return; - } - - /* get the history for this channel */ - try { + private AblyRest ably; + + @Before + public void setUpBefore() throws Exception { + ClientOptions opts = createOptions(testVars.keys[0].keyStr); + ably = new AblyRest(opts); + } + + /** + * Publish events with data of various datatypes using text protocol + */ + @Test + public void channelpublish() { + /* first, publish some messages */ + String channelName = "persisted:channelpublish_" + testParams.name; + Channel pubChannel = ably.channels.get(channelName); + try { + pubChannel.publish("pub_text", "This is a string message payload"); + pubChannel.publish("pub_binary", "This is a byte[] message payload".getBytes()); + } catch(AblyException e) { + e.printStackTrace(); + fail("channelpublish_text: Unexpected exception"); + return; + } + + /* get the history for this channel */ + try { + PaginatedResult messages = pubChannel.history(null); + assertNotNull("Expected non-null messages", messages); + assertEquals("Expected 2 messages", messages.items().length, 2); + HashMap messageContents = new HashMap(); + /* verify message contents */ + for(Message message : messages.items()) + messageContents.put(message.name, message.data); + assertEquals("Expect pub_text to be expected String", messageContents.get("pub_text"), "This is a string message payload"); + assertEquals("Expect pub_binary to be expected byte[]", new String((byte[])messageContents.get("pub_binary")), "This is a byte[] message payload"); + } catch (AblyException e) { + e.printStackTrace(); + fail("channelpublish_text: Unexpected exception"); + return; + } + } + + /** + * Publish events using the async publish API + */ + @Test + public void channelpublish_async() { + /* first, publish some messages */ + String channelName = "persisted:channelpublish_async_" + testParams.name; + String textPayload = "This is a string message payload"; + byte[] binaryPayload = "This is a byte[] message payload".getBytes(); + Channel pubChannel = ably.channels.get(channelName); + CompletionSet pubComplete = new CompletionSet(); + + pubChannel.publishAsync(new Message[] {new Message("pub_text", textPayload)}, pubComplete.add()); + pubChannel.publishAsync(new Message[] {new Message("pub_binary", binaryPayload)}, pubComplete.add()); + + ErrorInfo[] pubErrors = pubComplete.waitFor(); + if(pubErrors != null && pubErrors.length > 0) { + fail("channelpublish_async: Unexpected errors from publish"); + return; + } + + /* get the history for this channel */ + AsyncWaiter> callback = new AsyncWaiter>(); + pubChannel.historyAsync(null, callback); + callback.waitFor(); + + if(callback.error != null) { + fail("channelpublish_async: Unexpected errors from history: " + callback.error); + return; + } + + AsyncPaginatedResult messages = callback.result; + assertNotNull("Expected non-null messages", messages); + assertEquals("Expected 2 messages", messages.items().length, 2); + HashMap messageContents = new HashMap(); + /* verify message contents */ + for(Message message : messages.items()) + messageContents.put(message.name, message.data); + assertEquals("Expect pub_text to be expected String", messageContents.get("pub_text"), textPayload); + assertThat("Expect pub_binary to be expected byte[]", (byte[])messageContents.get("pub_binary"), equalTo(binaryPayload)); + } + + /** + * Verify processing of a client-supplied message id + */ + @Test + public void channel_idempotent_publish_client_generated_single() { + + String channelName = "persisted:channel_idempotent_publish_client_generated_single_" + testParams.name; + Channel pubChannel; + final Message messageWithId = new Message("name_withId", "data_withId"); + messageWithId.id = "client_generated_id"; + try { + DebugOptions opts = new DebugOptions(testVars.keys[0].keyStr); + fillInOptions(opts); + opts.useBinaryProtocol = true; + opts.httpListener = new DebugOptions.RawHttpListener() { + @Override + public HttpCore.Response onRawHttpRequest(String id, HttpURLConnection conn, String method, String authHeader, Map> requestHeaders, HttpCore.RequestBody requestBody) { + /* verify request body contains the supplied ids */ + try { + if(method.equalsIgnoreCase("POST")) { + Message[] requestedMessages = MessageSerializer.readMsgpack(requestBody.getEncoded()); + assertEquals(requestedMessages[0].id, messageWithId.id); + } + } catch (AblyException e) { + e.printStackTrace(); + } + return null; + } + + @Override + public void onRawHttpResponse(String id, String method, HttpCore.Response response) {} + + @Override + public void onRawHttpException(String id, String method, Throwable t) {} + }; + AblyRest ably = new AblyRest(opts); + + /* first, publish messages */ + pubChannel = ably.channels.get(channelName); + pubChannel.publish(new Message[]{messageWithId}); + } catch(AblyException e) { + e.printStackTrace(); + fail("channel_idempotent_publish_client_generated_single: Unexpected exception"); + return; + } + + /* get the history for this channel */ + try { PaginatedResult messages = pubChannel.history(new Param[]{new Param("direction", "forwards")}); - assertNotNull("Expected non-null messages", messages); - assertEquals("Expected 1 message", messages.items().length, 1); - assertEquals(messages.items()[0].id, messageWithId.id); - } catch (AblyException e) { - e.printStackTrace(); - fail("channel_idempotent_publish_client_generated_single: Unexpected exception"); - return; - } - } - - /** - * Verify processing of a client-supplied message id - */ - @Test - public void channel_idempotent_publish_client_generated_multiple() { - - String channelName = "persisted:channel_idempotent_publish_client_generated_multiple_" + testParams.name; - Channel pubChannel; - final Message messageWithId0 = new Message("name_withId", "data_withId"); - messageWithId0.id = "client_generated_id:0"; - final Message messageWithId1 = new Message("name_withId", "data_withId"); - messageWithId1.id = "client_generated_id:1"; - try { - DebugOptions opts = new DebugOptions(testVars.keys[0].keyStr); - fillInOptions(opts); - opts.useBinaryProtocol = true; - opts.httpListener = new DebugOptions.RawHttpListener() { - @Override - public HttpCore.Response onRawHttpRequest(String id, HttpURLConnection conn, String method, String authHeader, Map> requestHeaders, HttpCore.RequestBody requestBody) { - /* verify request body contains the supplied ids */ - try { - if(method.equalsIgnoreCase("POST")) { - Message[] requestedMessages = MessageSerializer.readMsgpack(requestBody.getEncoded()); - assertEquals(requestedMessages[0].id, messageWithId0.id); - assertEquals(requestedMessages[1].id, messageWithId1.id); - } - } catch (AblyException e) { - e.printStackTrace(); - } - return null; - } - - @Override - public void onRawHttpResponse(String id, String method, HttpCore.Response response) {} - - @Override - public void onRawHttpException(String id, String method, Throwable t) {} - }; - AblyRest ably = new AblyRest(opts); - - /* first, publish messages */ - pubChannel = ably.channels.get(channelName); - pubChannel.publish(new Message[]{messageWithId0, messageWithId1}); - } catch(AblyException e) { - e.printStackTrace(); - fail("channel_idempotent_publish_client_generated_multiple: Unexpected exception"); - return; - } - - /* get the history for this channel */ - try { - PaginatedResult messages = pubChannel.history(new Param[]{new Param("direction", "forwards")}); - assertNotNull("Expected non-null messages", messages); - assertEquals("Expected 2 messages", messages.items().length, 2); - assertEquals(messages.items()[0].id, messageWithId0.id); - assertEquals(messages.items()[1].id, messageWithId1.id); - } catch (AblyException e) { - e.printStackTrace(); - fail("channel_idempotent_publish_client_generated_multiple: Unexpected exception"); - return; - } - } - - static class FailFirstRequest implements DebugOptions.RawHttpListener { - int postRequestCount = 0; - final String expectedId; - - FailFirstRequest() { - this.expectedId = null; - } - - FailFirstRequest(String expectedId) { - this.expectedId = expectedId; - } - - @Override - public HttpCore.Response onRawHttpRequest(String id, HttpURLConnection conn, String method, String authHeader, Map> requestHeaders, HttpCore.RequestBody requestBody) { - /* verify request body contains the supplied ids */ - try { - if(method.equalsIgnoreCase("POST")) { - ++postRequestCount; - Message[] requestedMessages = MessageSerializer.readMsgpack(requestBody.getEncoded()); - if(expectedId != null) { - assertEquals(requestedMessages[0].id, expectedId); - } - } - } catch (AblyException e) {} - return null; - } - - @Override - public void onRawHttpResponse(String id, String method, HttpCore.Response response) { - if(method.equalsIgnoreCase("POST") && postRequestCount == 1) { - response.statusCode = 500; - } - } - - @Override - public void onRawHttpException(String id, String method, Throwable t) {} - } - - /** - * Verify processing of a client-supplied message id - * Spec: RSL1k5 - */ - @Test - public void channel_idempotent_publish_client_generated_retried() { - String channelName = "persisted:channel_idempotent_publish_client_generated_retried_" + testParams.name; - - final Message messageWithId = new Message("name_withId", "data_withId"); - messageWithId.id = "client_generated_id"; - FailFirstRequest requestListener = new FailFirstRequest(messageWithId.id); - - try { - final ClientOptions optsForToken = createOptions(testVars.keys[0].keyStr); - final AblyRest ablyForToken = new AblyRest(optsForToken); - Auth.AuthOptions restAuthOptions = new Auth.AuthOptions() {{ - key = optsForToken.key; - queryTime = true; - }}; - - Auth.TokenDetails tokenDetails = ablyForToken.auth.requestToken(new Auth.TokenParams() {{ ttl = 8000L; }}, restAuthOptions); - - DebugOptions opts = new DebugOptions(tokenDetails.token); - fillInOptions(opts); - opts.useBinaryProtocol = true; - opts.httpListener = requestListener; - /* generate a fallback which resolves to the same address, which the library will treat as a different host */ - opts.fallbackHosts = new String[]{ablyForToken.httpCore.getPrimaryHost().toUpperCase()}; - AblyRest ably = new AblyRest(opts); - - /* publish message */ - Channel pubChannel = ably.channels.get(channelName); - pubChannel.publish(new Message[]{messageWithId}); - - /* get the history for this channel */ - PaginatedResult messages = pubChannel.history(new Param[]{new Param("direction", "forwards")}); - assertEquals("Expected 2 requests", requestListener.postRequestCount, 2); - assertNotNull("Expected non-null messages", messages); - assertEquals("Expected 1 message", messages.items().length, 1); - assertEquals(messages.items()[0].id, messageWithId.id); - } catch (AblyException e) { - e.printStackTrace(); - fail("channel_idempotent_publish_client_generated_retried: Unexpected exception"); - return; - } - } - - /** - * Verify processing of a library-generated message id - */ - @Test - public void channel_idempotent_publish_library_generated_multiple() { - - String channelName = "persisted:channel_idempotent_publish_library_generated_multiple_" + testParams.name; - Channel pubChannel; - final Message messageWithId0 = new Message("name0", "data0"); - final Message messageWithId1 = new Message("name1", "data1"); - try { - DebugOptions opts = new DebugOptions(testVars.keys[0].keyStr); - fillInOptions(opts); - opts.idempotentRestPublishing = true; - opts.useBinaryProtocol = true; - opts.httpListener = new DebugOptions.RawHttpListener() { - @Override - public HttpCore.Response onRawHttpRequest(String id, HttpURLConnection conn, String method, String authHeader, Map> requestHeaders, HttpCore.RequestBody requestBody) { - /* verify request body contains the library-generated ids */ - try { - if(method.equalsIgnoreCase("POST")) { - Message[] requestedMessages = MessageSerializer.readMsgpack(requestBody.getEncoded()); - assertTrue(requestedMessages[0].id.endsWith(":0")); - assertTrue(requestedMessages[1].id.endsWith(":1")); - } - } catch (AblyException e) { - e.printStackTrace(); - } - return null; - } - - @Override - public void onRawHttpResponse(String id, String method, HttpCore.Response response) {} - - @Override - public void onRawHttpException(String id, String method, Throwable t) {} - }; - AblyRest ably = new AblyRest(opts); - - /* first, publish messages */ - pubChannel = ably.channels.get(channelName); - pubChannel.publish(new Message[]{messageWithId0, messageWithId1}); - } catch(AblyException e) { - e.printStackTrace(); - fail("channel_idempotent_publish_client_generated_multiple: Unexpected exception"); - return; - } - - /* get the history for this channel */ - try { - PaginatedResult messages = pubChannel.history(new Param[]{new Param("direction", "forwards")}); - assertNotNull("Expected non-null messages", messages); - assertEquals("Expected 2 messages", messages.items().length, 2); - assertEquals(messages.items()[0].id, messageWithId0.id); - assertEquals(messages.items()[1].id, messageWithId1.id); - } catch (AblyException e) { - e.printStackTrace(); - fail("channel_idempotent_publish_client_generated_multiple: Unexpected exception"); - return; - } - } - - /** - * Verify processing of a library-generated message id - * Spec: RSL1k4 - */ - @Test - public void channel_idempotent_publish_library_generated_retried() { - String channelName = "persisted:channel_idempotent_publish_library_generated_retried_" + testParams.name; - - final Message messageWithId = new Message("name0", "data0"); - FailFirstRequest requestListener = new FailFirstRequest(); - - try { - final ClientOptions optsForToken = createOptions(testVars.keys[0].keyStr); - final AblyRest ablyForToken = new AblyRest(optsForToken); - Auth.AuthOptions restAuthOptions = new Auth.AuthOptions() {{ - key = optsForToken.key; - queryTime = true; - }}; - - Auth.TokenDetails tokenDetails = ablyForToken.auth.requestToken(new Auth.TokenParams() {{ ttl = 8000L; }}, restAuthOptions); - - DebugOptions opts = new DebugOptions(tokenDetails.token); - fillInOptions(opts); - opts.idempotentRestPublishing = true; - opts.useBinaryProtocol = true; - opts.httpListener = requestListener; - /* generate a fallback which resolves to the same address, which the library will treat as a different host */ - opts.fallbackHosts = new String[]{ablyForToken.httpCore.getPrimaryHost().toUpperCase()}; - AblyRest ably = new AblyRest(opts); - - /* publish message */ - Channel pubChannel = ably.channels.get(channelName); - pubChannel.publish(new Message[]{messageWithId}); - - /* get the history for this channel */ - PaginatedResult messages = pubChannel.history(new Param[]{new Param("direction", "forwards")}); - assertEquals("Expected 2 requests", requestListener.postRequestCount, 2); - assertNotNull("Expected non-null messages", messages); - assertEquals("Expected 1 message", messages.items().length, 1); - assertEquals(messages.items()[0].id, messageWithId.id); - } catch (AblyException e) { - e.printStackTrace(); - fail("channel_idempotent_publish_library_generated_retried: Unexpected exception"); - return; - } - } + assertNotNull("Expected non-null messages", messages); + assertEquals("Expected 1 message", messages.items().length, 1); + assertEquals(messages.items()[0].id, messageWithId.id); + } catch (AblyException e) { + e.printStackTrace(); + fail("channel_idempotent_publish_client_generated_single: Unexpected exception"); + return; + } + } + + /** + * Verify processing of a client-supplied message id + */ + @Test + public void channel_idempotent_publish_client_generated_multiple() { + + String channelName = "persisted:channel_idempotent_publish_client_generated_multiple_" + testParams.name; + Channel pubChannel; + final Message messageWithId0 = new Message("name_withId", "data_withId"); + messageWithId0.id = "client_generated_id:0"; + final Message messageWithId1 = new Message("name_withId", "data_withId"); + messageWithId1.id = "client_generated_id:1"; + try { + DebugOptions opts = new DebugOptions(testVars.keys[0].keyStr); + fillInOptions(opts); + opts.useBinaryProtocol = true; + opts.httpListener = new DebugOptions.RawHttpListener() { + @Override + public HttpCore.Response onRawHttpRequest(String id, HttpURLConnection conn, String method, String authHeader, Map> requestHeaders, HttpCore.RequestBody requestBody) { + /* verify request body contains the supplied ids */ + try { + if(method.equalsIgnoreCase("POST")) { + Message[] requestedMessages = MessageSerializer.readMsgpack(requestBody.getEncoded()); + assertEquals(requestedMessages[0].id, messageWithId0.id); + assertEquals(requestedMessages[1].id, messageWithId1.id); + } + } catch (AblyException e) { + e.printStackTrace(); + } + return null; + } + + @Override + public void onRawHttpResponse(String id, String method, HttpCore.Response response) {} + + @Override + public void onRawHttpException(String id, String method, Throwable t) {} + }; + AblyRest ably = new AblyRest(opts); + + /* first, publish messages */ + pubChannel = ably.channels.get(channelName); + pubChannel.publish(new Message[]{messageWithId0, messageWithId1}); + } catch(AblyException e) { + e.printStackTrace(); + fail("channel_idempotent_publish_client_generated_multiple: Unexpected exception"); + return; + } + + /* get the history for this channel */ + try { + PaginatedResult messages = pubChannel.history(new Param[]{new Param("direction", "forwards")}); + assertNotNull("Expected non-null messages", messages); + assertEquals("Expected 2 messages", messages.items().length, 2); + assertEquals(messages.items()[0].id, messageWithId0.id); + assertEquals(messages.items()[1].id, messageWithId1.id); + } catch (AblyException e) { + e.printStackTrace(); + fail("channel_idempotent_publish_client_generated_multiple: Unexpected exception"); + return; + } + } + + static class FailFirstRequest implements DebugOptions.RawHttpListener { + int postRequestCount = 0; + final String expectedId; + + FailFirstRequest() { + this.expectedId = null; + } + + FailFirstRequest(String expectedId) { + this.expectedId = expectedId; + } + + @Override + public HttpCore.Response onRawHttpRequest(String id, HttpURLConnection conn, String method, String authHeader, Map> requestHeaders, HttpCore.RequestBody requestBody) { + /* verify request body contains the supplied ids */ + try { + if(method.equalsIgnoreCase("POST")) { + ++postRequestCount; + Message[] requestedMessages = MessageSerializer.readMsgpack(requestBody.getEncoded()); + if(expectedId != null) { + assertEquals(requestedMessages[0].id, expectedId); + } + } + } catch (AblyException e) {} + return null; + } + + @Override + public void onRawHttpResponse(String id, String method, HttpCore.Response response) { + if(method.equalsIgnoreCase("POST") && postRequestCount == 1) { + response.statusCode = 500; + } + } + + @Override + public void onRawHttpException(String id, String method, Throwable t) {} + } + + /** + * Verify processing of a client-supplied message id + * Spec: RSL1k5 + */ + @Test + public void channel_idempotent_publish_client_generated_retried() { + String channelName = "persisted:channel_idempotent_publish_client_generated_retried_" + testParams.name; + + final Message messageWithId = new Message("name_withId", "data_withId"); + messageWithId.id = "client_generated_id"; + FailFirstRequest requestListener = new FailFirstRequest(messageWithId.id); + + try { + final ClientOptions optsForToken = createOptions(testVars.keys[0].keyStr); + final AblyRest ablyForToken = new AblyRest(optsForToken); + Auth.AuthOptions restAuthOptions = new Auth.AuthOptions() {{ + key = optsForToken.key; + queryTime = true; + }}; + + Auth.TokenDetails tokenDetails = ablyForToken.auth.requestToken(new Auth.TokenParams() {{ ttl = 8000L; }}, restAuthOptions); + + DebugOptions opts = new DebugOptions(tokenDetails.token); + fillInOptions(opts); + opts.useBinaryProtocol = true; + opts.httpListener = requestListener; + /* generate a fallback which resolves to the same address, which the library will treat as a different host */ + opts.fallbackHosts = new String[]{ablyForToken.httpCore.getPrimaryHost().toUpperCase()}; + AblyRest ably = new AblyRest(opts); + + /* publish message */ + Channel pubChannel = ably.channels.get(channelName); + pubChannel.publish(new Message[]{messageWithId}); + + /* get the history for this channel */ + PaginatedResult messages = pubChannel.history(new Param[]{new Param("direction", "forwards")}); + assertEquals("Expected 2 requests", requestListener.postRequestCount, 2); + assertNotNull("Expected non-null messages", messages); + assertEquals("Expected 1 message", messages.items().length, 1); + assertEquals(messages.items()[0].id, messageWithId.id); + } catch (AblyException e) { + e.printStackTrace(); + fail("channel_idempotent_publish_client_generated_retried: Unexpected exception"); + return; + } + } + + /** + * Verify processing of a library-generated message id + */ + @Test + public void channel_idempotent_publish_library_generated_multiple() { + + String channelName = "persisted:channel_idempotent_publish_library_generated_multiple_" + testParams.name; + Channel pubChannel; + final Message messageWithId0 = new Message("name0", "data0"); + final Message messageWithId1 = new Message("name1", "data1"); + try { + DebugOptions opts = new DebugOptions(testVars.keys[0].keyStr); + fillInOptions(opts); + opts.idempotentRestPublishing = true; + opts.useBinaryProtocol = true; + opts.httpListener = new DebugOptions.RawHttpListener() { + @Override + public HttpCore.Response onRawHttpRequest(String id, HttpURLConnection conn, String method, String authHeader, Map> requestHeaders, HttpCore.RequestBody requestBody) { + /* verify request body contains the library-generated ids */ + try { + if(method.equalsIgnoreCase("POST")) { + Message[] requestedMessages = MessageSerializer.readMsgpack(requestBody.getEncoded()); + assertTrue(requestedMessages[0].id.endsWith(":0")); + assertTrue(requestedMessages[1].id.endsWith(":1")); + } + } catch (AblyException e) { + e.printStackTrace(); + } + return null; + } + + @Override + public void onRawHttpResponse(String id, String method, HttpCore.Response response) {} + + @Override + public void onRawHttpException(String id, String method, Throwable t) {} + }; + AblyRest ably = new AblyRest(opts); + + /* first, publish messages */ + pubChannel = ably.channels.get(channelName); + pubChannel.publish(new Message[]{messageWithId0, messageWithId1}); + } catch(AblyException e) { + e.printStackTrace(); + fail("channel_idempotent_publish_client_generated_multiple: Unexpected exception"); + return; + } + + /* get the history for this channel */ + try { + PaginatedResult messages = pubChannel.history(new Param[]{new Param("direction", "forwards")}); + assertNotNull("Expected non-null messages", messages); + assertEquals("Expected 2 messages", messages.items().length, 2); + assertEquals(messages.items()[0].id, messageWithId0.id); + assertEquals(messages.items()[1].id, messageWithId1.id); + } catch (AblyException e) { + e.printStackTrace(); + fail("channel_idempotent_publish_client_generated_multiple: Unexpected exception"); + return; + } + } + + /** + * Verify processing of a library-generated message id + * Spec: RSL1k4 + */ + @Test + public void channel_idempotent_publish_library_generated_retried() { + String channelName = "persisted:channel_idempotent_publish_library_generated_retried_" + testParams.name; + + final Message messageWithId = new Message("name0", "data0"); + FailFirstRequest requestListener = new FailFirstRequest(); + + try { + final ClientOptions optsForToken = createOptions(testVars.keys[0].keyStr); + final AblyRest ablyForToken = new AblyRest(optsForToken); + Auth.AuthOptions restAuthOptions = new Auth.AuthOptions() {{ + key = optsForToken.key; + queryTime = true; + }}; + + Auth.TokenDetails tokenDetails = ablyForToken.auth.requestToken(new Auth.TokenParams() {{ ttl = 8000L; }}, restAuthOptions); + + DebugOptions opts = new DebugOptions(tokenDetails.token); + fillInOptions(opts); + opts.idempotentRestPublishing = true; + opts.useBinaryProtocol = true; + opts.httpListener = requestListener; + /* generate a fallback which resolves to the same address, which the library will treat as a different host */ + opts.fallbackHosts = new String[]{ablyForToken.httpCore.getPrimaryHost().toUpperCase()}; + AblyRest ably = new AblyRest(opts); + + /* publish message */ + Channel pubChannel = ably.channels.get(channelName); + pubChannel.publish(new Message[]{messageWithId}); + + /* get the history for this channel */ + PaginatedResult messages = pubChannel.history(new Param[]{new Param("direction", "forwards")}); + assertEquals("Expected 2 requests", requestListener.postRequestCount, 2); + assertNotNull("Expected non-null messages", messages); + assertEquals("Expected 1 message", messages.items().length, 1); + assertEquals(messages.items()[0].id, messageWithId.id); + } catch (AblyException e) { + e.printStackTrace(); + fail("channel_idempotent_publish_library_generated_retried: Unexpected exception"); + return; + } + } } diff --git a/lib/src/test/java/io/ably/lib/test/rest/RestChannelTest.java b/lib/src/test/java/io/ably/lib/test/rest/RestChannelTest.java index 91f7f5609..5d7afc300 100644 --- a/lib/src/test/java/io/ably/lib/test/rest/RestChannelTest.java +++ b/lib/src/test/java/io/ably/lib/test/rest/RestChannelTest.java @@ -15,34 +15,34 @@ * Test for basic Channel operation not related to publish or history */ public class RestChannelTest { - /** - * Test if ably.channel.get() returns same object if the same name is given - * Tests RTN2, RTN3, RSN3a, RSN4a - */ - @Test - public void channel_object_caching() throws AblyException { - Setup.TestVars testVars = Setup.getTestVars(); - ClientOptions opts = new ClientOptions(testVars.keys[0].keyStr); - AblyRest ablyRest = new AblyRest(opts); + /** + * Test if ably.channel.get() returns same object if the same name is given + * Tests RTN2, RTN3, RSN3a, RSN4a + */ + @Test + public void channel_object_caching() throws AblyException { + Setup.TestVars testVars = Setup.getTestVars(); + ClientOptions opts = new ClientOptions(testVars.keys[0].keyStr); + AblyRest ablyRest = new AblyRest(opts); - Channel channel1 = ablyRest.channels.get("channel_1"); - Channel channel2 = ablyRest.channels.get("channel_2"); + Channel channel1 = ablyRest.channels.get("channel_1"); + Channel channel2 = ablyRest.channels.get("channel_2"); - Channel channel1_copy = ablyRest.channels.get("channel_1"); + Channel channel1_copy = ablyRest.channels.get("channel_1"); - assertEquals("Verify channel objects are cached", channel1, channel1_copy); - assertNotEquals("Verify channel objects are different if different names are requested", channel1, channel2); + assertEquals("Verify channel objects are cached", channel1, channel1_copy); + assertNotEquals("Verify channel objects are different if different names are requested", channel1, channel2); - /* Test channel enumeration */ - assertEquals("Verify total number of channels", ablyRest.channels.size(), 2); - assertTrue("Verify there is channel 1 in the list", ablyRest.channels.containsKey("channel_1")); - assertTrue("Verify there is channel 2 in the list", ablyRest.channels.containsKey("channel_2")); - for (final Channel channel : ablyRest.channels.values()) { - assertTrue("Verify correct channels are in the hashmap", - channel == channel1 || channel == channel2); - } - ablyRest.channels.release("channel_1"); - channel1_copy = ablyRest.channels.get("channel_1"); - assertNotEquals("Verify channel is re-created after release", channel1, channel1_copy); - } + /* Test channel enumeration */ + assertEquals("Verify total number of channels", ablyRest.channels.size(), 2); + assertTrue("Verify there is channel 1 in the list", ablyRest.channels.containsKey("channel_1")); + assertTrue("Verify there is channel 2 in the list", ablyRest.channels.containsKey("channel_2")); + for (final Channel channel : ablyRest.channels.values()) { + assertTrue("Verify correct channels are in the hashmap", + channel == channel1 || channel == channel2); + } + ablyRest.channels.release("channel_1"); + channel1_copy = ablyRest.channels.get("channel_1"); + assertNotEquals("Verify channel is re-created after release", channel1, channel1_copy); + } } diff --git a/lib/src/test/java/io/ably/lib/test/rest/RestCryptoTest.java b/lib/src/test/java/io/ably/lib/test/rest/RestCryptoTest.java index 5ebe36489..215d8aa26 100644 --- a/lib/src/test/java/io/ably/lib/test/rest/RestCryptoTest.java +++ b/lib/src/test/java/io/ably/lib/test/rest/RestCryptoTest.java @@ -27,266 +27,266 @@ public class RestCryptoTest extends ParameterizedTest { - private static final String TAG = RestCryptoTest.class.getName(); - private AblyRest ably; - private AblyRest ably_alt; + private static final String TAG = RestCryptoTest.class.getName(); + private AblyRest ably; + private AblyRest ably_alt; - @Before - public void setUpBefore() throws Exception { - ClientOptions opts = createOptions(testVars.keys[0].keyStr); - ably = new AblyRest(opts); - ClientOptions opts_alt = createOptions(testVars.keys[0].keyStr); - opts_alt.useBinaryProtocol = testParams.useBinaryProtocol; - ably_alt = new AblyRest(opts_alt); - } + @Before + public void setUpBefore() throws Exception { + ClientOptions opts = createOptions(testVars.keys[0].keyStr); + ably = new AblyRest(opts); + ClientOptions opts_alt = createOptions(testVars.keys[0].keyStr); + opts_alt.useBinaryProtocol = testParams.useBinaryProtocol; + ably_alt = new AblyRest(opts_alt); + } - /** - * Publish events with data of various datatypes using text protocol - */ - @Test - public void crypto_publish() { - /* first, publish some messages */ - Channel publish0; - try { - ChannelOptions channelOpts = new ChannelOptions() {{ encrypted = true; }}; - publish0 = ably.channels.get("persisted:crypto_publish_" + testParams.name, channelOpts); + /** + * Publish events with data of various datatypes using text protocol + */ + @Test + public void crypto_publish() { + /* first, publish some messages */ + Channel publish0; + try { + ChannelOptions channelOpts = new ChannelOptions() {{ encrypted = true; }}; + publish0 = ably.channels.get("persisted:crypto_publish_" + testParams.name, channelOpts); - publish0.publish("publish0", "This is a string message payload"); - publish0.publish("publish1", "This is a byte[] message payload".getBytes()); - } catch(AblyException e) { - e.printStackTrace(); - fail("channelpublish_text: Unexpected exception"); - return; - } + publish0.publish("publish0", "This is a string message payload"); + publish0.publish("publish1", "This is a byte[] message payload".getBytes()); + } catch(AblyException e) { + e.printStackTrace(); + fail("channelpublish_text: Unexpected exception"); + return; + } - /* get the history for this channel */ - try { - PaginatedResult messages = publish0.history(null); - assertNotNull("Expected non-null messages", messages); - assertEquals("Expected 2 messages", messages.items().length, 2); - HashMap messageContents = new HashMap(); - /* verify message contents */ - for(Message message : messages.items()) - messageContents.put(message.name, message.data); - assertEquals("Expect publish0 to be expected String", messageContents.get("publish0"), "This is a string message payload"); - assertEquals("Expect publish1 to be expected byte[]", new String((byte[])messageContents.get("publish1")), "This is a byte[] message payload"); - } catch (AblyException e) { - e.printStackTrace(); - fail("channelpublish_text: Unexpected exception"); - return; - } - } + /* get the history for this channel */ + try { + PaginatedResult messages = publish0.history(null); + assertNotNull("Expected non-null messages", messages); + assertEquals("Expected 2 messages", messages.items().length, 2); + HashMap messageContents = new HashMap(); + /* verify message contents */ + for(Message message : messages.items()) + messageContents.put(message.name, message.data); + assertEquals("Expect publish0 to be expected String", messageContents.get("publish0"), "This is a string message payload"); + assertEquals("Expect publish1 to be expected byte[]", new String((byte[])messageContents.get("publish1")), "This is a byte[] message payload"); + } catch (AblyException e) { + e.printStackTrace(); + fail("channelpublish_text: Unexpected exception"); + return; + } + } - /** - * Publish events with data of various datatypes using text protocol with a 256-bit key - */ - @Test - public void crypto_publish_256() { - /* first, publish some messages */ - Channel publish0; - try { - /* create a key */ - KeyGenerator keygen = KeyGenerator.getInstance("AES"); - keygen.init(256); - byte[] key = keygen.generateKey().getEncoded(); - final CipherParams params = Crypto.getDefaultParams(key); + /** + * Publish events with data of various datatypes using text protocol with a 256-bit key + */ + @Test + public void crypto_publish_256() { + /* first, publish some messages */ + Channel publish0; + try { + /* create a key */ + KeyGenerator keygen = KeyGenerator.getInstance("AES"); + keygen.init(256); + byte[] key = keygen.generateKey().getEncoded(); + final CipherParams params = Crypto.getDefaultParams(key); - /* create a channel */ - ChannelOptions channelOpts = new ChannelOptions() {{ encrypted = true; this.cipherParams = params; }}; - publish0 = ably.channels.get("persisted:crypto_publish_256_" + testParams.name, channelOpts); + /* create a channel */ + ChannelOptions channelOpts = new ChannelOptions() {{ encrypted = true; this.cipherParams = params; }}; + publish0 = ably.channels.get("persisted:crypto_publish_256_" + testParams.name, channelOpts); - publish0.publish("publish0", "This is a string message payload"); - publish0.publish("publish1", "This is a byte[] message payload".getBytes()); - } catch(AblyException e) { - e.printStackTrace(); - fail("channelpublish_text: Unexpected exception"); - return; - } catch (NoSuchAlgorithmException e) { - e.printStackTrace(); - fail("init0: Unexpected exception generating key"); - return; - } + publish0.publish("publish0", "This is a string message payload"); + publish0.publish("publish1", "This is a byte[] message payload".getBytes()); + } catch(AblyException e) { + e.printStackTrace(); + fail("channelpublish_text: Unexpected exception"); + return; + } catch (NoSuchAlgorithmException e) { + e.printStackTrace(); + fail("init0: Unexpected exception generating key"); + return; + } - /* get the history for this channel */ - try { - PaginatedResult messages = publish0.history(null); - assertNotNull("Expected non-null messages", messages); - assertEquals("Expected 2 messages", messages.items().length, 2); - HashMap messageContents = new HashMap(); - /* verify message contents */ - for(Message message : messages.items()) - messageContents.put(message.name, message.data); - assertEquals("Expect publish0 to be expected String", messageContents.get("publish0"), "This is a string message payload"); - assertEquals("Expect publish1 to be expected byte[]", new String((byte[])messageContents.get("publish1")), "This is a byte[] message payload"); - } catch (AblyException e) { - e.printStackTrace(); - fail("channelpublish_text: Unexpected exception"); - return; - } - } + /* get the history for this channel */ + try { + PaginatedResult messages = publish0.history(null); + assertNotNull("Expected non-null messages", messages); + assertEquals("Expected 2 messages", messages.items().length, 2); + HashMap messageContents = new HashMap(); + /* verify message contents */ + for(Message message : messages.items()) + messageContents.put(message.name, message.data); + assertEquals("Expect publish0 to be expected String", messageContents.get("publish0"), "This is a string message payload"); + assertEquals("Expect publish1 to be expected byte[]", new String((byte[])messageContents.get("publish1")), "This is a byte[] message payload"); + } catch (AblyException e) { + e.printStackTrace(); + fail("channelpublish_text: Unexpected exception"); + return; + } + } - /** - * Connect twice to the service, using the default (binary) protocol - * and the text protocol. Publish an encrypted message on that channel using - * the default cipher params and verify correct receipt. - */ - @Test - public void crypto_publish_alt() { - /* first, publish some messages */ - Channel tx_publish; - ChannelOptions channelOpts; - String channelName = "persisted:crypto_publish_alt_" + testParams.name; - try { - /* create a key */ - final CipherParams params = Crypto.getDefaultParams(); + /** + * Connect twice to the service, using the default (binary) protocol + * and the text protocol. Publish an encrypted message on that channel using + * the default cipher params and verify correct receipt. + */ + @Test + public void crypto_publish_alt() { + /* first, publish some messages */ + Channel tx_publish; + ChannelOptions channelOpts; + String channelName = "persisted:crypto_publish_alt_" + testParams.name; + try { + /* create a key */ + final CipherParams params = Crypto.getDefaultParams(); - /* create a channel */ - channelOpts = new ChannelOptions() {{ encrypted = true; cipherParams = params; }}; - tx_publish = ably.channels.get(channelName, channelOpts); + /* create a channel */ + channelOpts = new ChannelOptions() {{ encrypted = true; cipherParams = params; }}; + tx_publish = ably.channels.get(channelName, channelOpts); - tx_publish.publish("publish0", "This is a string message payload"); - tx_publish.publish("publish1", "This is a byte[] message payload".getBytes()); - } catch(AblyException e) { - e.printStackTrace(); - fail("channelpublish_text: Unexpected exception"); - return; - } + tx_publish.publish("publish0", "This is a string message payload"); + tx_publish.publish("publish1", "This is a byte[] message payload".getBytes()); + } catch(AblyException e) { + e.printStackTrace(); + fail("channelpublish_text: Unexpected exception"); + return; + } - /* get the history for this channel */ - try { - Channel rx_publish = ably_alt.channels.get(channelName, channelOpts); - PaginatedResult messages = rx_publish.history(null); - assertNotNull("Expected non-null messages", messages); - assertEquals("Expected 2 messages", messages.items().length, 2); - HashMap messageContents = new HashMap(); - /* verify message contents */ - for(Message message : messages.items()) - messageContents.put(message.name, message.data); - assertEquals("Expect publish0 to be expected String", messageContents.get("publish0"), "This is a string message payload"); - assertEquals("Expect publish1 to be expected byte[]", new String((byte[])messageContents.get("publish1")), "This is a byte[] message payload"); - } catch (AblyException e) { - e.printStackTrace(); - fail("channelpublish_text: Unexpected exception"); - return; - } - } + /* get the history for this channel */ + try { + Channel rx_publish = ably_alt.channels.get(channelName, channelOpts); + PaginatedResult messages = rx_publish.history(null); + assertNotNull("Expected non-null messages", messages); + assertEquals("Expected 2 messages", messages.items().length, 2); + HashMap messageContents = new HashMap(); + /* verify message contents */ + for(Message message : messages.items()) + messageContents.put(message.name, message.data); + assertEquals("Expect publish0 to be expected String", messageContents.get("publish0"), "This is a string message payload"); + assertEquals("Expect publish1 to be expected byte[]", new String((byte[])messageContents.get("publish1")), "This is a byte[] message payload"); + } catch (AblyException e) { + e.printStackTrace(); + fail("channelpublish_text: Unexpected exception"); + return; + } + } - /** - * Connect twice to the service, using different cipher keys. - * Publish an encrypted message on that channel using - * the default cipher params and verify that the decrypt failure - * is noticed as bad recovered plaintext. - */ - @Test - public void crypto_publish_key_mismatch() { - /* first, publish some messages */ - Channel tx_publish; - String channelName = "persisted:crypto_publish_key_mismatch_" + testParams.name; - try { - /* create a channel */ - ChannelOptions tx_channelOpts = new ChannelOptions() {{ encrypted = true; }}; - tx_publish = ably.channels.get(channelName, tx_channelOpts); + /** + * Connect twice to the service, using different cipher keys. + * Publish an encrypted message on that channel using + * the default cipher params and verify that the decrypt failure + * is noticed as bad recovered plaintext. + */ + @Test + public void crypto_publish_key_mismatch() { + /* first, publish some messages */ + Channel tx_publish; + String channelName = "persisted:crypto_publish_key_mismatch_" + testParams.name; + try { + /* create a channel */ + ChannelOptions tx_channelOpts = new ChannelOptions() {{ encrypted = true; }}; + tx_publish = ably.channels.get(channelName, tx_channelOpts); - tx_publish.publish("publish0", "This is a string message payload"); - tx_publish.publish("publish1", "This is a byte[] message payload".getBytes()); - } catch(AblyException e) { - e.printStackTrace(); - fail("channelpublish_text: Unexpected exception"); - return; - } + tx_publish.publish("publish0", "This is a string message payload"); + tx_publish.publish("publish1", "This is a byte[] message payload".getBytes()); + } catch(AblyException e) { + e.printStackTrace(); + fail("channelpublish_text: Unexpected exception"); + return; + } - /* get the history for this channel */ - try { - ChannelOptions rx_channelOpts = new ChannelOptions() {{ encrypted = true; }}; - Channel rx_publish = ably.channels.get(channelName, rx_channelOpts); + /* get the history for this channel */ + try { + ChannelOptions rx_channelOpts = new ChannelOptions() {{ encrypted = true; }}; + Channel rx_publish = ably.channels.get(channelName, rx_channelOpts); - PaginatedResult messages = rx_publish.history(new Param[] { new Param("direction", "backwards"), new Param("limit", "2") }); - for (Message failedMessage: messages.items()) - assertTrue("Check decrypt failure", failedMessage.encoding.contains("cipher")); - } catch (AblyException e) { - fail("Didn't expect exception"); - } - } + PaginatedResult messages = rx_publish.history(new Param[] { new Param("direction", "backwards"), new Param("limit", "2") }); + for (Message failedMessage: messages.items()) + assertTrue("Check decrypt failure", failedMessage.encoding.contains("cipher")); + } catch (AblyException e) { + fail("Didn't expect exception"); + } + } - /** - * Connect twice to the service, one with and one without encryption. - * Publish an unencrypted message and verify that the receiving connection - * does not attempt to decrypt it. - */ - @Test - public void crypto_send_unencrypted() { - String channelName = "persisted:crypto_send_unencrypted_" + testParams.name; - /* first, publish some messages */ - try { - /* create a channel */ - Channel tx_publish = ably.channels.get(channelName); + /** + * Connect twice to the service, one with and one without encryption. + * Publish an unencrypted message and verify that the receiving connection + * does not attempt to decrypt it. + */ + @Test + public void crypto_send_unencrypted() { + String channelName = "persisted:crypto_send_unencrypted_" + testParams.name; + /* first, publish some messages */ + try { + /* create a channel */ + Channel tx_publish = ably.channels.get(channelName); - tx_publish.publish("publish0", "This is a string message payload"); - tx_publish.publish("publish1", "This is a byte[] message payload".getBytes()); - } catch(AblyException e) { - e.printStackTrace(); - fail("crypto_send_unencrypted: Unexpected exception"); - return; - } + tx_publish.publish("publish0", "This is a string message payload"); + tx_publish.publish("publish1", "This is a byte[] message payload".getBytes()); + } catch(AblyException e) { + e.printStackTrace(); + fail("crypto_send_unencrypted: Unexpected exception"); + return; + } - /* get the history for this channel */ - try { - ChannelOptions channelOpts = new ChannelOptions() {{ encrypted = true; }}; - Channel rx_publish = ably.channels.get(channelName, channelOpts); - PaginatedResult messages = rx_publish.history(null); - assertNotNull("Expected non-null messages", messages); - assertEquals("Expected 2 messages", messages.items().length, 2); - HashMap messageContents = new HashMap(); - /* verify message contents */ - for(Message message : messages.items()) - messageContents.put(message.name, message.data); - assertEquals("Expect publish0 to be expected String", messageContents.get("publish0"), "This is a string message payload"); - assertEquals("Expect publish1 to be expected byte[]", new String((byte[])messageContents.get("publish1")), "This is a byte[] message payload"); - } catch (AblyException e) { - e.printStackTrace(); - fail("channelpublish_text: Unexpected exception"); - return; - } - } + /* get the history for this channel */ + try { + ChannelOptions channelOpts = new ChannelOptions() {{ encrypted = true; }}; + Channel rx_publish = ably.channels.get(channelName, channelOpts); + PaginatedResult messages = rx_publish.history(null); + assertNotNull("Expected non-null messages", messages); + assertEquals("Expected 2 messages", messages.items().length, 2); + HashMap messageContents = new HashMap(); + /* verify message contents */ + for(Message message : messages.items()) + messageContents.put(message.name, message.data); + assertEquals("Expect publish0 to be expected String", messageContents.get("publish0"), "This is a string message payload"); + assertEquals("Expect publish1 to be expected byte[]", new String((byte[])messageContents.get("publish1")), "This is a byte[] message payload"); + } catch (AblyException e) { + e.printStackTrace(); + fail("channelpublish_text: Unexpected exception"); + return; + } + } - /** - * Connect twice to the service, one with and one without encryption. - * Publish an encrypted message and verify that the receiving connection - * is unable to decrypt it and leaves it as encoded cipher data - */ - @Test - public void crypto_send_encrypted_unhandled() { - String channelName = "persisted:crypto_send_encrypted_unhandled_" + testParams.name; - /* first, publish some messages */ - try { - /* create a channel */ - ChannelOptions channelOpts = new ChannelOptions() {{ encrypted = true; }}; - Channel tx_publish = ably.channels.get(channelName, channelOpts); + /** + * Connect twice to the service, one with and one without encryption. + * Publish an encrypted message and verify that the receiving connection + * is unable to decrypt it and leaves it as encoded cipher data + */ + @Test + public void crypto_send_encrypted_unhandled() { + String channelName = "persisted:crypto_send_encrypted_unhandled_" + testParams.name; + /* first, publish some messages */ + try { + /* create a channel */ + ChannelOptions channelOpts = new ChannelOptions() {{ encrypted = true; }}; + Channel tx_publish = ably.channels.get(channelName, channelOpts); - tx_publish.publish("publish0", "This is a string message payload"); - tx_publish.publish("publish1", "This is a byte[] message payload".getBytes()); - } catch(AblyException e) { - e.printStackTrace(); - fail("channelpublish_text: Unexpected exception"); - return; - } + tx_publish.publish("publish0", "This is a string message payload"); + tx_publish.publish("publish1", "This is a byte[] message payload".getBytes()); + } catch(AblyException e) { + e.printStackTrace(); + fail("channelpublish_text: Unexpected exception"); + return; + } - /* get the history for this channel */ - try { - Channel rx_publish = ably_alt.channels.get(channelName); - PaginatedResult messages = rx_publish.history(null); - assertNotNull("Expected non-null messages", messages); - assertEquals("Expected 2 messages", messages.items().length, 2); - HashMap messageContents = new HashMap(); - /* verify message contents */ - for(Message message : messages.items()) - messageContents.put(message.name, message); - assertTrue("Expect publish0 to be unprocessed CipherData", messageContents.get("publish0").encoding.contains("cipher")); - assertTrue("Expect publish1 to be unprocessed CipherData", messageContents.get("publish1").encoding.contains("cipher")); - } catch (AblyException e) { - e.printStackTrace(); - fail("crypto_send_encrypted_unhandled: Unexpected exception"); - return; - } - } + /* get the history for this channel */ + try { + Channel rx_publish = ably_alt.channels.get(channelName); + PaginatedResult messages = rx_publish.history(null); + assertNotNull("Expected non-null messages", messages); + assertEquals("Expected 2 messages", messages.items().length, 2); + HashMap messageContents = new HashMap(); + /* verify message contents */ + for(Message message : messages.items()) + messageContents.put(message.name, message); + assertTrue("Expect publish0 to be unprocessed CipherData", messageContents.get("publish0").encoding.contains("cipher")); + assertTrue("Expect publish1 to be unprocessed CipherData", messageContents.get("publish1").encoding.contains("cipher")); + } catch (AblyException e) { + e.printStackTrace(); + fail("crypto_send_encrypted_unhandled: Unexpected exception"); + return; + } + } } diff --git a/lib/src/test/java/io/ably/lib/test/rest/RestErrorTest.java b/lib/src/test/java/io/ably/lib/test/rest/RestErrorTest.java index 455ced47e..10f2b7fd6 100644 --- a/lib/src/test/java/io/ably/lib/test/rest/RestErrorTest.java +++ b/lib/src/test/java/io/ably/lib/test/rest/RestErrorTest.java @@ -19,145 +19,145 @@ public class RestErrorTest extends ParameterizedTest { - private static SessionHandlerNanoHTTPD server; - - @BeforeClass - public static void setUp() throws IOException { - /* Create custom RouterNanoHTTPD class for getting session object */ - server = new SessionHandlerNanoHTTPD(27331); - server.start(NanoHTTPD.SOCKET_READ_TIMEOUT, true); - - /* wait for server to start */ - while (!server.wasStarted()) { - try { - Thread.sleep(100); - } catch (InterruptedException e) { - e.printStackTrace(); - } - } - } - - @AfterClass - public static void tearDown() { - server.stop(); - } - - /** - * Verify an href is logged when included in an error.href - * Spec: TI5 - */ - @Test - public void errorHrefWhenPresentInHref() { - final Vector logMessages = new Vector(); - try { - ClientOptions opts = createOptions(testVars.keys[0].keyStr); - opts.environment = null; - opts.tls = false; - opts.port = server.getListeningPort(); - opts.restHost = "localhost"; - opts.logHandler = new Log.LogHandler() { - @Override - public void println(int severity, String tag, String msg, Throwable tr) { - logMessages.add(msg); - } - }; - AblyRest ably = new AblyRest(opts); - - /* make a call that will generate an error */ - ably.stats(new Param[]{new Param("message", encodeURIComponent("Test message")), new Param("href", href(12345))}); - } catch (AblyException e) { - /* verify that the expected error message is present */ - assertTrue(logMessages.get(0).contains(href(12345))); - } - } - - /** - * Verify an href is logged when included in an error.message - * Spec: TI5 - */ - @Test - public void errorHrefWhenPresentInMessage() { - final Vector logMessages = new Vector(); - try { - ClientOptions opts = createOptions(testVars.keys[0].keyStr); - opts.environment = null; - opts.tls = false; - opts.port = server.getListeningPort(); - opts.restHost = "localhost"; - opts.logHandler = new Log.LogHandler() { - @Override - public void println(int severity, String tag, String msg, Throwable tr) { - logMessages.add(msg); - } - }; - AblyRest ably = new AblyRest(opts); - - /* make a call that will generate an error */ - ably.stats(new Param[]{new Param("message", encodeURIComponent("Test message. See " + href(12345)))}); - } catch (AblyException e) { - /* verify that the expected error message is present */ - assertTrue(logMessages.get(0).contains(href(12345))); - } - } - - /** - * Verify an href is logged when derived from error.code - * Spec: TI5 - */ - @Test - public void errorHrefWhenCodePresent() { - final Vector logMessages = new Vector(); - try { - ClientOptions opts = createOptions(testVars.keys[0].keyStr); - opts.environment = null; - opts.tls = false; - opts.port = server.getListeningPort(); - opts.restHost = "localhost"; - opts.logHandler = new Log.LogHandler() { - @Override - public void println(int severity, String tag, String msg, Throwable tr) { - logMessages.add(msg); - } - }; - AblyRest ably = new AblyRest(opts); - - /* make a call that will generate an error */ - ably.stats(new Param[]{new Param("message", encodeURIComponent("Test message")), new Param("code", "12345")}); - } catch (AblyException e) { - /* verify that the expected error message is present */ - assertTrue(logMessages.get(0).contains(href(12345))); - } - } - - private static String href(int code) { return HREF_BASE + String.valueOf(code); } - private static final String HREF_BASE = "https://help.ably.io/error/"; - - private static class SessionHandlerNanoHTTPD extends NanoHTTPD { - Map requestHeaders; - Map requestParams; - - public SessionHandlerNanoHTTPD(int port) { - super(port); - } - - @Override - public Response serve(IHTTPSession session) { - requestHeaders = new HashMap<>(session.getHeaders()); - requestParams = new HashMap<>(session.getParms()); - String code = requestParams.get("code"), - href = requestParams.get("href"), - message = requestParams.get("message"); - - StringBuilder responseBody = new StringBuilder().append("{\"error\":{"); - responseBody.append("\"message\":\"").append(message).append("\""); - if(code != null) { - responseBody.append(",\"code\":\"").append(code).append("\""); - } - if(href != null) { - responseBody.append(",\"href\":\"").append(href).append("\""); - } - responseBody.append("}}"); - return newFixedLengthResponse(Response.Status.BAD_REQUEST, "application/json", responseBody.toString()); - } - } + private static SessionHandlerNanoHTTPD server; + + @BeforeClass + public static void setUp() throws IOException { + /* Create custom RouterNanoHTTPD class for getting session object */ + server = new SessionHandlerNanoHTTPD(27331); + server.start(NanoHTTPD.SOCKET_READ_TIMEOUT, true); + + /* wait for server to start */ + while (!server.wasStarted()) { + try { + Thread.sleep(100); + } catch (InterruptedException e) { + e.printStackTrace(); + } + } + } + + @AfterClass + public static void tearDown() { + server.stop(); + } + + /** + * Verify an href is logged when included in an error.href + * Spec: TI5 + */ + @Test + public void errorHrefWhenPresentInHref() { + final Vector logMessages = new Vector(); + try { + ClientOptions opts = createOptions(testVars.keys[0].keyStr); + opts.environment = null; + opts.tls = false; + opts.port = server.getListeningPort(); + opts.restHost = "localhost"; + opts.logHandler = new Log.LogHandler() { + @Override + public void println(int severity, String tag, String msg, Throwable tr) { + logMessages.add(msg); + } + }; + AblyRest ably = new AblyRest(opts); + + /* make a call that will generate an error */ + ably.stats(new Param[]{new Param("message", encodeURIComponent("Test message")), new Param("href", href(12345))}); + } catch (AblyException e) { + /* verify that the expected error message is present */ + assertTrue(logMessages.get(0).contains(href(12345))); + } + } + + /** + * Verify an href is logged when included in an error.message + * Spec: TI5 + */ + @Test + public void errorHrefWhenPresentInMessage() { + final Vector logMessages = new Vector(); + try { + ClientOptions opts = createOptions(testVars.keys[0].keyStr); + opts.environment = null; + opts.tls = false; + opts.port = server.getListeningPort(); + opts.restHost = "localhost"; + opts.logHandler = new Log.LogHandler() { + @Override + public void println(int severity, String tag, String msg, Throwable tr) { + logMessages.add(msg); + } + }; + AblyRest ably = new AblyRest(opts); + + /* make a call that will generate an error */ + ably.stats(new Param[]{new Param("message", encodeURIComponent("Test message. See " + href(12345)))}); + } catch (AblyException e) { + /* verify that the expected error message is present */ + assertTrue(logMessages.get(0).contains(href(12345))); + } + } + + /** + * Verify an href is logged when derived from error.code + * Spec: TI5 + */ + @Test + public void errorHrefWhenCodePresent() { + final Vector logMessages = new Vector(); + try { + ClientOptions opts = createOptions(testVars.keys[0].keyStr); + opts.environment = null; + opts.tls = false; + opts.port = server.getListeningPort(); + opts.restHost = "localhost"; + opts.logHandler = new Log.LogHandler() { + @Override + public void println(int severity, String tag, String msg, Throwable tr) { + logMessages.add(msg); + } + }; + AblyRest ably = new AblyRest(opts); + + /* make a call that will generate an error */ + ably.stats(new Param[]{new Param("message", encodeURIComponent("Test message")), new Param("code", "12345")}); + } catch (AblyException e) { + /* verify that the expected error message is present */ + assertTrue(logMessages.get(0).contains(href(12345))); + } + } + + private static String href(int code) { return HREF_BASE + String.valueOf(code); } + private static final String HREF_BASE = "https://help.ably.io/error/"; + + private static class SessionHandlerNanoHTTPD extends NanoHTTPD { + Map requestHeaders; + Map requestParams; + + public SessionHandlerNanoHTTPD(int port) { + super(port); + } + + @Override + public Response serve(IHTTPSession session) { + requestHeaders = new HashMap<>(session.getHeaders()); + requestParams = new HashMap<>(session.getParms()); + String code = requestParams.get("code"), + href = requestParams.get("href"), + message = requestParams.get("message"); + + StringBuilder responseBody = new StringBuilder().append("{\"error\":{"); + responseBody.append("\"message\":\"").append(message).append("\""); + if(code != null) { + responseBody.append(",\"code\":\"").append(code).append("\""); + } + if(href != null) { + responseBody.append(",\"href\":\"").append(href).append("\""); + } + responseBody.append("}}"); + return newFixedLengthResponse(Response.Status.BAD_REQUEST, "application/json", responseBody.toString()); + } + } } diff --git a/lib/src/test/java/io/ably/lib/test/rest/RestInitTest.java b/lib/src/test/java/io/ably/lib/test/rest/RestInitTest.java index 61fe4490f..577dc5561 100644 --- a/lib/src/test/java/io/ably/lib/test/rest/RestInitTest.java +++ b/lib/src/test/java/io/ably/lib/test/rest/RestInitTest.java @@ -22,308 +22,308 @@ public class RestInitTest { - /** - * Init library with a key only - */ - @Test - public void init_key_string() { - try { - TestVars testVars = Setup.getTestVars(); - new AblyRest(testVars.keys[0].keyStr); - } catch (AblyException e) { - e.printStackTrace(); - fail("init0: Unexpected exception instantiating library"); - } - } + /** + * Init library with a key only + */ + @Test + public void init_key_string() { + try { + TestVars testVars = Setup.getTestVars(); + new AblyRest(testVars.keys[0].keyStr); + } catch (AblyException e) { + e.printStackTrace(); + fail("init0: Unexpected exception instantiating library"); + } + } - /** - * Init library with a null key (RSC1) - */ - @Test - public void init_null_key_string() { - try { - String key = null; - new AblyRest(key); - fail("init_null_key_string: Expected AblyException to be thrown when instantiating library with null key"); - } catch (AblyException e) {} - } + /** + * Init library with a null key (RSC1) + */ + @Test + public void init_null_key_string() { + try { + String key = null; + new AblyRest(key); + fail("init_null_key_string: Expected AblyException to be thrown when instantiating library with null key"); + } catch (AblyException e) {} + } - /** - * Init library with a key in options (RSC1) - */ - @Test - public void init_key_opts() { - try { - String sampleKey = "sample:key"; - ClientOptions opts = new ClientOptions(sampleKey); - new AblyRest(opts); - } catch (AblyException e) { - e.printStackTrace(); - fail("init_key_opts: Unexpected exception instantiating library"); - } - } + /** + * Init library with a key in options (RSC1) + */ + @Test + public void init_key_opts() { + try { + String sampleKey = "sample:key"; + ClientOptions opts = new ClientOptions(sampleKey); + new AblyRest(opts); + } catch (AblyException e) { + e.printStackTrace(); + fail("init_key_opts: Unexpected exception instantiating library"); + } + } - /** - * Init library with a null key in options (RSC1) - */ - @Test - public void init_null_key_opts() { - try { - ClientOptions opts = new ClientOptions(null); - new AblyRest(opts); - fail("init_null_key_opts: Expected AblyException to be thrown when instantiating library with null key in options"); - } catch (AblyException e) {} - } + /** + * Init library with a null key in options (RSC1) + */ + @Test + public void init_null_key_opts() { + try { + ClientOptions opts = new ClientOptions(null); + new AblyRest(opts); + fail("init_null_key_opts: Expected AblyException to be thrown when instantiating library with null key in options"); + } catch (AblyException e) {} + } - /** - * Init library with key string - */ - @Test - public void init_key() { - try { - TestVars testVars = Setup.getTestVars(); - ClientOptions opts = new ClientOptions(testVars.keys[0].keyStr); - new AblyRest(opts); - } catch (AblyException e) { - e.printStackTrace(); - fail("init2: Unexpected exception instantiating library"); - } - } + /** + * Init library with key string + */ + @Test + public void init_key() { + try { + TestVars testVars = Setup.getTestVars(); + ClientOptions opts = new ClientOptions(testVars.keys[0].keyStr); + new AblyRest(opts); + } catch (AblyException e) { + e.printStackTrace(); + fail("init2: Unexpected exception instantiating library"); + } + } - /** - * Init library with no authentication mechanism - * Spec: RSC1 - */ - @Test - public void init_no_auth() { - try { - ClientOptions opts = new ClientOptions(); - new AblyRest(opts); - fail("init2: Unexpected success instantiating library"); - } catch (AblyException e) { - ErrorInfo err = e.errorInfo; - assertEquals("Verify expected error code", err.statusCode, 400); - } - } + /** + * Init library with no authentication mechanism + * Spec: RSC1 + */ + @Test + public void init_no_auth() { + try { + ClientOptions opts = new ClientOptions(); + new AblyRest(opts); + fail("init2: Unexpected success instantiating library"); + } catch (AblyException e) { + ErrorInfo err = e.errorInfo; + assertEquals("Verify expected error code", err.statusCode, 400); + } + } - /** - * Init library with specified host - */ - @Test - public void init_host() { - try { - String hostExpected = "some.other.host"; + /** + * Init library with specified host + */ + @Test + public void init_host() { + try { + String hostExpected = "some.other.host"; - TestVars testVars = Setup.getTestVars(); - ClientOptions opts = new ClientOptions(testVars.keys[0].keyStr); - opts.restHost = hostExpected; - AblyRest ably = new AblyRest(opts); - assertEquals("Unexpected host mismatch", hostExpected, ably.options.restHost); - } catch (AblyException e) { - e.printStackTrace(); - fail("init4: Unexpected exception instantiating library"); - } - } + TestVars testVars = Setup.getTestVars(); + ClientOptions opts = new ClientOptions(testVars.keys[0].keyStr); + opts.restHost = hostExpected; + AblyRest ably = new AblyRest(opts); + assertEquals("Unexpected host mismatch", hostExpected, ably.options.restHost); + } catch (AblyException e) { + e.printStackTrace(); + fail("init4: Unexpected exception instantiating library"); + } + } - /** - * Init library with specified port - */ - @SuppressWarnings("unused") - @Test - public void init_port() { - try { - TestVars testVars = Setup.getTestVars(); - ClientOptions opts = new ClientOptions(testVars.keys[0].keyStr); - opts.port = 9998; - opts.tlsPort = 9999; - AblyRest ably = new AblyRest(opts); - assertEquals("Unexpected port mismatch", Defaults.getPort(opts), opts.tlsPort); - } catch (AblyException e) { - e.printStackTrace(); - fail("init5: Unexpected exception instantiating library"); - } - } + /** + * Init library with specified port + */ + @SuppressWarnings("unused") + @Test + public void init_port() { + try { + TestVars testVars = Setup.getTestVars(); + ClientOptions opts = new ClientOptions(testVars.keys[0].keyStr); + opts.port = 9998; + opts.tlsPort = 9999; + AblyRest ably = new AblyRest(opts); + assertEquals("Unexpected port mismatch", Defaults.getPort(opts), opts.tlsPort); + } catch (AblyException e) { + e.printStackTrace(); + fail("init5: Unexpected exception instantiating library"); + } + } - /** - * Verify tls defaults to true - */ - @SuppressWarnings("unused") - @Test - public void init_default_secure() { - try { - TestVars testVars = Setup.getTestVars(); - ClientOptions opts = new ClientOptions(testVars.keys[0].keyStr); - AblyRest ably = new AblyRest(opts); - assertEquals("Unexpected port mismatch", Defaults.getPort(opts), Defaults.TLS_PORT); - } catch (AblyException e) { - e.printStackTrace(); - fail("init6: Unexpected exception instantiating library"); - } - } + /** + * Verify tls defaults to true + */ + @SuppressWarnings("unused") + @Test + public void init_default_secure() { + try { + TestVars testVars = Setup.getTestVars(); + ClientOptions opts = new ClientOptions(testVars.keys[0].keyStr); + AblyRest ably = new AblyRest(opts); + assertEquals("Unexpected port mismatch", Defaults.getPort(opts), Defaults.TLS_PORT); + } catch (AblyException e) { + e.printStackTrace(); + fail("init6: Unexpected exception instantiating library"); + } + } - /** - * Verify tls can be set to false - */ - @SuppressWarnings("unused") - @Test - public void init_insecure() { - try { - TestVars testVars = Setup.getTestVars(); - ClientOptions opts = new ClientOptions(testVars.keys[0].keyStr); - opts.tls = false; - AblyRest ably = new AblyRest(opts); - assertEquals("Unexpected scheme mismatch", Defaults.getPort(opts), Defaults.PORT); - } catch (AblyException e) { - e.printStackTrace(); - fail("init7: Unexpected exception instantiating library"); - } - } + /** + * Verify tls can be set to false + */ + @SuppressWarnings("unused") + @Test + public void init_insecure() { + try { + TestVars testVars = Setup.getTestVars(); + ClientOptions opts = new ClientOptions(testVars.keys[0].keyStr); + opts.tls = false; + AblyRest ably = new AblyRest(opts); + assertEquals("Unexpected scheme mismatch", Defaults.getPort(opts), Defaults.PORT); + } catch (AblyException e) { + e.printStackTrace(); + fail("init7: Unexpected exception instantiating library"); + } + } - /** - * Init with default log level - * Spec: RSC2 - */ - @Test - public void init_defaultLogLevel() { - try { - TestVars testVars = Setup.getTestVars(); - ClientOptions opts = new ClientOptions(testVars.keys[0].keyStr); - assertEquals("Verify default log level is WARN", opts.logLevel, Log.WARN); - } catch (AblyException e) { - e.printStackTrace(); - fail("init8: Unexpected exception instantiating library"); - } - } + /** + * Init with default log level + * Spec: RSC2 + */ + @Test + public void init_defaultLogLevel() { + try { + TestVars testVars = Setup.getTestVars(); + ClientOptions opts = new ClientOptions(testVars.keys[0].keyStr); + assertEquals("Verify default log level is WARN", opts.logLevel, Log.WARN); + } catch (AblyException e) { + e.printStackTrace(); + fail("init8: Unexpected exception instantiating library"); + } + } - /** - * Init with log handler; check called - * Spec: RSC3, RSC4 - */ - private boolean init8_logCalled; - @Test - public void init_log_handler() { - try { - TestVars testVars = Setup.getTestVars(); - ClientOptions opts = new ClientOptions(testVars.keys[0].keyStr); - opts.logHandler = new LogHandler() { - @Override - public void println(int severity, String tag, String msg, Throwable tr) { - init8_logCalled = true; - System.out.println(msg); - } - }; - opts.logLevel = Log.VERBOSE; - new AblyRest(opts); - assertTrue("Log handler not called", init8_logCalled); - } catch (AblyException e) { - e.printStackTrace(); - fail("init8: Unexpected exception instantiating library"); - } - } + /** + * Init with log handler; check called + * Spec: RSC3, RSC4 + */ + private boolean init8_logCalled; + @Test + public void init_log_handler() { + try { + TestVars testVars = Setup.getTestVars(); + ClientOptions opts = new ClientOptions(testVars.keys[0].keyStr); + opts.logHandler = new LogHandler() { + @Override + public void println(int severity, String tag, String msg, Throwable tr) { + init8_logCalled = true; + System.out.println(msg); + } + }; + opts.logLevel = Log.VERBOSE; + new AblyRest(opts); + assertTrue("Log handler not called", init8_logCalled); + } catch (AblyException e) { + e.printStackTrace(); + fail("init8: Unexpected exception instantiating library"); + } + } - /** - * Init with log handler; check not called if logLevel == NONE - */ - private boolean init9_logCalled; - @Test - public void init_log_level() { - try { - TestVars testVars = Setup.getTestVars(); - ClientOptions opts = new ClientOptions(testVars.keys[0].keyStr); - opts.logHandler = new LogHandler() { - @Override - public void println(int severity, String tag, String msg, Throwable tr) { - init9_logCalled = true; - System.out.println(msg); - } - }; - opts.logLevel = Log.NONE; - new AblyRest(opts); - assertFalse("Log handler incorrectly called", init9_logCalled); - } catch (AblyException e) { - e.printStackTrace(); - fail("init9: Unexpected exception instantiating library"); - } - } + /** + * Init with log handler; check not called if logLevel == NONE + */ + private boolean init9_logCalled; + @Test + public void init_log_level() { + try { + TestVars testVars = Setup.getTestVars(); + ClientOptions opts = new ClientOptions(testVars.keys[0].keyStr); + opts.logHandler = new LogHandler() { + @Override + public void println(int severity, String tag, String msg, Throwable tr) { + init9_logCalled = true; + System.out.println(msg); + } + }; + opts.logLevel = Log.NONE; + new AblyRest(opts); + assertFalse("Log handler incorrectly called", init9_logCalled); + } catch (AblyException e) { + e.printStackTrace(); + fail("init9: Unexpected exception instantiating library"); + } + } - /** - * Check that the logger outputs to System.out by default (RSC2) - */ - @Test - public void init_default_log_output_stream() { - ByteArrayOutputStream outContent = new ByteArrayOutputStream(); - PrintStream newTarget = new PrintStream(outContent); - PrintStream oldTarget = System.out; - /* Log level was changed in above tests, turning in back to defaults */ - Log.setLevel(Log.defaultLevel); - try { - System.setOut(newTarget); + /** + * Check that the logger outputs to System.out by default (RSC2) + */ + @Test + public void init_default_log_output_stream() { + ByteArrayOutputStream outContent = new ByteArrayOutputStream(); + PrintStream newTarget = new PrintStream(outContent); + PrintStream oldTarget = System.out; + /* Log level was changed in above tests, turning in back to defaults */ + Log.setLevel(Log.defaultLevel); + try { + System.setOut(newTarget); - Log.w(null, "hello"); - System.out.flush(); - String logContent = outContent.toString(); - /* \n or \r\n at the end because logs are printed with println() function */ - assertEquals("(WARN): hello" + System.lineSeparator(), logContent); - } finally { - System.setOut(oldTarget); - } - } + Log.w(null, "hello"); + System.out.flush(); + String logContent = outContent.toString(); + /* \n or \r\n at the end because logs are printed with println() function */ + assertEquals("(WARN): hello" + System.lineSeparator(), logContent); + } finally { + System.setOut(oldTarget); + } + } - /** - * Init library with 'production' environment - * Spec: RSC11 - */ - @Test - public void init_production_environment() { - try { - TestVars testVars = Setup.getTestVars(); - ClientOptions opts = new ClientOptions(testVars.keys[0].keyStr); - opts.environment = "production"; - AblyRest ably = new AblyRest(opts); - assertEquals("Unexpected host mismatch", Defaults.HOST_REST, ably.httpCore.getPrimaryHost()); - } catch (AblyException e) { - e.printStackTrace(); - fail("init4: Unexpected exception instantiating library"); - } - } + /** + * Init library with 'production' environment + * Spec: RSC11 + */ + @Test + public void init_production_environment() { + try { + TestVars testVars = Setup.getTestVars(); + ClientOptions opts = new ClientOptions(testVars.keys[0].keyStr); + opts.environment = "production"; + AblyRest ably = new AblyRest(opts); + assertEquals("Unexpected host mismatch", Defaults.HOST_REST, ably.httpCore.getPrimaryHost()); + } catch (AblyException e) { + e.printStackTrace(); + fail("init4: Unexpected exception instantiating library"); + } + } - /** - * Init library with given environment - * Spec: RSC11 - */ - @Test - public void init_given_environment() { - final String givenEnvironment = "staging"; - try { - TestVars testVars = Setup.getTestVars(); - ClientOptions opts = new ClientOptions(testVars.keys[0].keyStr); - opts.environment = givenEnvironment; - AblyRest ably = new AblyRest(opts); - assertEquals("Unexpected host mismatch", String.format("%s-%s", givenEnvironment, Defaults.HOST_REST), ably.httpCore.getPrimaryHost()); - } catch (AblyException e) { - e.printStackTrace(); - fail("init4: Unexpected exception instantiating library"); - } - } + /** + * Init library with given environment + * Spec: RSC11 + */ + @Test + public void init_given_environment() { + final String givenEnvironment = "staging"; + try { + TestVars testVars = Setup.getTestVars(); + ClientOptions opts = new ClientOptions(testVars.keys[0].keyStr); + opts.environment = givenEnvironment; + AblyRest ably = new AblyRest(opts); + assertEquals("Unexpected host mismatch", String.format("%s-%s", givenEnvironment, Defaults.HOST_REST), ably.httpCore.getPrimaryHost()); + } catch (AblyException e) { + e.printStackTrace(); + fail("init4: Unexpected exception instantiating library"); + } + } - /** - * Init library with given environment and specified host - * Spec: RSC11 - */ - @Test - public void init_given_host_environment() { - final String givenEnvironment = "staging"; - final String specifiedHost = "fake.ably.io"; - try { - TestVars testVars = Setup.getTestVars(); - ClientOptions opts = new ClientOptions(testVars.keys[0].keyStr); - opts.restHost = specifiedHost; - opts.environment = givenEnvironment; - AblyRest ably = new AblyRest(opts); - fail("init4: Expected exception instantiating library"); - assertEquals("Unexpected host mismatch", specifiedHost, ably.options.restHost); - } catch (AblyException e) { - /* pass: Got exception from setting restHost and environment */ - } - } + /** + * Init library with given environment and specified host + * Spec: RSC11 + */ + @Test + public void init_given_host_environment() { + final String givenEnvironment = "staging"; + final String specifiedHost = "fake.ably.io"; + try { + TestVars testVars = Setup.getTestVars(); + ClientOptions opts = new ClientOptions(testVars.keys[0].keyStr); + opts.restHost = specifiedHost; + opts.environment = givenEnvironment; + AblyRest ably = new AblyRest(opts); + fail("init4: Expected exception instantiating library"); + assertEquals("Unexpected host mismatch", specifiedHost, ably.options.restHost); + } catch (AblyException e) { + /* pass: Got exception from setting restHost and environment */ + } + } } diff --git a/lib/src/test/java/io/ably/lib/test/rest/RestJWTTest.java b/lib/src/test/java/io/ably/lib/test/rest/RestJWTTest.java index f40495676..91787d4ba 100644 --- a/lib/src/test/java/io/ably/lib/test/rest/RestJWTTest.java +++ b/lib/src/test/java/io/ably/lib/test/rest/RestJWTTest.java @@ -15,150 +15,150 @@ public class RestJWTTest extends ParameterizedTest { - private Key key = testVars.keys[0]; - Param[] environment = new Param[]{ new Param("environment", testVars.environment) }; - Param[] validKeys = new Param[]{ new Param("keyName", key.keyName), new Param("keySecret", key.keySecret) }; - Param[] invalidKeys = new Param[]{ new Param("keyName", key.keyName), new Param("keySecret", "invalidinvalid") }; - Param[] tokenEmbedded = new Param[]{ new Param("jwtType", "embedded") }; - Param[] tokenEmbeddedAndEncrypted = new Param[]{ new Param("jwtType", "embedded"), new Param("encrypted", 1) }; - Param[] jwtReturnType = new Param[]{ new Param("returnType", "jwt") }; - private static final String echoServer = "https://echo.ably.io/createJWT"; + private Key key = testVars.keys[0]; + Param[] environment = new Param[]{ new Param("environment", testVars.environment) }; + Param[] validKeys = new Param[]{ new Param("keyName", key.keyName), new Param("keySecret", key.keySecret) }; + Param[] invalidKeys = new Param[]{ new Param("keyName", key.keyName), new Param("keySecret", "invalidinvalid") }; + Param[] tokenEmbedded = new Param[]{ new Param("jwtType", "embedded") }; + Param[] tokenEmbeddedAndEncrypted = new Param[]{ new Param("jwtType", "embedded"), new Param("encrypted", 1) }; + Param[] jwtReturnType = new Param[]{ new Param("returnType", "jwt") }; + private static final String echoServer = "https://echo.ably.io/createJWT"; - /** - * Base request of a JWT token (RSA8g RSA8c) - */ - @Test - public void auth_jwt_request() { - try { - ClientOptions options = buildClientOptions(validKeys); - AblyRest client = new AblyRest(options); - PaginatedResult stats = client.stats(null); - assertNotNull("Stats should not be null", stats); - } catch (AblyException e) { - e.printStackTrace(); - fail("auth_jwt_request: Unexpected exception"); - } - } + /** + * Base request of a JWT token (RSA8g RSA8c) + */ + @Test + public void auth_jwt_request() { + try { + ClientOptions options = buildClientOptions(validKeys); + AblyRest client = new AblyRest(options); + PaginatedResult stats = client.stats(null); + assertNotNull("Stats should not be null", stats); + } catch (AblyException e) { + e.printStackTrace(); + fail("auth_jwt_request: Unexpected exception"); + } + } - /** - * Base request of a JWT token with wrong credentials (RSA8g RSA8c) - */ - @Test - public void auth_jwt_request_wrong_keys() { - try { - ClientOptions options = buildClientOptions(invalidKeys); - AblyRest client = new AblyRest(options); - PaginatedResult stats = client.stats(null); - } catch (AblyException e) { - assertEquals("Unexpected code from exception", 40144, e.errorInfo.code); - assertEquals("Unexpected statusCode from exception", 401, e.errorInfo.statusCode); - assertTrue("Error message not matching the expected one", e.errorInfo.message.contains("signature verification failed")); - } - } + /** + * Base request of a JWT token with wrong credentials (RSA8g RSA8c) + */ + @Test + public void auth_jwt_request_wrong_keys() { + try { + ClientOptions options = buildClientOptions(invalidKeys); + AblyRest client = new AblyRest(options); + PaginatedResult stats = client.stats(null); + } catch (AblyException e) { + assertEquals("Unexpected code from exception", 40144, e.errorInfo.code); + assertEquals("Unexpected statusCode from exception", 401, e.errorInfo.statusCode); + assertTrue("Error message not matching the expected one", e.errorInfo.message.contains("signature verification failed")); + } + } - /** - * Request of a JWT token that embeds and Ably token (RSC1 RSC1a RSC1c RSA3d) - */ - @Test - public void auth_jwt_request_embedded_token() { - try { - ClientOptions options = buildClientOptions(mergeParams(new Param[][]{environment, validKeys, tokenEmbedded})); - AblyRest client = new AblyRest(options); - PaginatedResult stats = client.stats(null); - assertNotNull("Stats should not be null", stats); - } catch (AblyException e) { - e.printStackTrace(); - fail("auth_jwt_request_embedded_token: Unexpected exception"); - } - } + /** + * Request of a JWT token that embeds and Ably token (RSC1 RSC1a RSC1c RSA3d) + */ + @Test + public void auth_jwt_request_embedded_token() { + try { + ClientOptions options = buildClientOptions(mergeParams(new Param[][]{environment, validKeys, tokenEmbedded})); + AblyRest client = new AblyRest(options); + PaginatedResult stats = client.stats(null); + assertNotNull("Stats should not be null", stats); + } catch (AblyException e) { + e.printStackTrace(); + fail("auth_jwt_request_embedded_token: Unexpected exception"); + } + } - /** - * Request of a JWT token that embeds and Ably token and is encrypted (RSC1 RSC1a RSC1c RSA3d) - */ - @Test - public void auth_jwt_request_embedded_token_encrypted() { - try { - ClientOptions options = buildClientOptions(mergeParams(new Param[][]{environment, validKeys, tokenEmbeddedAndEncrypted})); - AblyRest client = new AblyRest(options); - PaginatedResult stats = client.stats(null); - assertNotNull("Stats should not be null", stats); - } catch (AblyException e) { - e.printStackTrace(); - fail("auth_jwt_request_embedded_token_encrypted: Unexpected exception"); - } - } + /** + * Request of a JWT token that embeds and Ably token and is encrypted (RSC1 RSC1a RSC1c RSA3d) + */ + @Test + public void auth_jwt_request_embedded_token_encrypted() { + try { + ClientOptions options = buildClientOptions(mergeParams(new Param[][]{environment, validKeys, tokenEmbeddedAndEncrypted})); + AblyRest client = new AblyRest(options); + PaginatedResult stats = client.stats(null); + assertNotNull("Stats should not be null", stats); + } catch (AblyException e) { + e.printStackTrace(); + fail("auth_jwt_request_embedded_token_encrypted: Unexpected exception"); + } + } - /** - * Request of a JWT token that is returned with application/jwt content type (RSA4f, RSA8c) - */ - @Test - public void auth_jwt_request_returntype() { - try { - ClientOptions options = createOptions(); - options.authUrl = echoServer; - options.authParams = mergeParams(new Param[][]{environment, validKeys, jwtReturnType}); - AblyRest client = new AblyRest(options); - PaginatedResult stats = client.stats(null); - assertNotNull("Stats should not be null", stats); - } catch (AblyException e) { - e.printStackTrace(); - fail("auth_jwt_request_returntype: Unexpected exception"); - } - } + /** + * Request of a JWT token that is returned with application/jwt content type (RSA4f, RSA8c) + */ + @Test + public void auth_jwt_request_returntype() { + try { + ClientOptions options = createOptions(); + options.authUrl = echoServer; + options.authParams = mergeParams(new Param[][]{environment, validKeys, jwtReturnType}); + AblyRest client = new AblyRest(options); + PaginatedResult stats = client.stats(null); + assertNotNull("Stats should not be null", stats); + } catch (AblyException e) { + e.printStackTrace(); + fail("auth_jwt_request_returntype: Unexpected exception"); + } + } - /** - * Request of a JWT token via authCallback (RSA8g) - */ - @Test - public void auth_jwt_request_authcallback() { - try { - final AblyRest restJWTRequester = new AblyRest(createOptions(testVars.keys[0].keyStr)); - final boolean[] callbackCalled = new boolean[] { false }; - TokenCallback authCallback = new TokenCallback() { - @Override - public Object getTokenRequest(TokenParams params) throws AblyException { - callbackCalled[0] = true; - return restJWTRequester.auth.requestToken(params, null); - } - }; - ClientOptions optionsWithCallback = createOptions(); - optionsWithCallback.authCallback = authCallback; - AblyRest client = new AblyRest(optionsWithCallback); - PaginatedResult stats = client.stats(null); - assertNotNull("Stats should not be null", stats); - assertTrue("Callback was not called", callbackCalled[0]); - } catch (AblyException e) { - e.printStackTrace(); - fail("auth_jwt_request_authcallback: Unexpected exception"); - } - } + /** + * Request of a JWT token via authCallback (RSA8g) + */ + @Test + public void auth_jwt_request_authcallback() { + try { + final AblyRest restJWTRequester = new AblyRest(createOptions(testVars.keys[0].keyStr)); + final boolean[] callbackCalled = new boolean[] { false }; + TokenCallback authCallback = new TokenCallback() { + @Override + public Object getTokenRequest(TokenParams params) throws AblyException { + callbackCalled[0] = true; + return restJWTRequester.auth.requestToken(params, null); + } + }; + ClientOptions optionsWithCallback = createOptions(); + optionsWithCallback.authCallback = authCallback; + AblyRest client = new AblyRest(optionsWithCallback); + PaginatedResult stats = client.stats(null); + assertNotNull("Stats should not be null", stats); + assertTrue("Callback was not called", callbackCalled[0]); + } catch (AblyException e) { + e.printStackTrace(); + fail("auth_jwt_request_authcallback: Unexpected exception"); + } + } - /** - * Helper to fetch a token with params via authUrl - */ - private ClientOptions buildClientOptions(Param[] params) { - try { - ClientOptions options = createOptions(); - final String[] resultToken = new String[1]; - AblyRest rest = new AblyRest(createOptions(testVars.keys[0].keyStr)); - HttpHelpers.getUri(rest.httpCore, echoServer, null, params, new HttpCore.ResponseHandler() { - @Override - public Object handleResponse(HttpCore.Response response, ErrorInfo error) throws AblyException { - try { - resultToken[0] = new String(response.body, "UTF-8"); - } catch (UnsupportedEncodingException e) { - e.printStackTrace(); - fail("Error in fetching a JWT token " + e); - } - return null; - } - }); - options.token = resultToken[0]; - return options; - } catch (AblyException e) { - fail("Failure in fetching a JWT token" + e); - return null; - } - } + /** + * Helper to fetch a token with params via authUrl + */ + private ClientOptions buildClientOptions(Param[] params) { + try { + ClientOptions options = createOptions(); + final String[] resultToken = new String[1]; + AblyRest rest = new AblyRest(createOptions(testVars.keys[0].keyStr)); + HttpHelpers.getUri(rest.httpCore, echoServer, null, params, new HttpCore.ResponseHandler() { + @Override + public Object handleResponse(HttpCore.Response response, ErrorInfo error) throws AblyException { + try { + resultToken[0] = new String(response.body, "UTF-8"); + } catch (UnsupportedEncodingException e) { + e.printStackTrace(); + fail("Error in fetching a JWT token " + e); + } + return null; + } + }); + options.token = resultToken[0]; + return options; + } catch (AblyException e) { + fail("Failure in fetching a JWT token" + e); + return null; + } + } } diff --git a/lib/src/test/java/io/ably/lib/test/rest/RestPresenceTest.java b/lib/src/test/java/io/ably/lib/test/rest/RestPresenceTest.java index 72d3c941e..40d87748d 100644 --- a/lib/src/test/java/io/ably/lib/test/rest/RestPresenceTest.java +++ b/lib/src/test/java/io/ably/lib/test/rest/RestPresenceTest.java @@ -20,310 +20,310 @@ public class RestPresenceTest extends ParameterizedTest { - private static final String[] clientIds = new String[] { - "client_string_0", - "client_string_1", - "client_string_2", - "client_string_3" - }; - - private AblyRest ably_text; - - @Before - public void setUpBefore() throws Exception { - ClientOptions opts_text = createOptions(testVars.keys[0].keyStr); - ably_text = new AblyRest(opts_text); - } - - /** - * Get member data of various datatypes - */ - @Test - public void rest_getpresence() { - String channelName = "restpresence_notpersisted"; - /* get channel */ - Channel channel = ably_text.channels.get(channelName); - try { - PresenceMessage[] members = channel.presence.get(null).items(); - assertNotNull("Expected non-null messages", members); - assertEquals("Expected 1 message", members.length, 1); - - /* verify presence contents */ - assertEquals("Expect data to be expected String", members[0].data, "This is a string data payload"); - } catch(AblyException e) { - e.printStackTrace(); - fail("rest_getpresence: Unexpected exception"); - return; - } - } - - /** - * Get presence history data of various datatypes - */ - @Test - public void rest_presencehistory_simple() { - String channelName = "persisted:restpresence_persisted"; - /* get channel */ - Channel channel = ably_text.channels.get(channelName); - try { - /* get the history for this channel */ - PaginatedResult members = channel.presence.history(new Param[]{ new Param("direction", "forwards") }); - assertNotNull("Expected non-null messages", members); - assertEquals("Expected 4 messages", members.items().length, 4); - - /* verify presence contents */ - HashMap memberData = new HashMap(); - for(PresenceMessage member : members.items()) - memberData.put(member.clientId, member.data); - assertEquals("Expect client_string_0 to be expected String", memberData.get("client_string_0"), "This is a string data payload"); - } catch(AblyException e) { - e.printStackTrace(); - fail("rest_presencehistory_simple: Unexpected exception"); - return; - } - } - - /** - * Get presence history data in the forward direction and check order - * DISABLED: See issue https://github.com/ably/ably-java/issues/159 - */ - /*@Test*/ - public void rest_presencehistory_order_f() { - String channelName = "persisted:restpresence_persisted"; - /* get channel */ - Channel channel = ably_text.channels.get(channelName); - try { - /* get the history for this channel */ - PaginatedResult members = channel.presence.history(new Param[]{ new Param("direction", "forwards") }); - assertNotNull("Expected non-null messages", members); - assertEquals("Expected 4 messages", members.items().length, 4); - - /* verify message order */ - for(int i = 0; i < members.items().length; i++) { - PresenceMessage member = members.items()[i]; - assertEquals("Verify expected member (" + i + ')', clientIds[i], member.clientId); - } - } catch(AblyException e) { - e.printStackTrace(); - fail("rest_presencehistory_order_f: Unexpected exception"); - return; - } - } - - /** - * Get presence history data in the backwards direction using text protocol and check order - * DISABLED: See issue https://github.com/ably/ably-java/issues/159 - */ - /*@Test*/ - public void rest_presencehistory_order_b() { - String channelName = "persisted:restpresence_persisted"; - /* get channel */ - Channel channel = ably_text.channels.get(channelName); - try { - /* get the history for this channel */ - PaginatedResult members = channel.presence.history(new Param[]{ new Param("direction", "backwards") }); - assertNotNull("Expected non-null messages", members); - assertEquals("Expected 4 messages", members.items().length, 4); - - /* verify message order */ - for(int i = 0; i < members.items().length; i++) { - PresenceMessage member = members.items()[i]; - assertEquals("Verify expected member (" + i + ')', clientIds[3 - i], member.clientId); - } - } catch(AblyException e) { - e.printStackTrace(); - fail("rest_presencehistory_order_b: Unexpected exception"); - return; - } - } - - /** - * Get limited presence history data in the forward direction using text protocol and check order - * DISABLED: See issue https://github.com/ably/ably-java/issues/159 - */ - /*@Test*/ - public void rest_presencehistory_limit_f() { - String channelName = "persisted:restpresence_persisted"; - /* get channel */ - Channel channel = ably_text.channels.get(channelName); - try { - /* get the history for this channel */ - PaginatedResult members = channel.presence.history(new Param[]{ new Param("direction", "forwards"), new Param("limit", "2") }); - assertNotNull("Expected non-null messages", members); - assertEquals("Expected 2 messages", members.items().length, 2); - - /* verify message order */ - for(int i = 0; i < members.items().length; i++) { - PresenceMessage member = members.items()[i]; - assertEquals("Verify expected member (" + i + ')', clientIds[i], member.clientId); - } - } catch(AblyException e) { - e.printStackTrace(); - fail("rest_presencehistory_limit_text_f: Unexpected exception"); - return; - } - } - - /** - * Get limited presence history data in the backwards direction using text protocol and check order - * DISABLED: See issue https://github.com/ably/ably-java/issues/159 - */ - /*@Test*/ - public void rest_presencehistory_limit_b() { - String channelName = "persisted:restpresence_persisted"; - /* get channel */ - Channel channel = ably_text.channels.get(channelName); - try { - /* get the history for this channel */ - PaginatedResult members = channel.presence.history(new Param[]{ new Param("direction", "backwards"), new Param("limit", "2") }); - assertNotNull("Expected non-null messages", members); - assertEquals("Expected 2 messages", members.items().length, 2); - - /* verify message order */ - for(int i = 0; i < members.items().length; i++) { - PresenceMessage member = members.items()[i]; - assertEquals("Verify expected member (" + i + ')', clientIds[3 - i], member.clientId); - } - } catch(AblyException e) { - e.printStackTrace(); - fail("rest_presencehistory_limit_b: Unexpected exception"); - return; - } - } - - /** - * Get paginated presence history data in the forward direction using text protocol - * DISABLED: See issue https://github.com/ably/ably-java/issues/159 - */ - /*@Test*/ - public void rest_presencehistory_paginate_f() { - /* get channel */ - String channelName = "persisted:restpresence_persisted"; - Channel channel = ably_text.channels.get(channelName); - try { - /* get the history for this channel */ - PaginatedResult members = channel.presence.history(new Param[]{ new Param("direction", "forwards"), new Param("limit", "1") }); - assertNotNull("Expected non-null messages", members); - assertEquals("Expected 1 message", members.items().length, 1); - - /* verify message order */ - for(int i = 0; i < members.items().length; i++) { - PresenceMessage member = members.items()[i]; - assertEquals("Verify expected member (" + i + ')', clientIds[i], member.clientId); - } - - /* get next page */ - members = members.next(); - assertNotNull("Expected non-null messages", members); - assertEquals("Expected 1 message", members.items().length, 1); - - /* verify message order */ - for(int i = 0; i < members.items().length; i++) { - PresenceMessage member = members.items()[i]; - assertEquals("Verify expected member (" + i + ')', clientIds[1 + i], member.clientId); - } - - /* get next page */ - members = members.next(); - assertNotNull("Expected non-null messages", members); - assertEquals("Expected 1 message", members.items().length, 1); - - /* verify message order */ - for(int i = 0; i < members.items().length; i++) { - PresenceMessage member = members.items()[i]; - assertEquals("Verify expected member (" + i + ')', clientIds[2 + i], member.clientId); - } - - /* get next page */ - members = members.next(); - assertNotNull("Expected non-null messages", members); - assertEquals("Expected 1 message", members.items().length, 1); - - /* verify message order */ - for(int i = 0; i < members.items().length; i++) { - PresenceMessage member = members.items()[i]; - assertEquals("Verify expected member (" + i + ')', clientIds[3 + i], member.clientId); - } - - /* verify there are no further results */ - if(members.hasNext()) { - members = members.next(); - if(members != null) - assertEquals("Expected no further members", members.items().length, 0); - } - - } catch(AblyException e) { - e.printStackTrace(); - fail("rest_presencehistory_paginate_f: Unexpected exception"); - return; - } - } - - /** - * Get paginated presence history data in the backwards direction using text protocol - * DISABLED: See issue https://github.com/ably/ably-java/issues/159 - */ - /*@Test*/ - public void rest_presencehistory_paginate_text_b() { - /* get channel */ - String channelName = "persisted:restpresence_persisted"; - Channel channel = ably_text.channels.get(channelName); - try { - /* get the history for this channel */ - PaginatedResult members = channel.presence.history(new Param[]{ new Param("direction", "backwards"), new Param("limit", "1") }); - assertNotNull("Expected non-null messages", members); - assertEquals("Expected 1 message", members.items().length, 1); - - /* verify message order */ - for(int i = 0; i < members.items().length; i++) { - PresenceMessage member = members.items()[i]; - assertEquals("Verify expected member (" + i + ')', clientIds[3 - i], member.clientId); - } - - /* get next page */ - members = members.next(); - assertNotNull("Expected non-null messages", members); - assertEquals("Expected 1 messages", members.items().length, 1); - - /* verify message order */ - for(int i = 0; i < members.items().length; i++) { - PresenceMessage member = members.items()[i]; - assertEquals("Verify expected member (" + i + ')', clientIds[2 - i], member.clientId); - } - - /* get next page */ - members = members.next(); - assertNotNull("Expected non-null messages", members); - assertEquals("Expected 1 message", members.items().length, 1); - - /* verify message order */ - for(int i = 0; i < members.items().length; i++) { - PresenceMessage member = members.items()[i]; - assertEquals("Verify expected member (" + i + ')', clientIds[1 - i], member.clientId); - } - - /* get next page */ - members = members.next(); - assertNotNull("Expected non-null messages", members); - assertEquals("Expected 1 message", members.items().length, 1); - - /* verify message order */ - for(int i = 0; i < members.items().length; i++) { - PresenceMessage member = members.items()[i]; - assertEquals("Verify expected member (" + i + ')', clientIds[0 - i], member.clientId); - } - - /* verify there are no further results */ - if(members.hasNext()) { - members = members.next(); - if(members != null) - assertEquals("Expected no further members", members.items().length, 0); - } - - } catch(AblyException e) { - e.printStackTrace(); - fail("rest_presencehistory_paginate_text_f: Unexpected exception"); - return; - } - } + private static final String[] clientIds = new String[] { + "client_string_0", + "client_string_1", + "client_string_2", + "client_string_3" + }; + + private AblyRest ably_text; + + @Before + public void setUpBefore() throws Exception { + ClientOptions opts_text = createOptions(testVars.keys[0].keyStr); + ably_text = new AblyRest(opts_text); + } + + /** + * Get member data of various datatypes + */ + @Test + public void rest_getpresence() { + String channelName = "restpresence_notpersisted"; + /* get channel */ + Channel channel = ably_text.channels.get(channelName); + try { + PresenceMessage[] members = channel.presence.get(null).items(); + assertNotNull("Expected non-null messages", members); + assertEquals("Expected 1 message", members.length, 1); + + /* verify presence contents */ + assertEquals("Expect data to be expected String", members[0].data, "This is a string data payload"); + } catch(AblyException e) { + e.printStackTrace(); + fail("rest_getpresence: Unexpected exception"); + return; + } + } + + /** + * Get presence history data of various datatypes + */ + @Test + public void rest_presencehistory_simple() { + String channelName = "persisted:restpresence_persisted"; + /* get channel */ + Channel channel = ably_text.channels.get(channelName); + try { + /* get the history for this channel */ + PaginatedResult members = channel.presence.history(new Param[]{ new Param("direction", "forwards") }); + assertNotNull("Expected non-null messages", members); + assertEquals("Expected 4 messages", members.items().length, 4); + + /* verify presence contents */ + HashMap memberData = new HashMap(); + for(PresenceMessage member : members.items()) + memberData.put(member.clientId, member.data); + assertEquals("Expect client_string_0 to be expected String", memberData.get("client_string_0"), "This is a string data payload"); + } catch(AblyException e) { + e.printStackTrace(); + fail("rest_presencehistory_simple: Unexpected exception"); + return; + } + } + + /** + * Get presence history data in the forward direction and check order + * DISABLED: See issue https://github.com/ably/ably-java/issues/159 + */ + /*@Test*/ + public void rest_presencehistory_order_f() { + String channelName = "persisted:restpresence_persisted"; + /* get channel */ + Channel channel = ably_text.channels.get(channelName); + try { + /* get the history for this channel */ + PaginatedResult members = channel.presence.history(new Param[]{ new Param("direction", "forwards") }); + assertNotNull("Expected non-null messages", members); + assertEquals("Expected 4 messages", members.items().length, 4); + + /* verify message order */ + for(int i = 0; i < members.items().length; i++) { + PresenceMessage member = members.items()[i]; + assertEquals("Verify expected member (" + i + ')', clientIds[i], member.clientId); + } + } catch(AblyException e) { + e.printStackTrace(); + fail("rest_presencehistory_order_f: Unexpected exception"); + return; + } + } + + /** + * Get presence history data in the backwards direction using text protocol and check order + * DISABLED: See issue https://github.com/ably/ably-java/issues/159 + */ + /*@Test*/ + public void rest_presencehistory_order_b() { + String channelName = "persisted:restpresence_persisted"; + /* get channel */ + Channel channel = ably_text.channels.get(channelName); + try { + /* get the history for this channel */ + PaginatedResult members = channel.presence.history(new Param[]{ new Param("direction", "backwards") }); + assertNotNull("Expected non-null messages", members); + assertEquals("Expected 4 messages", members.items().length, 4); + + /* verify message order */ + for(int i = 0; i < members.items().length; i++) { + PresenceMessage member = members.items()[i]; + assertEquals("Verify expected member (" + i + ')', clientIds[3 - i], member.clientId); + } + } catch(AblyException e) { + e.printStackTrace(); + fail("rest_presencehistory_order_b: Unexpected exception"); + return; + } + } + + /** + * Get limited presence history data in the forward direction using text protocol and check order + * DISABLED: See issue https://github.com/ably/ably-java/issues/159 + */ + /*@Test*/ + public void rest_presencehistory_limit_f() { + String channelName = "persisted:restpresence_persisted"; + /* get channel */ + Channel channel = ably_text.channels.get(channelName); + try { + /* get the history for this channel */ + PaginatedResult members = channel.presence.history(new Param[]{ new Param("direction", "forwards"), new Param("limit", "2") }); + assertNotNull("Expected non-null messages", members); + assertEquals("Expected 2 messages", members.items().length, 2); + + /* verify message order */ + for(int i = 0; i < members.items().length; i++) { + PresenceMessage member = members.items()[i]; + assertEquals("Verify expected member (" + i + ')', clientIds[i], member.clientId); + } + } catch(AblyException e) { + e.printStackTrace(); + fail("rest_presencehistory_limit_text_f: Unexpected exception"); + return; + } + } + + /** + * Get limited presence history data in the backwards direction using text protocol and check order + * DISABLED: See issue https://github.com/ably/ably-java/issues/159 + */ + /*@Test*/ + public void rest_presencehistory_limit_b() { + String channelName = "persisted:restpresence_persisted"; + /* get channel */ + Channel channel = ably_text.channels.get(channelName); + try { + /* get the history for this channel */ + PaginatedResult members = channel.presence.history(new Param[]{ new Param("direction", "backwards"), new Param("limit", "2") }); + assertNotNull("Expected non-null messages", members); + assertEquals("Expected 2 messages", members.items().length, 2); + + /* verify message order */ + for(int i = 0; i < members.items().length; i++) { + PresenceMessage member = members.items()[i]; + assertEquals("Verify expected member (" + i + ')', clientIds[3 - i], member.clientId); + } + } catch(AblyException e) { + e.printStackTrace(); + fail("rest_presencehistory_limit_b: Unexpected exception"); + return; + } + } + + /** + * Get paginated presence history data in the forward direction using text protocol + * DISABLED: See issue https://github.com/ably/ably-java/issues/159 + */ + /*@Test*/ + public void rest_presencehistory_paginate_f() { + /* get channel */ + String channelName = "persisted:restpresence_persisted"; + Channel channel = ably_text.channels.get(channelName); + try { + /* get the history for this channel */ + PaginatedResult members = channel.presence.history(new Param[]{ new Param("direction", "forwards"), new Param("limit", "1") }); + assertNotNull("Expected non-null messages", members); + assertEquals("Expected 1 message", members.items().length, 1); + + /* verify message order */ + for(int i = 0; i < members.items().length; i++) { + PresenceMessage member = members.items()[i]; + assertEquals("Verify expected member (" + i + ')', clientIds[i], member.clientId); + } + + /* get next page */ + members = members.next(); + assertNotNull("Expected non-null messages", members); + assertEquals("Expected 1 message", members.items().length, 1); + + /* verify message order */ + for(int i = 0; i < members.items().length; i++) { + PresenceMessage member = members.items()[i]; + assertEquals("Verify expected member (" + i + ')', clientIds[1 + i], member.clientId); + } + + /* get next page */ + members = members.next(); + assertNotNull("Expected non-null messages", members); + assertEquals("Expected 1 message", members.items().length, 1); + + /* verify message order */ + for(int i = 0; i < members.items().length; i++) { + PresenceMessage member = members.items()[i]; + assertEquals("Verify expected member (" + i + ')', clientIds[2 + i], member.clientId); + } + + /* get next page */ + members = members.next(); + assertNotNull("Expected non-null messages", members); + assertEquals("Expected 1 message", members.items().length, 1); + + /* verify message order */ + for(int i = 0; i < members.items().length; i++) { + PresenceMessage member = members.items()[i]; + assertEquals("Verify expected member (" + i + ')', clientIds[3 + i], member.clientId); + } + + /* verify there are no further results */ + if(members.hasNext()) { + members = members.next(); + if(members != null) + assertEquals("Expected no further members", members.items().length, 0); + } + + } catch(AblyException e) { + e.printStackTrace(); + fail("rest_presencehistory_paginate_f: Unexpected exception"); + return; + } + } + + /** + * Get paginated presence history data in the backwards direction using text protocol + * DISABLED: See issue https://github.com/ably/ably-java/issues/159 + */ + /*@Test*/ + public void rest_presencehistory_paginate_text_b() { + /* get channel */ + String channelName = "persisted:restpresence_persisted"; + Channel channel = ably_text.channels.get(channelName); + try { + /* get the history for this channel */ + PaginatedResult members = channel.presence.history(new Param[]{ new Param("direction", "backwards"), new Param("limit", "1") }); + assertNotNull("Expected non-null messages", members); + assertEquals("Expected 1 message", members.items().length, 1); + + /* verify message order */ + for(int i = 0; i < members.items().length; i++) { + PresenceMessage member = members.items()[i]; + assertEquals("Verify expected member (" + i + ')', clientIds[3 - i], member.clientId); + } + + /* get next page */ + members = members.next(); + assertNotNull("Expected non-null messages", members); + assertEquals("Expected 1 messages", members.items().length, 1); + + /* verify message order */ + for(int i = 0; i < members.items().length; i++) { + PresenceMessage member = members.items()[i]; + assertEquals("Verify expected member (" + i + ')', clientIds[2 - i], member.clientId); + } + + /* get next page */ + members = members.next(); + assertNotNull("Expected non-null messages", members); + assertEquals("Expected 1 message", members.items().length, 1); + + /* verify message order */ + for(int i = 0; i < members.items().length; i++) { + PresenceMessage member = members.items()[i]; + assertEquals("Verify expected member (" + i + ')', clientIds[1 - i], member.clientId); + } + + /* get next page */ + members = members.next(); + assertNotNull("Expected non-null messages", members); + assertEquals("Expected 1 message", members.items().length, 1); + + /* verify message order */ + for(int i = 0; i < members.items().length; i++) { + PresenceMessage member = members.items()[i]; + assertEquals("Verify expected member (" + i + ')', clientIds[0 - i], member.clientId); + } + + /* verify there are no further results */ + if(members.hasNext()) { + members = members.next(); + if(members != null) + assertEquals("Expected no further members", members.items().length, 0); + } + + } catch(AblyException e) { + e.printStackTrace(); + fail("rest_presencehistory_paginate_text_f: Unexpected exception"); + return; + } + } } diff --git a/lib/src/test/java/io/ably/lib/test/rest/RestProxyTest.java b/lib/src/test/java/io/ably/lib/test/rest/RestProxyTest.java index 8149d8f60..94fb26312 100644 --- a/lib/src/test/java/io/ably/lib/test/rest/RestProxyTest.java +++ b/lib/src/test/java/io/ably/lib/test/rest/RestProxyTest.java @@ -18,189 +18,189 @@ public class RestProxyTest extends ParameterizedTest { - /** - * Check access to stats API via proxy with invalid host, expecting failure - */ - @Test - public void proxy_simple_invalid_host() { - try { - /* setup client */ - ClientOptions opts = createOptions(testVars.keys[0].keyStr); - opts.proxy = new ProxyOptions() {{ - host = "not-sandbox-proxy.ably.io"; - port = 6128; - }}; - AblyRest ably = new AblyRest(opts); + /** + * Check access to stats API via proxy with invalid host, expecting failure + */ + @Test + public void proxy_simple_invalid_host() { + try { + /* setup client */ + ClientOptions opts = createOptions(testVars.keys[0].keyStr); + opts.proxy = new ProxyOptions() {{ + host = "not-sandbox-proxy.ably.io"; + port = 6128; + }}; + AblyRest ably = new AblyRest(opts); - /* attempt the call, expecting no exception */ - ably.stats(null); - fail("proxy_simple_invalid_host: call succeeded unexpectedly"); - } catch (AblyException.HostFailedException e) { - /* Verify we got a 50x */ - assertTrue(true); - } catch (AblyException e) { - fail("Wrong error code"); - } - } + /* attempt the call, expecting no exception */ + ably.stats(null); + fail("proxy_simple_invalid_host: call succeeded unexpectedly"); + } catch (AblyException.HostFailedException e) { + /* Verify we got a 50x */ + assertTrue(true); + } catch (AblyException e) { + fail("Wrong error code"); + } + } - /** - * Check access to stats API via proxy with invalid port, expecting failure - */ - @Test - public void proxy_simple_invalid_port() { - try { - /* setup client */ - ClientOptions opts = createOptions(testVars.keys[0].keyStr); - opts.proxy = new ProxyOptions() {{ - host = "sandbox-proxy.ably.io"; - port = 6127; - }}; - AblyRest ably = new AblyRest(opts); + /** + * Check access to stats API via proxy with invalid port, expecting failure + */ + @Test + public void proxy_simple_invalid_port() { + try { + /* setup client */ + ClientOptions opts = createOptions(testVars.keys[0].keyStr); + opts.proxy = new ProxyOptions() {{ + host = "sandbox-proxy.ably.io"; + port = 6127; + }}; + AblyRest ably = new AblyRest(opts); - /* attempt the call, expecting no exception */ - ably.stats(null); - fail("proxy_simple_invalid_port: call succeeded unexpectedly"); - } catch (AblyException.HostFailedException e) { - /* Verify we got a 50x */ - assertTrue(true); - } catch (AblyException e) { - fail("Wrong error code"); - } - } + /* attempt the call, expecting no exception */ + ably.stats(null); + fail("proxy_simple_invalid_port: call succeeded unexpectedly"); + } catch (AblyException.HostFailedException e) { + /* Verify we got a 50x */ + assertTrue(true); + } catch (AblyException e) { + fail("Wrong error code"); + } + } - /** - * Check access to stats API via proxy, non-TLS - */ - @Test - @Ignore("Proxy server not running") - public void proxy_simple_plain() { - try { - /* setup client */ - ClientOptions opts = createOptions(testVars.keys[0].keyStr); - opts.clientId = "testClientId"; /* force use of token auth */ - opts.tls = false; - opts.proxy = new ProxyOptions() {{ - host = "sandbox-proxy.ably.io"; - port = 6128; - }}; - AblyRest ably = new AblyRest(opts); + /** + * Check access to stats API via proxy, non-TLS + */ + @Test + @Ignore("Proxy server not running") + public void proxy_simple_plain() { + try { + /* setup client */ + ClientOptions opts = createOptions(testVars.keys[0].keyStr); + opts.clientId = "testClientId"; /* force use of token auth */ + opts.tls = false; + opts.proxy = new ProxyOptions() {{ + host = "sandbox-proxy.ably.io"; + port = 6128; + }}; + AblyRest ably = new AblyRest(opts); - /* attempt the call, expecting no exception */ - PaginatedResult stats = ably.stats(null); - assertNotNull("Expected non-null stats", stats); - } catch (AblyException e) { - e.printStackTrace(); - fail("proxy_simple_plain: Unexpected exception"); - return; - } - } + /* attempt the call, expecting no exception */ + PaginatedResult stats = ably.stats(null); + assertNotNull("Expected non-null stats", stats); + } catch (AblyException e) { + e.printStackTrace(); + fail("proxy_simple_plain: Unexpected exception"); + return; + } + } - /** - * Check access to stats API via proxy - */ - @Test - @Ignore("Proxy server not running") - public void proxy_simple_tls() { - try { - /* setup client */ - ClientOptions opts = createOptions(testVars.keys[0].keyStr); - opts.proxy = new ProxyOptions() {{ - host = "sandbox-proxy.ably.io"; - port = 6128; - }}; - AblyRest ably = new AblyRest(opts); + /** + * Check access to stats API via proxy + */ + @Test + @Ignore("Proxy server not running") + public void proxy_simple_tls() { + try { + /* setup client */ + ClientOptions opts = createOptions(testVars.keys[0].keyStr); + opts.proxy = new ProxyOptions() {{ + host = "sandbox-proxy.ably.io"; + port = 6128; + }}; + AblyRest ably = new AblyRest(opts); - /* attempt the call, expecting no exception */ - PaginatedResult stats = ably.stats(null); - assertNotNull("Expected non-null stats", stats); - } catch (AblyException e) { - e.printStackTrace(); - fail("proxy_simple_tls: Unexpected exception"); - return; - } - } + /* attempt the call, expecting no exception */ + PaginatedResult stats = ably.stats(null); + assertNotNull("Expected non-null stats", stats); + } catch (AblyException e) { + e.printStackTrace(); + fail("proxy_simple_tls: Unexpected exception"); + return; + } + } - /** - * Check access to stats API via proxy with authentication, non-tls - */ - @Test - @Ignore("Proxy server not running") - public void proxy_basic_auth_plain() { - try { - /* setup client */ - ClientOptions opts = createOptions(testVars.keys[0].keyStr); - opts.clientId = "testClientId"; - opts.tls = false; - opts.proxy = new ProxyOptions() {{ - host = "sandbox-proxy.ably.io"; - port = 6129; - username = "ably"; - password = "password"; - }}; - AblyRest ably = new AblyRest(opts); - - /* attempt the call, expecting no exception */ - PaginatedResult stats = ably.stats(null); - assertNotNull("Expected non-null stats", stats); - } catch (AblyException e) { - e.printStackTrace(); - fail("proxy_basic_auth_plain: Unexpected exception"); - return; - } - } + /** + * Check access to stats API via proxy with authentication, non-tls + */ + @Test + @Ignore("Proxy server not running") + public void proxy_basic_auth_plain() { + try { + /* setup client */ + ClientOptions opts = createOptions(testVars.keys[0].keyStr); + opts.clientId = "testClientId"; + opts.tls = false; + opts.proxy = new ProxyOptions() {{ + host = "sandbox-proxy.ably.io"; + port = 6129; + username = "ably"; + password = "password"; + }}; + AblyRest ably = new AblyRest(opts); + + /* attempt the call, expecting no exception */ + PaginatedResult stats = ably.stats(null); + assertNotNull("Expected non-null stats", stats); + } catch (AblyException e) { + e.printStackTrace(); + fail("proxy_basic_auth_plain: Unexpected exception"); + return; + } + } - /** - * Check access to stats API via proxy with authentication, non-tls - */ - //@Test - public void proxy_digest_auth_plain() { - try { - /* setup client */ - ClientOptions opts = createOptions(testVars.keys[0].keyStr); - opts.clientId = "testClientId"; - opts.tls = false; - opts.proxy = new ProxyOptions() {{ - host = "sandbox-proxy.ably.io"; - port = 6129; - username = "ably-digest"; - password = "password"; - prefAuthType = HttpAuth.Type.DIGEST; - }}; - AblyRest ably = new AblyRest(opts); - - /* attempt the call, expecting no exception */ - PaginatedResult stats = ably.stats(null); - assertNotNull("Expected non-null stats", stats); - } catch (AblyException e) { - e.printStackTrace(); - fail("proxy_digest_auth_plain: Unexpected exception"); - return; - } - } + /** + * Check access to stats API via proxy with authentication, non-tls + */ + //@Test + public void proxy_digest_auth_plain() { + try { + /* setup client */ + ClientOptions opts = createOptions(testVars.keys[0].keyStr); + opts.clientId = "testClientId"; + opts.tls = false; + opts.proxy = new ProxyOptions() {{ + host = "sandbox-proxy.ably.io"; + port = 6129; + username = "ably-digest"; + password = "password"; + prefAuthType = HttpAuth.Type.DIGEST; + }}; + AblyRest ably = new AblyRest(opts); + + /* attempt the call, expecting no exception */ + PaginatedResult stats = ably.stats(null); + assertNotNull("Expected non-null stats", stats); + } catch (AblyException e) { + e.printStackTrace(); + fail("proxy_digest_auth_plain: Unexpected exception"); + return; + } + } - /** - * Check access to stats API via proxy with authentication, tls - */ - //@Test - public void proxy_basic_auth_tls() { - try { - /* setup client */ - ClientOptions opts = createOptions(testVars.keys[0].keyStr); - opts.proxy = new ProxyOptions() {{ - host = "sandbox-proxy.ably.io"; - port = 6129; - username = "ably"; - password = "password"; - }}; - AblyRest ably = new AblyRest(opts); - - /* attempt the call, expecting no exception */ - PaginatedResult stats = ably.stats(null); - assertNotNull("Expected non-null stats", stats); - } catch (AblyException e) { - e.printStackTrace(); - fail("proxy_basic_auth_tls: Unexpected exception"); - return; - } - } + /** + * Check access to stats API via proxy with authentication, tls + */ + //@Test + public void proxy_basic_auth_tls() { + try { + /* setup client */ + ClientOptions opts = createOptions(testVars.keys[0].keyStr); + opts.proxy = new ProxyOptions() {{ + host = "sandbox-proxy.ably.io"; + port = 6129; + username = "ably"; + password = "password"; + }}; + AblyRest ably = new AblyRest(opts); + + /* attempt the call, expecting no exception */ + PaginatedResult stats = ably.stats(null); + assertNotNull("Expected non-null stats", stats); + } catch (AblyException e) { + e.printStackTrace(); + fail("proxy_basic_auth_tls: Unexpected exception"); + return; + } + } } diff --git a/lib/src/test/java/io/ably/lib/test/rest/RestPushTest.java b/lib/src/test/java/io/ably/lib/test/rest/RestPushTest.java index 0532aed3f..2d6215a81 100644 --- a/lib/src/test/java/io/ably/lib/test/rest/RestPushTest.java +++ b/lib/src/test/java/io/ably/lib/test/rest/RestPushTest.java @@ -33,787 +33,787 @@ import io.ably.lib.util.JsonUtils; public class RestPushTest extends ParameterizedTest { - private static AblyRest rest; - private static AblyRealtime realtime; - - private static DeviceDetails deviceDetails; - private static DeviceDetails deviceDetails1ClientA; - private static DeviceDetails deviceDetails2ClientA; - private static DeviceDetails deviceDetails3ClientB; - private static DeviceDetails deviceDetails4ClientC; - private static DeviceDetails[] allDeviceDetails; - - private static ChannelSubscription subscriptionFooDevice1; - private static ChannelSubscription subscriptionFooDevice2; - private static ChannelSubscription subscriptionBarDevice2; - private static ChannelSubscription subscriptionFooClientA; - private static ChannelSubscription subscriptionFooClientB; - private static ChannelSubscription subscriptionBarClientB; - private static ChannelSubscription subscriptionFooDevice4; - private static ChannelSubscription[] allSubscriptions; - - private static Helpers.RawHttpTracker httpTracker; - - private static JsonObject testPayload = JsonUtils.object() - .add("data", JsonUtils.object() - .add("foo", "bar")) - .add("notification", JsonUtils.object() - .add("body", null) - .add("collapseKey", null) - .add("icon", null) - .add("sound", null) - .add("title", null) - .add("ttl", null) - ).toJson(); - - @Rule - public Timeout testTimeout = Timeout.seconds(60); - - @Before - public void setUpBefore() throws Exception { - if (rest != null) { - return; - } - - httpTracker = new Helpers.RawHttpTracker(); - DebugOptions options = createOptions(testVars.keys[0].keyStr); - options.httpListener = httpTracker; - rest = new AblyRest(options); - realtime = new AblyRealtime(options); - - deviceDetails = DeviceDetails.fromJsonObject(JsonUtils.object() - .add("id", "testDeviceDetails") - .add("platform", "ios") - .add("formFactor", "phone") - .add("metadata", JsonUtils.object()) - .add("push", JsonUtils.object() - .add("recipient", JsonUtils.object() - .add("transportType", "apns") - .add("deviceToken", "foo"))) - .toJson()); - - deviceDetails1ClientA = DeviceDetails.fromJsonObject(JsonUtils.object() - .add("id", "deviceDetails1ClientA") - .add("platform", "android") - .add("formFactor", "tablet") - .add("clientId", "clientA") - .add("metadata", JsonUtils.object()) - .add("push", JsonUtils.object() - .add("recipient", JsonUtils.object() - .add("transportType", "fcm") - .add("registrationToken", "qux"))) - .toJson()); - - deviceDetails2ClientA = DeviceDetails.fromJsonObject(JsonUtils.object() - .add("id", "deviceDetails2ClientA") - .add("platform", "android") - .add("formFactor", "tablet") - .add("clientId", "clientA") - .add("metadata", JsonUtils.object()) - .add("push", JsonUtils.object() - .add("recipient", JsonUtils.object() - .add("transportType", "fcm") - .add("registrationToken", "qux"))) - .toJson()); - - deviceDetails3ClientB = DeviceDetails.fromJsonObject(JsonUtils.object() - .add("id", "deviceDetails3ClientB") - .add("platform", "android") - .add("formFactor", "tablet") - .add("clientId", "clientB") - .add("metadata", JsonUtils.object()) - .add("push", JsonUtils.object() - .add("recipient", JsonUtils.object() - .add("transportType", "fcm") - .add("registrationToken", "qux"))) - .toJson()); - - deviceDetails4ClientC = DeviceDetails.fromJsonObject(JsonUtils.object() - .add("id", "deviceDetails4ClientC") - .add("platform", "android") - .add("formFactor", "tablet") - .add("clientId", "clientC") - .add("metadata", JsonUtils.object()) - .add("push", JsonUtils.object() - .add("recipient", JsonUtils.object() - .add("transportType", "fcm") - .add("registrationToken", "qux"))) - .toJson()); - - allDeviceDetails = new DeviceDetails[]{ - deviceDetails, - deviceDetails1ClientA, - deviceDetails2ClientA, - deviceDetails3ClientB, - deviceDetails4ClientC - }; - - for (DeviceDetails device : allDeviceDetails) { - rest.push.admin.deviceRegistrations.save(device); - } - - subscriptionFooDevice1 = ChannelSubscription.forDevice("pushenabled:foo", "deviceDetails1ClientA"); - subscriptionFooDevice2 = ChannelSubscription.forDevice("pushenabled:foo", "deviceDetails2ClientA"); - subscriptionBarDevice2 = ChannelSubscription.forDevice("pushenabled:bar", "deviceDetails2ClientA"); - subscriptionFooClientA = ChannelSubscription.forClientId("pushenabled:foo", "clientA"); - subscriptionFooClientB = ChannelSubscription.forClientId("pushenabled:foo", "clientB"); - subscriptionBarClientB = ChannelSubscription.forClientId("pushenabled:bar", "clientB"); - subscriptionFooDevice4 = ChannelSubscription.forDevice("pushenabled:foo", "deviceDetails4ClientC"); - - allSubscriptions = new ChannelSubscription[]{ - subscriptionFooDevice1, - subscriptionFooDevice2, - subscriptionBarDevice2, - subscriptionFooClientA, - subscriptionFooClientB, - subscriptionBarClientB, - subscriptionFooDevice4 - }; - - for (ChannelSubscription sub : allSubscriptions) { - rest.push.admin.channelSubscriptions.save(sub); - } - } - - @AfterClass - public static void tearDownAfter() throws Exception { - for (DeviceDetails device : allDeviceDetails) { - rest.push.admin.deviceRegistrations.remove(device); - } - for (ChannelSubscription sub : allSubscriptions) { - rest.push.admin.channelSubscriptions.remove(sub); - } - } - - // RHS1a - @Test - public void push_admin_publish() throws Exception { - class TestCase extends TestCases.Base { - final Param[] recipient; - final JsonObject payload; - - TestCase(String name, Param[] recipient, JsonObject data, String expectedError) { - this(name, recipient, data, expectedError, 0); - } - - TestCase(String name, Param[] recipient, JsonObject payload, String expectedError, int expectedStatusCode) { - super(name, expectedError, expectedStatusCode); - this.payload = payload; - this.recipient = recipient; - } - - @Override - public void run() throws Exception { - final String channelName = "pushenabled:push_admin_publish-" + this.name; - - CompletionWaiter waiter = new CompletionWaiter(); - realtime.channels.get(channelName).attach(waiter); - waiter.waitFor(1); - - new Helpers.SyncAndAsync() { - @Override - public Void getSync(Void arg) throws AblyException { - rest.push.admin.publish(recipient, payload); - return null; - } - - @Override - public void getAsync(Void arg, Callback callback) { - rest.push.admin.publishAsync(recipient, payload, new CompletionListener.FromCallback(callback)); - } - - @Override - public void then(Helpers.AblyFunction get) throws AblyException { - MessageWaiter messages = new MessageWaiter(realtime.channels.get(channelName), "__ably_push__"); - get.apply(null); - messages.waitFor(1, 10000); - - assertEquals(1, messages.receivedMessages.size()); - assertEquals(payload.toString(), messages.receivedMessages.get(0).data); - } - }.run(); - } - } - - TestCases testCases = new TestCases(); - testCases.add(new TestCase( - "ok", - new Param[]{ - new Param("transportType", "ablyChannel"), - new Param("channel", "pushenabled:push_admin_publish-ok"), - new Param("ablyKey", testVars.keys[0].keyStr), - new Param("ablyUrl", String.format("%s%s:%d", rest.httpCore.scheme, rest.httpCore.getPrimaryHost(), rest.httpCore.port)), - }, - testPayload, - null)); - testCases.add(new TestCase( - "bad recipient", - Param.set(null, "foo", "bar"), - JsonUtils.object() - .add("data", JsonUtils.object() - .add("foo", "bar")).toJson(), - "", 400)); - testCases.add(new TestCase( - "empty recipient", - new Param[]{}, - JsonUtils.object() - .add("data", JsonUtils.object() - .add("foo", "bar")).toJson(), - "recipient")); - testCases.add(new TestCase( - "empty payload", - Param.set(null, "ablyChannel", "pushenabled:push_admin_publish-ok"), - null, - "payload")); - - testCases.run(); - } - - // RHS1b1 - @Test - public void push_admin_deviceRegistrations_get() throws Exception { - class TestCase extends TestCases.Base { - final String deviceId; - final DeviceDetails expectedDevice; - - TestCase(String name, String deviceId, DeviceDetails expectedDevice, String expectedError, int expectedStatusCode) { - super(name, expectedError, expectedStatusCode); - this.deviceId = deviceId; - this.expectedDevice = expectedDevice; - } - - @Override - public void run() throws Exception { - new Helpers.SyncAndAsync() { - @Override - public DeviceDetails getSync(Void arg) throws AblyException { - return rest.push.admin.deviceRegistrations.get(deviceId); - } - - @Override - public void getAsync(Void arg, Callback callback) { - rest.push.admin.deviceRegistrations.getAsync(deviceId, callback); - } - - @Override - public void then(Helpers.AblyFunction get) throws AblyException { - assertEquals(expectedDevice, get.apply(null)); - } - }.run(); - } - } - - TestCases testCases = new TestCases(); - - testCases.add(new TestCase("found", deviceDetails.id, deviceDetails, null, 0)); - testCases.add(new TestCase("not found", "madeup", null, "not found", 404)); - - testCases.run(); - } - - // RHS1b2 - @Test - public void push_admin_deviceRegistrations_list() throws Exception { - class TestCase extends TestCases.Base { - private final Param[] params; - private final DeviceDetails[] expected; - - TestCase(String name, Param[] params, DeviceDetails[] expected, String expectedError, int expectedStatusCode) { - super(name, expectedError, expectedStatusCode); - this.params = params; - this.expected = expected; - } - - @Override - public void run() throws Exception { - new Helpers.SyncAndAsync() { - @Override - public DeviceDetails[] getSync(Void arg) throws AblyException { - return rest.push.admin.deviceRegistrations.list(params).items(); - } - - @Override - public void getAsync(Void arg, Callback callback) { - rest.push.admin.deviceRegistrations.listAsync(params, new Callback.Map, DeviceDetails[]>(callback) { - @Override - public DeviceDetails[] map(AsyncPaginatedResult result) { - return result.items(); - } - }); - } - - @Override - public void then(Helpers.AblyFunction get) throws AblyException { - Helpers.assertArrayUnorderedEquals(expected, get.apply(null)); - } - }.run(); - } - } - - TestCases testCases = new TestCases(); - - testCases.add(new TestCase( - "by deviceId", - Param.push(null, "deviceId", deviceDetails.id), - new DeviceDetails[]{deviceDetails}, - null, 0)); - testCases.add(new TestCase( - "by clientId A", - Param.push(null, "clientId", "clientA"), - new DeviceDetails[]{deviceDetails2ClientA, deviceDetails1ClientA}, - null, 0)); - testCases.add(new TestCase( - "by clientId B", - Param.push(null, "clientId", "clientB"), - new DeviceDetails[]{deviceDetails3ClientB}, - null, 0)); - testCases.add(new TestCase( - "all", - Param.push(null, "direction", "forwards"), - allDeviceDetails, - null, 0)); - testCases.add(new TestCase( - "none", - Param.push(null, "deviceId", "madeup"), - new DeviceDetails[]{}, - null, 0)); - - testCases.run(); - } - - // RHS1b3 - @Test - public void push_admin_deviceRegistrations_save() throws Exception { - new Helpers.SyncAndAsync() { - @Override - public DeviceDetails getSync(DeviceDetails saved) throws AblyException { - return rest.push.admin.deviceRegistrations.save(saved); - } - - @Override - public void getAsync(DeviceDetails saved, Callback callback) { - rest.push.admin.deviceRegistrations.saveAsync(saved, callback); - } - - @Override - public void then(final Helpers.AblyFunction get) throws AblyException { - final DeviceDetails saved = DeviceDetails.fromJsonObject(JsonUtils.object() - .add("id", "newDeviceDetails") - .add("platform", "ios") - .add("formFactor", "phone") - .add("metadata", JsonUtils.object()) - .add("push", JsonUtils.object() - .add("recipient", JsonUtils.object() - .add("transportType", "apns") - .add("deviceToken", "foo"))) - .toJson()); - try { - // Save new - DeviceDetails got = get.apply(saved); - Helpers.RawHttpRequest request = httpTracker.getLastRequest(); - assertEquals("PUT", request.method); - assertEquals("/push/deviceRegistrations/" + saved.id, request.url.getPath()); - assertEquals(saved, got); - - // Mutate - saved.clientId = "foo"; - got = get.apply(saved); - assertEquals("foo", got.clientId); - - // Failing - Helpers.expectedError(new Helpers.AblyFunction() { - @Override - public Void apply(Void aVoid) throws AblyException { - saved.formFactor = "madeup"; - get.apply(saved); - return null; - } - }, "", 400); - } finally { - rest.push.admin.deviceRegistrations.remove(saved); - } - } - }.run(); - } - - // RHS1b4 - @Test - public void push_admin_deviceRegistrations_remove() throws Exception { - final DeviceDetails saved = DeviceDetails.fromJsonObject(JsonUtils.object() - .add("id", "newDeviceDetails") - .add("platform", "ios") - .add("formFactor", "phone") - .add("metadata", JsonUtils.object()) - .add("push", JsonUtils.object() - .add("recipient", JsonUtils.object() - .add("transportType", "apns") - .add("deviceToken", "foo"))) - .toJson()); - - new Helpers.SyncAndAsync() { - @Override - public Void getSync(Void aVoid) throws AblyException { - rest.push.admin.deviceRegistrations.remove(saved); - return null; - } - - @Override - public void getAsync(Void aVoid, Callback callback) { - rest.push.admin.deviceRegistrations.removeAsync(saved, new CompletionListener.FromCallback(callback)); - } - - @Override - public void then(Helpers.AblyFunction get) throws AblyException { - try { - rest.push.admin.deviceRegistrations.save(saved); - - // Ensure it exists - rest.push.admin.deviceRegistrations.get(saved.id); - - // Existing - get.apply(null); - Helpers.expectedError(new Helpers.AblyFunction() { - @Override - public Void apply(Void aVoid) throws AblyException { - rest.push.admin.deviceRegistrations.get(saved.id); - return null; - } - }, "", 404); - - // Non-existing - get.apply(null); - } finally { - rest.push.admin.deviceRegistrations.remove(saved); - } - } - }.run(); - } - - // RHS1b5 - @Test - public void push_admin_deviceRegistrations_removeWhere() throws Exception { - class TestCase extends TestCases.Base { - private final Param[] params; - private final DeviceDetails[] expectedRemoved; - - public TestCase(String name, String expectedError, Param[] params, DeviceDetails[] expectedRemoved) { - super(name, expectedError); - this.params = Param.push(params, "fullWait", "true"); - this.expectedRemoved = expectedRemoved; - } - - @Override - public void run() throws Exception { - new Helpers.SyncAndAsync() { - @Override - public Void getSync(Param[] params) throws AblyException { - rest.push.admin.deviceRegistrations.removeWhere(params); - return null; - } - - @Override - public void getAsync(Param[] params, Callback callback) { - rest.push.admin.deviceRegistrations.removeWhereAsync(params, new CompletionListener.FromCallback(callback)); - } - - @Override - public void then(Helpers.AblyFunction get) throws AblyException { - try { - get.apply(params); - - PaginatedResult result = rest.push.admin.deviceRegistrations.list(null); - - Set expectedRemaining = new CopyOnWriteArraySet(Arrays.asList(allDeviceDetails)); - expectedRemaining.removeAll(Arrays.asList(expectedRemoved)); - Set remaining = new CopyOnWriteArraySet(Arrays.asList(result.items())); - - assertEquals(expectedRemaining, remaining); - } finally { - for (DeviceDetails removed : expectedRemoved) { - rest.push.admin.deviceRegistrations.save(removed); - } - } - } - }.run(); - } - } - - TestCases testCases = new TestCases(); - - testCases.add(new TestCase( - "by clientId", - null, - Param.push(null, "clientId", "clientA"), - new DeviceDetails[]{ - deviceDetails1ClientA, - deviceDetails2ClientA - })); - testCases.add(new TestCase( - "by deviceId", - null, - Param.push(null, "deviceId", deviceDetails2ClientA.id), - new DeviceDetails[]{ - deviceDetails2ClientA - })); - testCases.add(new TestCase( - "matching none", - null, - Param.push(null, "deviceId", "madeup"), - new DeviceDetails[]{})); - - testCases.run(); - } - - // RHS1c1 - @Test - public void push_admin_channelSubscriptions_list() throws Exception { - class TestCase extends TestCases.Base { - private final Param[] params; - private final ChannelSubscription[] expected; - - TestCase(String name, Param[] params, ChannelSubscription[] expected, String expectedError, int expectedStatusCode) { - super(name, expectedError, expectedStatusCode); - this.params = params; - this.expected = expected; - } - - @Override - public void run() throws Exception { - new Helpers.SyncAndAsync() { - @Override - public ChannelSubscription[] getSync(Void arg) throws AblyException { - return rest.push.admin.channelSubscriptions.list(params).items(); - } - - @Override - public void getAsync(Void arg, Callback callback) { - rest.push.admin.channelSubscriptions.listAsync(params, new Callback.Map, ChannelSubscription[]>(callback) { - @Override - public ChannelSubscription[] map(AsyncPaginatedResult result) { - return result.items(); - } - }); - } - - @Override - public void then(Helpers.AblyFunction get) throws AblyException { - Helpers.assertArrayUnorderedEquals(expected, get.apply(null)); - } - }.run(); - } - } - - TestCases testCases = new TestCases(); - - testCases.add(new TestCase( - "by deviceId", - Param.push(null, "deviceId", deviceDetails4ClientC.id), - new ChannelSubscription[]{subscriptionFooDevice4}, - null, 0)); - testCases.add(new TestCase( - "by clientId A", - Param.push(null, "clientId", "clientA"), - new ChannelSubscription[]{subscriptionFooClientA}, - null, 0)); - testCases.add(new TestCase( - "by clientId B", - Param.push(null, "clientId", "clientB"), - new ChannelSubscription[]{subscriptionFooClientB, subscriptionBarClientB}, - null, 0)); - testCases.add(new TestCase( - "none", - Param.push(null, "deviceId", "madeup"), - new ChannelSubscription[]{}, - null, 0)); - testCases.add(new TestCase( - "by clientId B and channel", - Param.push(Param.push(null, "clientId", "clientB"), "channel", "pushenabled:bar"), - new ChannelSubscription[]{subscriptionBarClientB}, - null, 0)); - - testCases.run(); - } - - // RHS1c2 - @Test - @Ignore("FIXME: tests interfere") - public void push_admin_channelSubscriptions_listChannels() throws Exception { - new Helpers.SyncAndAsync(){ - @Override - public String[] getSync(Void aVoid) throws AblyException { - return rest.push.admin.channelSubscriptions.listChannels(null).items(); - } - - @Override - public void getAsync(Void aVoid, Callback callback) { - rest.push.admin.channelSubscriptions.listChannelsAsync(null, new Callback.Map, String[]>(callback) { - @Override - public String[] map(AsyncPaginatedResult result) { - return result.items(); - } - }); - } - - @Override - public void then(Helpers.AblyFunction get) throws AblyException { - Helpers.assertArrayUnorderedEquals(new String[]{"pushenabled:foo", "pushenabled:bar"}, get.apply(null)); - } - }.run(); - } + private static AblyRest rest; + private static AblyRealtime realtime; + + private static DeviceDetails deviceDetails; + private static DeviceDetails deviceDetails1ClientA; + private static DeviceDetails deviceDetails2ClientA; + private static DeviceDetails deviceDetails3ClientB; + private static DeviceDetails deviceDetails4ClientC; + private static DeviceDetails[] allDeviceDetails; + + private static ChannelSubscription subscriptionFooDevice1; + private static ChannelSubscription subscriptionFooDevice2; + private static ChannelSubscription subscriptionBarDevice2; + private static ChannelSubscription subscriptionFooClientA; + private static ChannelSubscription subscriptionFooClientB; + private static ChannelSubscription subscriptionBarClientB; + private static ChannelSubscription subscriptionFooDevice4; + private static ChannelSubscription[] allSubscriptions; + + private static Helpers.RawHttpTracker httpTracker; + + private static JsonObject testPayload = JsonUtils.object() + .add("data", JsonUtils.object() + .add("foo", "bar")) + .add("notification", JsonUtils.object() + .add("body", null) + .add("collapseKey", null) + .add("icon", null) + .add("sound", null) + .add("title", null) + .add("ttl", null) + ).toJson(); + + @Rule + public Timeout testTimeout = Timeout.seconds(60); + + @Before + public void setUpBefore() throws Exception { + if (rest != null) { + return; + } + + httpTracker = new Helpers.RawHttpTracker(); + DebugOptions options = createOptions(testVars.keys[0].keyStr); + options.httpListener = httpTracker; + rest = new AblyRest(options); + realtime = new AblyRealtime(options); + + deviceDetails = DeviceDetails.fromJsonObject(JsonUtils.object() + .add("id", "testDeviceDetails") + .add("platform", "ios") + .add("formFactor", "phone") + .add("metadata", JsonUtils.object()) + .add("push", JsonUtils.object() + .add("recipient", JsonUtils.object() + .add("transportType", "apns") + .add("deviceToken", "foo"))) + .toJson()); + + deviceDetails1ClientA = DeviceDetails.fromJsonObject(JsonUtils.object() + .add("id", "deviceDetails1ClientA") + .add("platform", "android") + .add("formFactor", "tablet") + .add("clientId", "clientA") + .add("metadata", JsonUtils.object()) + .add("push", JsonUtils.object() + .add("recipient", JsonUtils.object() + .add("transportType", "fcm") + .add("registrationToken", "qux"))) + .toJson()); + + deviceDetails2ClientA = DeviceDetails.fromJsonObject(JsonUtils.object() + .add("id", "deviceDetails2ClientA") + .add("platform", "android") + .add("formFactor", "tablet") + .add("clientId", "clientA") + .add("metadata", JsonUtils.object()) + .add("push", JsonUtils.object() + .add("recipient", JsonUtils.object() + .add("transportType", "fcm") + .add("registrationToken", "qux"))) + .toJson()); + + deviceDetails3ClientB = DeviceDetails.fromJsonObject(JsonUtils.object() + .add("id", "deviceDetails3ClientB") + .add("platform", "android") + .add("formFactor", "tablet") + .add("clientId", "clientB") + .add("metadata", JsonUtils.object()) + .add("push", JsonUtils.object() + .add("recipient", JsonUtils.object() + .add("transportType", "fcm") + .add("registrationToken", "qux"))) + .toJson()); + + deviceDetails4ClientC = DeviceDetails.fromJsonObject(JsonUtils.object() + .add("id", "deviceDetails4ClientC") + .add("platform", "android") + .add("formFactor", "tablet") + .add("clientId", "clientC") + .add("metadata", JsonUtils.object()) + .add("push", JsonUtils.object() + .add("recipient", JsonUtils.object() + .add("transportType", "fcm") + .add("registrationToken", "qux"))) + .toJson()); + + allDeviceDetails = new DeviceDetails[]{ + deviceDetails, + deviceDetails1ClientA, + deviceDetails2ClientA, + deviceDetails3ClientB, + deviceDetails4ClientC + }; + + for (DeviceDetails device : allDeviceDetails) { + rest.push.admin.deviceRegistrations.save(device); + } + + subscriptionFooDevice1 = ChannelSubscription.forDevice("pushenabled:foo", "deviceDetails1ClientA"); + subscriptionFooDevice2 = ChannelSubscription.forDevice("pushenabled:foo", "deviceDetails2ClientA"); + subscriptionBarDevice2 = ChannelSubscription.forDevice("pushenabled:bar", "deviceDetails2ClientA"); + subscriptionFooClientA = ChannelSubscription.forClientId("pushenabled:foo", "clientA"); + subscriptionFooClientB = ChannelSubscription.forClientId("pushenabled:foo", "clientB"); + subscriptionBarClientB = ChannelSubscription.forClientId("pushenabled:bar", "clientB"); + subscriptionFooDevice4 = ChannelSubscription.forDevice("pushenabled:foo", "deviceDetails4ClientC"); + + allSubscriptions = new ChannelSubscription[]{ + subscriptionFooDevice1, + subscriptionFooDevice2, + subscriptionBarDevice2, + subscriptionFooClientA, + subscriptionFooClientB, + subscriptionBarClientB, + subscriptionFooDevice4 + }; + + for (ChannelSubscription sub : allSubscriptions) { + rest.push.admin.channelSubscriptions.save(sub); + } + } + + @AfterClass + public static void tearDownAfter() throws Exception { + for (DeviceDetails device : allDeviceDetails) { + rest.push.admin.deviceRegistrations.remove(device); + } + for (ChannelSubscription sub : allSubscriptions) { + rest.push.admin.channelSubscriptions.remove(sub); + } + } + + // RHS1a + @Test + public void push_admin_publish() throws Exception { + class TestCase extends TestCases.Base { + final Param[] recipient; + final JsonObject payload; + + TestCase(String name, Param[] recipient, JsonObject data, String expectedError) { + this(name, recipient, data, expectedError, 0); + } + + TestCase(String name, Param[] recipient, JsonObject payload, String expectedError, int expectedStatusCode) { + super(name, expectedError, expectedStatusCode); + this.payload = payload; + this.recipient = recipient; + } + + @Override + public void run() throws Exception { + final String channelName = "pushenabled:push_admin_publish-" + this.name; + + CompletionWaiter waiter = new CompletionWaiter(); + realtime.channels.get(channelName).attach(waiter); + waiter.waitFor(1); + + new Helpers.SyncAndAsync() { + @Override + public Void getSync(Void arg) throws AblyException { + rest.push.admin.publish(recipient, payload); + return null; + } + + @Override + public void getAsync(Void arg, Callback callback) { + rest.push.admin.publishAsync(recipient, payload, new CompletionListener.FromCallback(callback)); + } + + @Override + public void then(Helpers.AblyFunction get) throws AblyException { + MessageWaiter messages = new MessageWaiter(realtime.channels.get(channelName), "__ably_push__"); + get.apply(null); + messages.waitFor(1, 10000); + + assertEquals(1, messages.receivedMessages.size()); + assertEquals(payload.toString(), messages.receivedMessages.get(0).data); + } + }.run(); + } + } + + TestCases testCases = new TestCases(); + testCases.add(new TestCase( + "ok", + new Param[]{ + new Param("transportType", "ablyChannel"), + new Param("channel", "pushenabled:push_admin_publish-ok"), + new Param("ablyKey", testVars.keys[0].keyStr), + new Param("ablyUrl", String.format("%s%s:%d", rest.httpCore.scheme, rest.httpCore.getPrimaryHost(), rest.httpCore.port)), + }, + testPayload, + null)); + testCases.add(new TestCase( + "bad recipient", + Param.set(null, "foo", "bar"), + JsonUtils.object() + .add("data", JsonUtils.object() + .add("foo", "bar")).toJson(), + "", 400)); + testCases.add(new TestCase( + "empty recipient", + new Param[]{}, + JsonUtils.object() + .add("data", JsonUtils.object() + .add("foo", "bar")).toJson(), + "recipient")); + testCases.add(new TestCase( + "empty payload", + Param.set(null, "ablyChannel", "pushenabled:push_admin_publish-ok"), + null, + "payload")); + + testCases.run(); + } + + // RHS1b1 + @Test + public void push_admin_deviceRegistrations_get() throws Exception { + class TestCase extends TestCases.Base { + final String deviceId; + final DeviceDetails expectedDevice; + + TestCase(String name, String deviceId, DeviceDetails expectedDevice, String expectedError, int expectedStatusCode) { + super(name, expectedError, expectedStatusCode); + this.deviceId = deviceId; + this.expectedDevice = expectedDevice; + } + + @Override + public void run() throws Exception { + new Helpers.SyncAndAsync() { + @Override + public DeviceDetails getSync(Void arg) throws AblyException { + return rest.push.admin.deviceRegistrations.get(deviceId); + } + + @Override + public void getAsync(Void arg, Callback callback) { + rest.push.admin.deviceRegistrations.getAsync(deviceId, callback); + } + + @Override + public void then(Helpers.AblyFunction get) throws AblyException { + assertEquals(expectedDevice, get.apply(null)); + } + }.run(); + } + } + + TestCases testCases = new TestCases(); + + testCases.add(new TestCase("found", deviceDetails.id, deviceDetails, null, 0)); + testCases.add(new TestCase("not found", "madeup", null, "not found", 404)); + + testCases.run(); + } + + // RHS1b2 + @Test + public void push_admin_deviceRegistrations_list() throws Exception { + class TestCase extends TestCases.Base { + private final Param[] params; + private final DeviceDetails[] expected; + + TestCase(String name, Param[] params, DeviceDetails[] expected, String expectedError, int expectedStatusCode) { + super(name, expectedError, expectedStatusCode); + this.params = params; + this.expected = expected; + } + + @Override + public void run() throws Exception { + new Helpers.SyncAndAsync() { + @Override + public DeviceDetails[] getSync(Void arg) throws AblyException { + return rest.push.admin.deviceRegistrations.list(params).items(); + } + + @Override + public void getAsync(Void arg, Callback callback) { + rest.push.admin.deviceRegistrations.listAsync(params, new Callback.Map, DeviceDetails[]>(callback) { + @Override + public DeviceDetails[] map(AsyncPaginatedResult result) { + return result.items(); + } + }); + } + + @Override + public void then(Helpers.AblyFunction get) throws AblyException { + Helpers.assertArrayUnorderedEquals(expected, get.apply(null)); + } + }.run(); + } + } + + TestCases testCases = new TestCases(); + + testCases.add(new TestCase( + "by deviceId", + Param.push(null, "deviceId", deviceDetails.id), + new DeviceDetails[]{deviceDetails}, + null, 0)); + testCases.add(new TestCase( + "by clientId A", + Param.push(null, "clientId", "clientA"), + new DeviceDetails[]{deviceDetails2ClientA, deviceDetails1ClientA}, + null, 0)); + testCases.add(new TestCase( + "by clientId B", + Param.push(null, "clientId", "clientB"), + new DeviceDetails[]{deviceDetails3ClientB}, + null, 0)); + testCases.add(new TestCase( + "all", + Param.push(null, "direction", "forwards"), + allDeviceDetails, + null, 0)); + testCases.add(new TestCase( + "none", + Param.push(null, "deviceId", "madeup"), + new DeviceDetails[]{}, + null, 0)); + + testCases.run(); + } + + // RHS1b3 + @Test + public void push_admin_deviceRegistrations_save() throws Exception { + new Helpers.SyncAndAsync() { + @Override + public DeviceDetails getSync(DeviceDetails saved) throws AblyException { + return rest.push.admin.deviceRegistrations.save(saved); + } + + @Override + public void getAsync(DeviceDetails saved, Callback callback) { + rest.push.admin.deviceRegistrations.saveAsync(saved, callback); + } + + @Override + public void then(final Helpers.AblyFunction get) throws AblyException { + final DeviceDetails saved = DeviceDetails.fromJsonObject(JsonUtils.object() + .add("id", "newDeviceDetails") + .add("platform", "ios") + .add("formFactor", "phone") + .add("metadata", JsonUtils.object()) + .add("push", JsonUtils.object() + .add("recipient", JsonUtils.object() + .add("transportType", "apns") + .add("deviceToken", "foo"))) + .toJson()); + try { + // Save new + DeviceDetails got = get.apply(saved); + Helpers.RawHttpRequest request = httpTracker.getLastRequest(); + assertEquals("PUT", request.method); + assertEquals("/push/deviceRegistrations/" + saved.id, request.url.getPath()); + assertEquals(saved, got); + + // Mutate + saved.clientId = "foo"; + got = get.apply(saved); + assertEquals("foo", got.clientId); + + // Failing + Helpers.expectedError(new Helpers.AblyFunction() { + @Override + public Void apply(Void aVoid) throws AblyException { + saved.formFactor = "madeup"; + get.apply(saved); + return null; + } + }, "", 400); + } finally { + rest.push.admin.deviceRegistrations.remove(saved); + } + } + }.run(); + } + + // RHS1b4 + @Test + public void push_admin_deviceRegistrations_remove() throws Exception { + final DeviceDetails saved = DeviceDetails.fromJsonObject(JsonUtils.object() + .add("id", "newDeviceDetails") + .add("platform", "ios") + .add("formFactor", "phone") + .add("metadata", JsonUtils.object()) + .add("push", JsonUtils.object() + .add("recipient", JsonUtils.object() + .add("transportType", "apns") + .add("deviceToken", "foo"))) + .toJson()); + + new Helpers.SyncAndAsync() { + @Override + public Void getSync(Void aVoid) throws AblyException { + rest.push.admin.deviceRegistrations.remove(saved); + return null; + } + + @Override + public void getAsync(Void aVoid, Callback callback) { + rest.push.admin.deviceRegistrations.removeAsync(saved, new CompletionListener.FromCallback(callback)); + } + + @Override + public void then(Helpers.AblyFunction get) throws AblyException { + try { + rest.push.admin.deviceRegistrations.save(saved); + + // Ensure it exists + rest.push.admin.deviceRegistrations.get(saved.id); + + // Existing + get.apply(null); + Helpers.expectedError(new Helpers.AblyFunction() { + @Override + public Void apply(Void aVoid) throws AblyException { + rest.push.admin.deviceRegistrations.get(saved.id); + return null; + } + }, "", 404); + + // Non-existing + get.apply(null); + } finally { + rest.push.admin.deviceRegistrations.remove(saved); + } + } + }.run(); + } + + // RHS1b5 + @Test + public void push_admin_deviceRegistrations_removeWhere() throws Exception { + class TestCase extends TestCases.Base { + private final Param[] params; + private final DeviceDetails[] expectedRemoved; + + public TestCase(String name, String expectedError, Param[] params, DeviceDetails[] expectedRemoved) { + super(name, expectedError); + this.params = Param.push(params, "fullWait", "true"); + this.expectedRemoved = expectedRemoved; + } + + @Override + public void run() throws Exception { + new Helpers.SyncAndAsync() { + @Override + public Void getSync(Param[] params) throws AblyException { + rest.push.admin.deviceRegistrations.removeWhere(params); + return null; + } + + @Override + public void getAsync(Param[] params, Callback callback) { + rest.push.admin.deviceRegistrations.removeWhereAsync(params, new CompletionListener.FromCallback(callback)); + } + + @Override + public void then(Helpers.AblyFunction get) throws AblyException { + try { + get.apply(params); + + PaginatedResult result = rest.push.admin.deviceRegistrations.list(null); + + Set expectedRemaining = new CopyOnWriteArraySet(Arrays.asList(allDeviceDetails)); + expectedRemaining.removeAll(Arrays.asList(expectedRemoved)); + Set remaining = new CopyOnWriteArraySet(Arrays.asList(result.items())); + + assertEquals(expectedRemaining, remaining); + } finally { + for (DeviceDetails removed : expectedRemoved) { + rest.push.admin.deviceRegistrations.save(removed); + } + } + } + }.run(); + } + } + + TestCases testCases = new TestCases(); + + testCases.add(new TestCase( + "by clientId", + null, + Param.push(null, "clientId", "clientA"), + new DeviceDetails[]{ + deviceDetails1ClientA, + deviceDetails2ClientA + })); + testCases.add(new TestCase( + "by deviceId", + null, + Param.push(null, "deviceId", deviceDetails2ClientA.id), + new DeviceDetails[]{ + deviceDetails2ClientA + })); + testCases.add(new TestCase( + "matching none", + null, + Param.push(null, "deviceId", "madeup"), + new DeviceDetails[]{})); + + testCases.run(); + } + + // RHS1c1 + @Test + public void push_admin_channelSubscriptions_list() throws Exception { + class TestCase extends TestCases.Base { + private final Param[] params; + private final ChannelSubscription[] expected; + + TestCase(String name, Param[] params, ChannelSubscription[] expected, String expectedError, int expectedStatusCode) { + super(name, expectedError, expectedStatusCode); + this.params = params; + this.expected = expected; + } + + @Override + public void run() throws Exception { + new Helpers.SyncAndAsync() { + @Override + public ChannelSubscription[] getSync(Void arg) throws AblyException { + return rest.push.admin.channelSubscriptions.list(params).items(); + } + + @Override + public void getAsync(Void arg, Callback callback) { + rest.push.admin.channelSubscriptions.listAsync(params, new Callback.Map, ChannelSubscription[]>(callback) { + @Override + public ChannelSubscription[] map(AsyncPaginatedResult result) { + return result.items(); + } + }); + } + + @Override + public void then(Helpers.AblyFunction get) throws AblyException { + Helpers.assertArrayUnorderedEquals(expected, get.apply(null)); + } + }.run(); + } + } + + TestCases testCases = new TestCases(); + + testCases.add(new TestCase( + "by deviceId", + Param.push(null, "deviceId", deviceDetails4ClientC.id), + new ChannelSubscription[]{subscriptionFooDevice4}, + null, 0)); + testCases.add(new TestCase( + "by clientId A", + Param.push(null, "clientId", "clientA"), + new ChannelSubscription[]{subscriptionFooClientA}, + null, 0)); + testCases.add(new TestCase( + "by clientId B", + Param.push(null, "clientId", "clientB"), + new ChannelSubscription[]{subscriptionFooClientB, subscriptionBarClientB}, + null, 0)); + testCases.add(new TestCase( + "none", + Param.push(null, "deviceId", "madeup"), + new ChannelSubscription[]{}, + null, 0)); + testCases.add(new TestCase( + "by clientId B and channel", + Param.push(Param.push(null, "clientId", "clientB"), "channel", "pushenabled:bar"), + new ChannelSubscription[]{subscriptionBarClientB}, + null, 0)); + + testCases.run(); + } + + // RHS1c2 + @Test + @Ignore("FIXME: tests interfere") + public void push_admin_channelSubscriptions_listChannels() throws Exception { + new Helpers.SyncAndAsync(){ + @Override + public String[] getSync(Void aVoid) throws AblyException { + return rest.push.admin.channelSubscriptions.listChannels(null).items(); + } + + @Override + public void getAsync(Void aVoid, Callback callback) { + rest.push.admin.channelSubscriptions.listChannelsAsync(null, new Callback.Map, String[]>(callback) { + @Override + public String[] map(AsyncPaginatedResult result) { + return result.items(); + } + }); + } + + @Override + public void then(Helpers.AblyFunction get) throws AblyException { + Helpers.assertArrayUnorderedEquals(new String[]{"pushenabled:foo", "pushenabled:bar"}, get.apply(null)); + } + }.run(); + } // RHS1c3 - @Test - public void push_admin_channelSubscriptions_save() throws Exception { - new Helpers.SyncAndAsync() { - @Override - public ChannelSubscription getSync(ChannelSubscription saved) throws AblyException { - return rest.push.admin.channelSubscriptions.save(saved); - } - - @Override - public void getAsync(ChannelSubscription saved, Callback callback) { - rest.push.admin.channelSubscriptions.saveAsync(saved, callback); - } - - @Override - public void then(final Helpers.AblyFunction get) throws AblyException { - ChannelSubscription saved = ChannelSubscription.forClientId("pushenabled:qux", "newClient"); - try { - // Save new - ChannelSubscription got = get.apply(saved); - Helpers.RawHttpRequest request = httpTracker.getLastRequest(); - assertEquals("POST", request.method); - assertEquals("/push/channelSubscriptions", request.url.getPath()); - assertEquals(saved, got); - - // Failing - Helpers.expectedError(new Helpers.AblyFunction() { - @Override - public Void apply(Void aVoid) throws AblyException { - get.apply(ChannelSubscription.forClientId("notpushenabled", "foo")); - return null; - } - }, "not enabled", 401); - } finally { - rest.push.admin.channelSubscriptions.remove(saved); - } - } - }.run(); - } - - // RHS1c4 - @Test - public void push_admin_channelSubscriptions_remove() throws Exception { - final ChannelSubscription saved = ChannelSubscription.forClientId("pushenabled:qux", "newClient"); - - new Helpers.SyncAndAsync() { - @Override - public Void getSync(Void aVoid) throws AblyException { - rest.push.admin.channelSubscriptions.remove(saved); - return null; - } - - @Override - public void getAsync(Void aVoid, Callback callback) { - rest.push.admin.channelSubscriptions.removeAsync(saved, new CompletionListener.FromCallback(callback)); - } - - @Override - public void then(Helpers.AblyFunction get) throws AblyException { - try { - rest.push.admin.channelSubscriptions.save(saved); - - // Ensure it exists - assertEquals(1, rest.push.admin.channelSubscriptions.list(Param.push(Param.push(null, "channel", "pushenabled:qux"), "clientId", "newClient")).items().length); - - // Existing - get.apply(null); - assertEquals(0, rest.push.admin.channelSubscriptions.list(Param.push(Param.push(null, "channel", "pushenabled:qux"), "clientId", "newClient")).items().length); - - // Non-existing - get.apply(null); - } finally { - rest.push.admin.channelSubscriptions.remove(saved); - } - } - }.run(); - } - - // RHS1c5 - @Test - public void push_admin_channelSubscriptions_removeWhere() throws Exception { - class TestCase extends TestCases.Base { - private final Param[] params; - private final ChannelSubscription[] expectedRemoved; - - public TestCase(String name, String expectedError, Param[] params, ChannelSubscription[] expectedRemoved) { - super(name, expectedError); - this.params = Param.push(params, "fullWait", "true"); - this.expectedRemoved = expectedRemoved; - } - - @Override - public void run() throws Exception { - new Helpers.SyncAndAsync() { - @Override - public Void getSync(Param[] params) throws AblyException { - rest.push.admin.channelSubscriptions.removeWhere(params); - return null; - } - - @Override - public void getAsync(Param[] params, Callback callback) { - rest.push.admin.channelSubscriptions.removeWhereAsync(params, new CompletionListener.FromCallback(callback)); - } - - @Override - public void then(Helpers.AblyFunction get) throws AblyException { - try { - get.apply(params); - - PaginatedResult result = rest.push.admin.channelSubscriptions.list(params); - assertEquals(0, result.items().length); - } finally { - for (ChannelSubscription removed : expectedRemoved) { - rest.push.admin.channelSubscriptions.save(removed); - } - } - } - }.run(); - } - } - - TestCases testCases = new TestCases(); - - testCases.add(new TestCase( - "by clientId", - null, - Param.push(null, "clientId", "clientB"), - new ChannelSubscription[]{ - subscriptionFooClientB, - subscriptionBarClientB - })); - testCases.add(new TestCase( - "by clientId and channel", - null, - Param.push(Param.push(null, "clientId", "clientB"), "channel", "pushenabled:foo"), - new ChannelSubscription[]{ - subscriptionFooClientB, - })); - testCases.add(new TestCase( - "by deviceId", - null, - Param.push(null, "deviceId", subscriptionBarDevice2.deviceId), - new ChannelSubscription[]{ - subscriptionFooDevice2, - subscriptionBarDevice2 - })); - testCases.add(new TestCase( - "matching none", - null, - Param.push(null, "deviceId", "madeup"), - new ChannelSubscription[]{})); - - testCases.run(); - } + @Test + public void push_admin_channelSubscriptions_save() throws Exception { + new Helpers.SyncAndAsync() { + @Override + public ChannelSubscription getSync(ChannelSubscription saved) throws AblyException { + return rest.push.admin.channelSubscriptions.save(saved); + } + + @Override + public void getAsync(ChannelSubscription saved, Callback callback) { + rest.push.admin.channelSubscriptions.saveAsync(saved, callback); + } + + @Override + public void then(final Helpers.AblyFunction get) throws AblyException { + ChannelSubscription saved = ChannelSubscription.forClientId("pushenabled:qux", "newClient"); + try { + // Save new + ChannelSubscription got = get.apply(saved); + Helpers.RawHttpRequest request = httpTracker.getLastRequest(); + assertEquals("POST", request.method); + assertEquals("/push/channelSubscriptions", request.url.getPath()); + assertEquals(saved, got); + + // Failing + Helpers.expectedError(new Helpers.AblyFunction() { + @Override + public Void apply(Void aVoid) throws AblyException { + get.apply(ChannelSubscription.forClientId("notpushenabled", "foo")); + return null; + } + }, "not enabled", 401); + } finally { + rest.push.admin.channelSubscriptions.remove(saved); + } + } + }.run(); + } + + // RHS1c4 + @Test + public void push_admin_channelSubscriptions_remove() throws Exception { + final ChannelSubscription saved = ChannelSubscription.forClientId("pushenabled:qux", "newClient"); + + new Helpers.SyncAndAsync() { + @Override + public Void getSync(Void aVoid) throws AblyException { + rest.push.admin.channelSubscriptions.remove(saved); + return null; + } + + @Override + public void getAsync(Void aVoid, Callback callback) { + rest.push.admin.channelSubscriptions.removeAsync(saved, new CompletionListener.FromCallback(callback)); + } + + @Override + public void then(Helpers.AblyFunction get) throws AblyException { + try { + rest.push.admin.channelSubscriptions.save(saved); + + // Ensure it exists + assertEquals(1, rest.push.admin.channelSubscriptions.list(Param.push(Param.push(null, "channel", "pushenabled:qux"), "clientId", "newClient")).items().length); + + // Existing + get.apply(null); + assertEquals(0, rest.push.admin.channelSubscriptions.list(Param.push(Param.push(null, "channel", "pushenabled:qux"), "clientId", "newClient")).items().length); + + // Non-existing + get.apply(null); + } finally { + rest.push.admin.channelSubscriptions.remove(saved); + } + } + }.run(); + } + + // RHS1c5 + @Test + public void push_admin_channelSubscriptions_removeWhere() throws Exception { + class TestCase extends TestCases.Base { + private final Param[] params; + private final ChannelSubscription[] expectedRemoved; + + public TestCase(String name, String expectedError, Param[] params, ChannelSubscription[] expectedRemoved) { + super(name, expectedError); + this.params = Param.push(params, "fullWait", "true"); + this.expectedRemoved = expectedRemoved; + } + + @Override + public void run() throws Exception { + new Helpers.SyncAndAsync() { + @Override + public Void getSync(Param[] params) throws AblyException { + rest.push.admin.channelSubscriptions.removeWhere(params); + return null; + } + + @Override + public void getAsync(Param[] params, Callback callback) { + rest.push.admin.channelSubscriptions.removeWhereAsync(params, new CompletionListener.FromCallback(callback)); + } + + @Override + public void then(Helpers.AblyFunction get) throws AblyException { + try { + get.apply(params); + + PaginatedResult result = rest.push.admin.channelSubscriptions.list(params); + assertEquals(0, result.items().length); + } finally { + for (ChannelSubscription removed : expectedRemoved) { + rest.push.admin.channelSubscriptions.save(removed); + } + } + } + }.run(); + } + } + + TestCases testCases = new TestCases(); + + testCases.add(new TestCase( + "by clientId", + null, + Param.push(null, "clientId", "clientB"), + new ChannelSubscription[]{ + subscriptionFooClientB, + subscriptionBarClientB + })); + testCases.add(new TestCase( + "by clientId and channel", + null, + Param.push(Param.push(null, "clientId", "clientB"), "channel", "pushenabled:foo"), + new ChannelSubscription[]{ + subscriptionFooClientB, + })); + testCases.add(new TestCase( + "by deviceId", + null, + Param.push(null, "deviceId", subscriptionBarDevice2.deviceId), + new ChannelSubscription[]{ + subscriptionFooDevice2, + subscriptionBarDevice2 + })); + testCases.add(new TestCase( + "matching none", + null, + Param.push(null, "deviceId", "madeup"), + new ChannelSubscription[]{})); + + testCases.run(); + } } diff --git a/lib/src/test/java/io/ably/lib/test/rest/RestRequestTest.java b/lib/src/test/java/io/ably/lib/test/rest/RestRequestTest.java index a4c642983..a371c8250 100644 --- a/lib/src/test/java/io/ably/lib/test/rest/RestRequestTest.java +++ b/lib/src/test/java/io/ably/lib/test/rest/RestRequestTest.java @@ -38,683 +38,683 @@ /* Spec: RSC19 */ public class RestRequestTest extends ParameterizedTest { - private AblyRest setupAbly; - private String channelName; - private String channelAltName; - private String channelNamePrefix; - private String channelPath; - private String channelsPath; - private String channelMessagesPath; - - @Rule - public Timeout testTimeout = Timeout.seconds(30); - - @Before - public void setUpBefore() throws Exception { - ClientOptions opts = createOptions(testVars.keys[0].keyStr); - setupAbly = new AblyRest(opts); - channelNamePrefix = "persisted:rest_request_" + testParams.name; - channelName = channelNamePrefix + "_channel"; - channelAltName = channelNamePrefix + "_alt_channel"; - channelsPath = "/channels"; - channelPath = channelsPath + "/" + channelName; - channelMessagesPath = channelPath + "/messages"; - - /* publish events */ - Channel channel = setupAbly.channels.get(channelName); - for(int i = 0; i < 4; i++) { - channel.publish("Test event", "Test data " + i); - } - Channel altChannel = setupAbly.channels.get(channelAltName); - for(int i = 0; i < 4; i++) { - altChannel.publish("Test event", "Test alt data " + i); - } - - /* wait to persist */ - try { Thread.sleep(1000L); } catch(InterruptedException ie) {} - } - - /** - * Get channel details using the request() API - * Spec: RSC19a, RSC19d, HP1, HP3, HP4, HP5, HP8 - */ - @Test - public void request_simple() { - try { - DebugOptions opts = new DebugOptions(testVars.keys[0].keyStr); - fillInOptions(opts); - RawHttpTracker httpListener = new RawHttpTracker(); - opts.httpListener = httpListener; - AblyRest ably = new AblyRest(opts); - - Param[] testParams = new Param[] { new Param("testParam", "testValue") }; - Param[] testHeaders = new Param[] { new Param("x-test-header", "testValue") }; - HttpPaginatedResponse channelResponse = ably.request(HttpConstants.Methods.GET, channelPath, testParams, null, testHeaders); - - /* check HttpPagninatedResponse details are present */ - assertEquals("Verify statusCode is present", channelResponse.statusCode, 200); - assertTrue("Verify success is indicated", channelResponse.success); - assertNull("Verify no error is indicated", channelResponse.errorMessage); - Map headers = HttpUtils.indexParams(channelResponse.headers); - assertEquals("Verify Content-Type header is present", headers.get("content-type").value, "application/json"); - - /* check it looks like a ChannelDetails */ - assertNotNull("Verify a result is returned", channelResponse); - JsonElement[] items = channelResponse.items(); - assertEquals("Verify a single items is returned", items.length, 1); - JsonElement channelDetails = items[0]; - assertTrue("Verify an object is returned", channelDetails.isJsonObject()); - assertTrue("Verify channelId member is present", channelDetails.getAsJsonObject().has("channelId")); - assertEquals("Verify channelId member is channelName", channelName, channelDetails.getAsJsonObject().get("channelId").getAsString()); - - /* check request has expected attributes; use last request in case of challenges preceding sending auth header */ - RawHttpRequest req = httpListener.getLastRequest(); - /* Spec: RSC19b */ - assertNotNull("Verify Authorization header present", httpListener.getRequestHeader(req.id, "Authorization")); - /* Spec: RSC19c */ - assertTrue("Verify Accept header present", httpListener.getRequestHeader(req.id, "Accept").contains("application/json")); - assertTrue("Verify Content-Type header present", httpListener.getResponseHeader(req.id, "Content-Type").contains("application/json")); - } catch(AblyException e) { - e.printStackTrace(); - fail("request_simple: Unexpected exception"); - return; - } - } - - /** - * Get channel details using the requestAsync() API - * Spec: RSC19a, RSC19d, HP1, HP3, HP4, HP5, HP8 - */ - @Test - public void request_simple_async() { - final Waiter waiter = new Waiter(); - DebugOptions opts; - try { - opts = new DebugOptions(testVars.keys[0].keyStr); - fillInOptions(opts); - final RawHttpTracker httpListener = new RawHttpTracker(); - opts.httpListener = httpListener; - AblyRest ably = new AblyRest(opts); - - ably.requestAsync(HttpConstants.Methods.GET, channelPath, null, null, null, new AsyncHttpPaginatedResponse.Callback() { - @Override - public void onResponse(AsyncHttpPaginatedResponse channelResponse) { - - /* check HttpPaginatedResponse details are present */ - waiter.assertEquals(channelResponse.statusCode, 200); - waiter.assertTrue(channelResponse.success); - waiter.assertNull(channelResponse.errorMessage); - Map headers = HttpUtils.indexParams(channelResponse.headers); - waiter.assertEquals(headers.get("content-type").value, "application/json"); - - /* check it looks like a ChannelDetails */ - /* Verify a result is returned */ - waiter.assertNotNull(channelResponse); - JsonElement[] items = channelResponse.items(); - waiter.assertEquals(items.length, 1); - JsonElement channelDetails = items[0]; - waiter.assertTrue(channelDetails.isJsonObject()); - waiter.assertTrue(channelDetails.getAsJsonObject().has("channelId")); - waiter.assertEquals(channelName, channelDetails.getAsJsonObject().get("channelId").getAsString()); - - /* check request has expected attributes */ - RawHttpRequest req = httpListener.values().iterator().next(); - /* Spec: RSC19b */ - waiter.assertNotNull(httpListener.getRequestHeader(req.id, "Authorization")); - /* Spec: RSC19c */ - waiter.assertTrue(httpListener.getRequestHeader(req.id, "Accept").contains("application/json")); - waiter.assertTrue(httpListener.getResponseHeader(req.id, "Content-Type").contains("application/json")); - waiter.resume(); - } - @Override - public void onError(ErrorInfo reason) { - waiter.fail("request_simple_async: Unexpected exception"); - waiter.resume(); - } - }); - - try { - waiter.await(15000); - } catch (TimeoutException e) { - fail("request_simple_async: Operation timed out"); - } - } catch (AblyException e) { - e.printStackTrace(); - fail("request_simple_async: Unexpected exception"); - } - } - - /** - * Get channel details using the paginatedRequest() API - * Spec: HP2 - */ - @Test - public void request_paginated() { - try { - DebugOptions opts = new DebugOptions(testVars.keys[0].keyStr); - fillInOptions(opts); - AblyRest ably = new AblyRest(opts); - - Param[] params = new Param[] { new Param("prefix", channelNamePrefix) }; - HttpPaginatedResponse channelsResponse = ably.request(HttpConstants.Methods.GET, channelsPath, params, null, null); - - /* check HttpPagninatedResponse details are present */ - assertEquals("Verify statusCode is present", channelsResponse.statusCode, 200); - assertTrue("Verify success is indicated", channelsResponse.success); - assertNull("Verify no error is indicated", channelsResponse.errorMessage); - Map headers = HttpUtils.indexParams(channelsResponse.headers); - assertEquals("Verify Content-Type header is present", headers.get("content-type").value, "application/json"); - - /* check it looks like an array of ChannelDetails */ - assertNotNull("Verify a result is returned", channelsResponse); - JsonElement[] items = channelsResponse.items(); - assertTrue("Verify at least two channels are returned", items.length >= 2); - for(int i = 0; i < items.length; i++) { - assertTrue("Verify channelId member is a matching channelName", items[i].getAsJsonObject().get("channelId").getAsString().startsWith(channelNamePrefix)); - } - - /* check that there is either no next link, or no results from it */ - if(channelsResponse.hasNext()) { - channelsResponse = channelsResponse.next(); - items = channelsResponse.items(); - assertEquals("Verify no further channels are returned", items.length, 0); - } - } catch(AblyException e) { - e.printStackTrace(); - fail("request_simple: Unexpected exception"); - return; - } - } - - /** - * Get channel details using the paginatedRequestAsync() API - * Spec: HP2 - */ - @Test - public void request_paginated_async() { - final Waiter waiter = new Waiter(); - try { - DebugOptions opts = new DebugOptions(testVars.keys[0].keyStr); - fillInOptions(opts); - AblyRest ably = new AblyRest(opts); - - Param[] params = new Param[] { new Param("prefix", channelNamePrefix) }; - ably.requestAsync(HttpConstants.Methods.GET, channelsPath, params, null, null, new AsyncHttpPaginatedResponse.Callback() { - @Override - public void onResponse(AsyncHttpPaginatedResponse channelResponse) { - - /* check HttpPaginatedResponse details are present */ - waiter.assertEquals(channelResponse.statusCode, 200); - waiter.assertTrue(channelResponse.success); - waiter.assertNull(channelResponse.errorMessage); - Map headers = HttpUtils.indexParams(channelResponse.headers); - waiter.assertEquals(headers.get("content-type").value, "application/json"); - - /* check it looks like an array of ChannelDetails */ - waiter.assertNotNull(channelResponse); - JsonElement[] items = channelResponse.items(); - waiter.assertTrue(items.length >= 2); - for(int i = 0; i < items.length; i++) { - waiter.assertTrue(items[i].getAsJsonObject().get("channelId").getAsString().startsWith(channelNamePrefix)); - } - /* check that there is either no next link, or no results from it */ - if(!channelResponse.hasNext()) { - waiter.resume(); - return; - } - channelResponse.next(new AsyncHttpPaginatedResponse.Callback() { - @Override - public void onResponse(AsyncHttpPaginatedResponse channelResponse) { - JsonElement[] items = channelResponse.items(); - assertEquals("Verify no further channels are returned", items.length, 0); - waiter.resume(); - } - @Override - public void onError(ErrorInfo reason) { - waiter.fail("request_paginated_async: Unexpected exception"); - waiter.resume(); - } - }); - } - @Override - public void onError(ErrorInfo reason) { - waiter.fail("request_paginated_async: Unexpected exception"); - waiter.resume(); - } - }); - - try { - waiter.await(15000); - } catch (TimeoutException e) { - fail("request_paginated_async: Operation timed out"); - } - } catch(AblyException e) { - e.printStackTrace(); - fail("request_paginated_async: Unexpected exception"); - return; - } - } - - /** - * Get channel details using the paginatedRequest() API with a specified limit, - * checking pagination links - * Spec: HP2 - */ - @Test - public void request_paginated_limit() { - try { - DebugOptions opts = new DebugOptions(testVars.keys[0].keyStr); - fillInOptions(opts); - AblyRest ably = new AblyRest(opts); - - Param[] params = new Param[] { new Param("prefix", channelNamePrefix), new Param("limit", "1") }; - HttpPaginatedResponse channelsResponse = ably.request(HttpConstants.Methods.GET, channelsPath, params, null, null); - - /* check HttpPagninatedResponse details are present */ - assertEquals("Verify statusCode is present", channelsResponse.statusCode, 200); - assertTrue("Verify success is indicated", channelsResponse.success); - assertNull("Verify no error is indicated", channelsResponse.errorMessage); - Map headers = HttpUtils.indexParams(channelsResponse.headers); - assertEquals("Verify Content-Type header is present", headers.get("content-type").value, "application/json"); - - /* check it looks like an array of ChannelDetails */ - assertNotNull("Verify a result is returned", channelsResponse); - JsonElement[] items = channelsResponse.items(); - assertTrue("Verify one channel is returned", items.length == 1); - for(int i = 0; i < items.length; i++) { - assertTrue("Verify channelId member is a matching channelName", items[i].getAsJsonObject().get("channelId").getAsString().startsWith(channelNamePrefix)); - } - - /* get next page */ - channelsResponse = channelsResponse.next(); - assertNotNull("Verify a result is returned", channelsResponse); - items = channelsResponse.items(); - assertTrue("Verify one channel is returned", items.length == 1); - for(int i = 0; i < items.length; i++) { - assertTrue("Verify channelId member is a matching channelName", items[i].getAsJsonObject().get("channelId").getAsString().startsWith(channelNamePrefix)); - } - - /* get first page */ - HttpPaginatedResponse firstResponse = channelsResponse.first(); - assertNotNull("Verify a result is returned", firstResponse); - items = channelsResponse.items(); - assertTrue("Verify one channel is returned", items.length == 1); - for(int i = 0; i < items.length; i++) { - assertTrue("Verify channelId member is a matching channelName", items[i].getAsJsonObject().get("channelId").getAsString().startsWith(channelNamePrefix)); - } - - /* check that there is either no next link, or no results from it */ - if(channelsResponse.hasNext()) { - channelsResponse = channelsResponse.next(); - items = channelsResponse.items(); - assertEquals("Verify no further channels are returned", items.length, 0); - } - } catch(AblyException e) { - e.printStackTrace(); - fail("request_paginated_limit: Unexpected exception"); - return; - } - } - - /** - * Get channel details using the paginatedRequestAsync() API with a specified limit, - * checking pagination links - * Spec: HP2 - */ - @Test - public void request_paginated_async_limit() { - final Waiter waiter = new Waiter(); - try { - DebugOptions opts = new DebugOptions(testVars.keys[0].keyStr); - fillInOptions(opts); - AblyRest ably = new AblyRest(opts); - - Param[] params = new Param[] { new Param("prefix", channelNamePrefix), new Param("limit", "1") }; - ably.requestAsync(HttpConstants.Methods.GET, channelsPath, params, null, null, new AsyncHttpPaginatedResponse.Callback() { - @Override - public void onResponse(AsyncHttpPaginatedResponse channelsResponse) { - - /* check HttpPagninatedResponse details are present */ - assertEquals("Verify statusCode is present", channelsResponse.statusCode, 200); - assertTrue("Verify success is indicated", channelsResponse.success); - assertNull("Verify no error is indicated", channelsResponse.errorMessage); - Map headers = HttpUtils.indexParams(channelsResponse.headers); - assertEquals("Verify Content-Type header is present", headers.get("content-type").value, "application/json"); - - /* check it looks like an array of ChannelDetails */ - waiter.assertNotNull(channelsResponse); - JsonElement[] items = channelsResponse.items(); - waiter.assertTrue(items.length == 1); - for(int i = 0; i < items.length; i++) { - waiter.assertTrue(items[i].getAsJsonObject().get("channelId").getAsString().startsWith(channelNamePrefix)); - } - channelsResponse.next(new AsyncHttpPaginatedResponse.Callback() { - @Override - public void onResponse(final AsyncHttpPaginatedResponse channelsResponse) { - /* check it looks like an array of ChannelDetails */ - waiter.assertNotNull(channelsResponse); - JsonElement[] items = channelsResponse.items(); - waiter.assertTrue(items.length == 1); - for(int i = 0; i < items.length; i++) { - waiter.assertTrue(items[i].getAsJsonObject().get("channelId").getAsString().startsWith(channelNamePrefix)); - } - - /* check that there is a first link */ - channelsResponse.first(new AsyncHttpPaginatedResponse.Callback() { - @Override - public void onResponse(AsyncHttpPaginatedResponse firstResponse) { - waiter.assertNotNull(firstResponse); - JsonElement[] items = firstResponse.items(); - waiter.assertTrue(items.length == 1); - for(int i = 0; i < items.length; i++) { - waiter.assertTrue(items[i].getAsJsonObject().get("channelId").getAsString().startsWith(channelNamePrefix)); - } - - /* check that there is either no next link, or no results from it */ - if(!channelsResponse.hasNext()) { - waiter.resume(); - return; - } - channelsResponse.next(new AsyncHttpPaginatedResponse.Callback() { - @Override - public void onResponse(AsyncHttpPaginatedResponse result) { - JsonElement[] items = result.items(); - waiter.assertEquals(items.length, 0); - waiter.resume(); - } - @Override - public void onError(ErrorInfo reason) { - waiter.fail("request_paginated_async_limit: Unexpected exception"); - waiter.resume(); - } - }); - } - @Override - public void onError(ErrorInfo reason) { - waiter.fail("request_paginated_async_limit: Unexpected exception"); - waiter.resume(); - }}); - } - @Override - public void onError(ErrorInfo reason) { - waiter.fail("request_paginated_async_limit: Unexpected exception"); - waiter.resume(); - } - }); - } - @Override - public void onError(ErrorInfo reason) { - waiter.fail("request_paginated_async_limit: Unexpected exception"); - waiter.resume(); - } - }); - - try { - waiter.await(15000); - } catch (TimeoutException e) { - fail("request_paginated_async: Operation timed out"); - } - } catch(AblyException e) { - e.printStackTrace(); - fail("request_paginated_async_limit: Unexpected exception"); - return; - } - } - - /** - * Publish a message using the request() API - * Spec: RSC19a, RSC19b - * - */ - @Test - public void request_post() { - final String messageData = "Test data (request_post)"; - DebugOptions opts; - try { - opts = new DebugOptions(testVars.keys[0].keyStr); - fillInOptions(opts); - final RawHttpTracker httpListener = new RawHttpTracker(); - opts.httpListener = httpListener; - AblyRest ably = new AblyRest(opts); - - /* publish a message */ - Message message = new Message("Test event", messageData); - HttpUtils.JsonRequestBody requestBody = new HttpUtils.JsonRequestBody(message); - HttpPaginatedResponse publishResponse = ably.request(HttpConstants.Methods.POST, channelMessagesPath, null, requestBody, null); - RawHttpRequest req = httpListener.getLastRequest(); - - /* check HttpPagninatedResponse details are present */ - assertEquals("Verify statusCode is present", publishResponse.statusCode, 201); - assertTrue("Verify success is indicated", publishResponse.success); - assertNull("Verify no error is indicated", publishResponse.errorMessage); - - /* wait to persist */ - try { Thread.sleep(1000L); } catch(InterruptedException ie) {} - - /* get the history */ - Param[] params = new Param[] { new Param("limit", "1") }; - PaginatedResult resultPage = ably.channels.get(channelName).history(params); - - /* check it looks like a result page */ - assertNotNull("Verify a result is returned", resultPage); - assertTrue("Verify an single message is returned", resultPage.items().length == 1); - assertEquals("Verify returned message was the one posted", messageData, resultPage.items()[0].data); - - /* check request has expected attributes */ - /* Spec: RSC19b */ - assertNotNull("Verify Authorization header present", httpListener.getRequestHeader(req.id, "authorization")); - /* Spec: RSC19c */ - assertTrue("Verify Accept header present", httpListener.getRequestHeader(req.id, "Accept").contains("application/json")); - assertTrue("Verify Content-Type header present", httpListener.getRequestHeader(req.id, "Content-Type").contains("application/json")); - assertTrue("Verify Content-Type header present", httpListener.getResponseHeader(req.id, "Content-Type").contains("application/json")); - } catch(AblyException e) { - e.printStackTrace(); - fail("request_post: Unexpected exception"); - return; - } - } - - /** - * Publish a message using the requestAsync() API - * Spec: RSC19a, RSC19b - */ - @Test - public void request_post_async() { - final Waiter waiter = new Waiter(); - final String messageData = "Test data (request_post_async)"; - DebugOptions opts; - try { - opts = new DebugOptions(testVars.keys[0].keyStr); - fillInOptions(opts); - final RawHttpTracker httpListener = new RawHttpTracker(); - opts.httpListener = httpListener; - AblyRest ably = new AblyRest(opts); - - /* publish a message */ - Message message = new Message("Test event", messageData); - HttpUtils.JsonRequestBody requestBody = new HttpUtils.JsonRequestBody(message); - ably.requestAsync(HttpConstants.Methods.POST, channelMessagesPath, null, requestBody, null, new AsyncHttpPaginatedResponse.Callback() { - @Override - public void onResponse(AsyncHttpPaginatedResponse publishResponse) { - - /* check HttpPaginatedResponse details are present */ - assertEquals("Verify statusCode is present", publishResponse.statusCode, 201); - assertTrue("Verify success is indicated", publishResponse.success); - assertNull("Verify no error is indicated", publishResponse.errorMessage); - - /* wait to persist */ - try { Thread.sleep(1000L); } catch(InterruptedException ie) {} - - /* get the history */ - Param[] params = new Param[] { new Param("limit", "1") }; - PaginatedResult resultPage; - try { - resultPage = setupAbly.channels.get(channelName).history(params); - - /* check it looks like a result page */ - waiter.assertNotNull(resultPage); - waiter.assertTrue(resultPage.items().length == 1); - waiter.assertEquals(messageData, resultPage.items()[0].data); - - /* check request has expected attributes */ - RawHttpRequest req = httpListener.values().iterator().next(); - /* Spec: RSC19b */ - waiter.assertNotNull(httpListener.getRequestHeader(req.id, "Authorization")); - /* Spec: RSC19c */ - waiter.assertTrue(httpListener.getRequestHeader(req.id, "Accept").contains("application/json")); - waiter.assertTrue(httpListener.getRequestHeader(req.id, "Content-Type").contains("application/json")); - waiter.assertTrue(httpListener.getResponseHeader(req.id, "Content-Type").contains("application/json")); - waiter.resume(); - } catch (AblyException e) { - e.printStackTrace(); - waiter.fail("request_post_async: Unexpected exception"); - waiter.resume(); - } - } - @Override - public void onError(ErrorInfo reason) { - waiter.fail("request_post_async: Unexpected exception"); - waiter.resume(); - } - }); - - try { - waiter.await(15000); - } catch (TimeoutException e) { - fail("request_paginated_async: Operation timed out"); - } - } catch(AblyException e) { - e.printStackTrace(); - fail("request_post_async: Unexpected exception"); - return; - } - } - - /** - * Verify 400 error responses are indicated with an HttpPaginatedResponse - * Spec: RSC19e, HP4, HP5, HP6, HP7 - */ - @Test - public void request_404() { - try { - DebugOptions opts = new DebugOptions(testVars.keys[0].keyStr); - fillInOptions(opts); - AblyRest ably = new AblyRest(opts); - HttpPaginatedResponse errorResponse = ably.request(HttpConstants.Methods.GET, "/non-existent-path", null, null, null); - - /* check HttpPaginatedResponse details are present */ - assertEquals("Verify statusCode is present", errorResponse.statusCode, 404); - assertFalse("Verify non-success is indicated", errorResponse.success); - assertNotNull("Verify error is indicated", errorResponse.errorMessage); - Map headers = HttpUtils.indexParams(errorResponse.headers); - assertEquals("Verify Content-Type header is present", headers.get("content-type").value, "application/json"); - } catch(AblyException e) { - e.printStackTrace(); - fail("request_404: Unexpected exception"); - } - } - - /** - * Verify 400 error responses are indicated with an response callback - * Spec: RSC19e, HP4, HP5, HP6, HP7 - */ - @Test - public void request_404_async() { - try { - final Waiter waiter = new Waiter(); - DebugOptions opts = new DebugOptions(testVars.keys[0].keyStr); - fillInOptions(opts); - AblyRest ably = new AblyRest(opts); - - ably.requestAsync(HttpConstants.Methods.GET, "/non-existent-path", null, null, null, new AsyncHttpPaginatedResponse.Callback() { - @Override - public void onResponse(AsyncHttpPaginatedResponse response) { - - /* check HttpPaginatedResponse details are present */ - waiter.assertEquals(response.statusCode, 404); - waiter.assertFalse(response.success); - waiter.assertNotNull(response.errorMessage); - waiter.assertTrue(response.errorCode != 0); - Map headers = HttpUtils.indexParams(response.headers); - waiter.assertEquals(headers.get("content-type").value, "application/json"); - waiter.resume(); - } - @Override - public void onError(ErrorInfo reason) { - waiter.fail("request_404_async: Expected a response callback"); - waiter.resume(); - } - }); - - try { - waiter.await(15000); - } catch (TimeoutException e) { - fail("request_404_async: Operation timed out"); - } - } catch(AblyException e) { - e.printStackTrace(); - fail("request_404_async: Unexpected exception"); - return; - } - } - - - /** - * Verify 500 error responses are indicated with an exception - * Spec: RSC19e - */ - @Test - public void request_500() { - try { - DebugOptions opts = new DebugOptions(testVars.keys[0].keyStr); - fillInOptions(opts); - opts.environment = "non.existent.env"; - AblyRest ably = new AblyRest(opts); - - ably.request(HttpConstants.Methods.GET, "/", null, null, null); - fail("request_500: Expected an exception"); - } catch(AblyException e) { - assertEquals("Verify expected status code in error response", e.errorInfo.statusCode, 500); - return; - } - } - - /** - * Verify 500 error responses are indicated with an error callback - * Spec: RSC19e - */ - @Test - public void request_500_async() { - try { - final Waiter waiter = new Waiter(); - DebugOptions opts = new DebugOptions(testVars.keys[0].keyStr); - fillInOptions(opts); - opts.environment = "non.existent.env"; - AblyRest ably = new AblyRest(opts); - - ably.requestAsync(HttpConstants.Methods.GET, "/", null, null, null, new AsyncHttpPaginatedResponse.Callback() { - @Override - public void onResponse(AsyncHttpPaginatedResponse response) { - waiter.fail("request_500_async: Expected an error"); - waiter.resume(); - } - @Override - public void onError(ErrorInfo reason) { - waiter.assertEquals(reason.statusCode, 500); - waiter.resume(); - } - }); - - try { - waiter.await(15000); - } catch (TimeoutException e) { - fail("request_500_async: Operation timed out"); - } - } catch(AblyException e) { - e.printStackTrace(); - fail("request_500_async: Unexpected exception"); - return; - } - } + private AblyRest setupAbly; + private String channelName; + private String channelAltName; + private String channelNamePrefix; + private String channelPath; + private String channelsPath; + private String channelMessagesPath; + + @Rule + public Timeout testTimeout = Timeout.seconds(30); + + @Before + public void setUpBefore() throws Exception { + ClientOptions opts = createOptions(testVars.keys[0].keyStr); + setupAbly = new AblyRest(opts); + channelNamePrefix = "persisted:rest_request_" + testParams.name; + channelName = channelNamePrefix + "_channel"; + channelAltName = channelNamePrefix + "_alt_channel"; + channelsPath = "/channels"; + channelPath = channelsPath + "/" + channelName; + channelMessagesPath = channelPath + "/messages"; + + /* publish events */ + Channel channel = setupAbly.channels.get(channelName); + for(int i = 0; i < 4; i++) { + channel.publish("Test event", "Test data " + i); + } + Channel altChannel = setupAbly.channels.get(channelAltName); + for(int i = 0; i < 4; i++) { + altChannel.publish("Test event", "Test alt data " + i); + } + + /* wait to persist */ + try { Thread.sleep(1000L); } catch(InterruptedException ie) {} + } + + /** + * Get channel details using the request() API + * Spec: RSC19a, RSC19d, HP1, HP3, HP4, HP5, HP8 + */ + @Test + public void request_simple() { + try { + DebugOptions opts = new DebugOptions(testVars.keys[0].keyStr); + fillInOptions(opts); + RawHttpTracker httpListener = new RawHttpTracker(); + opts.httpListener = httpListener; + AblyRest ably = new AblyRest(opts); + + Param[] testParams = new Param[] { new Param("testParam", "testValue") }; + Param[] testHeaders = new Param[] { new Param("x-test-header", "testValue") }; + HttpPaginatedResponse channelResponse = ably.request(HttpConstants.Methods.GET, channelPath, testParams, null, testHeaders); + + /* check HttpPagninatedResponse details are present */ + assertEquals("Verify statusCode is present", channelResponse.statusCode, 200); + assertTrue("Verify success is indicated", channelResponse.success); + assertNull("Verify no error is indicated", channelResponse.errorMessage); + Map headers = HttpUtils.indexParams(channelResponse.headers); + assertEquals("Verify Content-Type header is present", headers.get("content-type").value, "application/json"); + + /* check it looks like a ChannelDetails */ + assertNotNull("Verify a result is returned", channelResponse); + JsonElement[] items = channelResponse.items(); + assertEquals("Verify a single items is returned", items.length, 1); + JsonElement channelDetails = items[0]; + assertTrue("Verify an object is returned", channelDetails.isJsonObject()); + assertTrue("Verify channelId member is present", channelDetails.getAsJsonObject().has("channelId")); + assertEquals("Verify channelId member is channelName", channelName, channelDetails.getAsJsonObject().get("channelId").getAsString()); + + /* check request has expected attributes; use last request in case of challenges preceding sending auth header */ + RawHttpRequest req = httpListener.getLastRequest(); + /* Spec: RSC19b */ + assertNotNull("Verify Authorization header present", httpListener.getRequestHeader(req.id, "Authorization")); + /* Spec: RSC19c */ + assertTrue("Verify Accept header present", httpListener.getRequestHeader(req.id, "Accept").contains("application/json")); + assertTrue("Verify Content-Type header present", httpListener.getResponseHeader(req.id, "Content-Type").contains("application/json")); + } catch(AblyException e) { + e.printStackTrace(); + fail("request_simple: Unexpected exception"); + return; + } + } + + /** + * Get channel details using the requestAsync() API + * Spec: RSC19a, RSC19d, HP1, HP3, HP4, HP5, HP8 + */ + @Test + public void request_simple_async() { + final Waiter waiter = new Waiter(); + DebugOptions opts; + try { + opts = new DebugOptions(testVars.keys[0].keyStr); + fillInOptions(opts); + final RawHttpTracker httpListener = new RawHttpTracker(); + opts.httpListener = httpListener; + AblyRest ably = new AblyRest(opts); + + ably.requestAsync(HttpConstants.Methods.GET, channelPath, null, null, null, new AsyncHttpPaginatedResponse.Callback() { + @Override + public void onResponse(AsyncHttpPaginatedResponse channelResponse) { + + /* check HttpPaginatedResponse details are present */ + waiter.assertEquals(channelResponse.statusCode, 200); + waiter.assertTrue(channelResponse.success); + waiter.assertNull(channelResponse.errorMessage); + Map headers = HttpUtils.indexParams(channelResponse.headers); + waiter.assertEquals(headers.get("content-type").value, "application/json"); + + /* check it looks like a ChannelDetails */ + /* Verify a result is returned */ + waiter.assertNotNull(channelResponse); + JsonElement[] items = channelResponse.items(); + waiter.assertEquals(items.length, 1); + JsonElement channelDetails = items[0]; + waiter.assertTrue(channelDetails.isJsonObject()); + waiter.assertTrue(channelDetails.getAsJsonObject().has("channelId")); + waiter.assertEquals(channelName, channelDetails.getAsJsonObject().get("channelId").getAsString()); + + /* check request has expected attributes */ + RawHttpRequest req = httpListener.values().iterator().next(); + /* Spec: RSC19b */ + waiter.assertNotNull(httpListener.getRequestHeader(req.id, "Authorization")); + /* Spec: RSC19c */ + waiter.assertTrue(httpListener.getRequestHeader(req.id, "Accept").contains("application/json")); + waiter.assertTrue(httpListener.getResponseHeader(req.id, "Content-Type").contains("application/json")); + waiter.resume(); + } + @Override + public void onError(ErrorInfo reason) { + waiter.fail("request_simple_async: Unexpected exception"); + waiter.resume(); + } + }); + + try { + waiter.await(15000); + } catch (TimeoutException e) { + fail("request_simple_async: Operation timed out"); + } + } catch (AblyException e) { + e.printStackTrace(); + fail("request_simple_async: Unexpected exception"); + } + } + + /** + * Get channel details using the paginatedRequest() API + * Spec: HP2 + */ + @Test + public void request_paginated() { + try { + DebugOptions opts = new DebugOptions(testVars.keys[0].keyStr); + fillInOptions(opts); + AblyRest ably = new AblyRest(opts); + + Param[] params = new Param[] { new Param("prefix", channelNamePrefix) }; + HttpPaginatedResponse channelsResponse = ably.request(HttpConstants.Methods.GET, channelsPath, params, null, null); + + /* check HttpPagninatedResponse details are present */ + assertEquals("Verify statusCode is present", channelsResponse.statusCode, 200); + assertTrue("Verify success is indicated", channelsResponse.success); + assertNull("Verify no error is indicated", channelsResponse.errorMessage); + Map headers = HttpUtils.indexParams(channelsResponse.headers); + assertEquals("Verify Content-Type header is present", headers.get("content-type").value, "application/json"); + + /* check it looks like an array of ChannelDetails */ + assertNotNull("Verify a result is returned", channelsResponse); + JsonElement[] items = channelsResponse.items(); + assertTrue("Verify at least two channels are returned", items.length >= 2); + for(int i = 0; i < items.length; i++) { + assertTrue("Verify channelId member is a matching channelName", items[i].getAsJsonObject().get("channelId").getAsString().startsWith(channelNamePrefix)); + } + + /* check that there is either no next link, or no results from it */ + if(channelsResponse.hasNext()) { + channelsResponse = channelsResponse.next(); + items = channelsResponse.items(); + assertEquals("Verify no further channels are returned", items.length, 0); + } + } catch(AblyException e) { + e.printStackTrace(); + fail("request_simple: Unexpected exception"); + return; + } + } + + /** + * Get channel details using the paginatedRequestAsync() API + * Spec: HP2 + */ + @Test + public void request_paginated_async() { + final Waiter waiter = new Waiter(); + try { + DebugOptions opts = new DebugOptions(testVars.keys[0].keyStr); + fillInOptions(opts); + AblyRest ably = new AblyRest(opts); + + Param[] params = new Param[] { new Param("prefix", channelNamePrefix) }; + ably.requestAsync(HttpConstants.Methods.GET, channelsPath, params, null, null, new AsyncHttpPaginatedResponse.Callback() { + @Override + public void onResponse(AsyncHttpPaginatedResponse channelResponse) { + + /* check HttpPaginatedResponse details are present */ + waiter.assertEquals(channelResponse.statusCode, 200); + waiter.assertTrue(channelResponse.success); + waiter.assertNull(channelResponse.errorMessage); + Map headers = HttpUtils.indexParams(channelResponse.headers); + waiter.assertEquals(headers.get("content-type").value, "application/json"); + + /* check it looks like an array of ChannelDetails */ + waiter.assertNotNull(channelResponse); + JsonElement[] items = channelResponse.items(); + waiter.assertTrue(items.length >= 2); + for(int i = 0; i < items.length; i++) { + waiter.assertTrue(items[i].getAsJsonObject().get("channelId").getAsString().startsWith(channelNamePrefix)); + } + /* check that there is either no next link, or no results from it */ + if(!channelResponse.hasNext()) { + waiter.resume(); + return; + } + channelResponse.next(new AsyncHttpPaginatedResponse.Callback() { + @Override + public void onResponse(AsyncHttpPaginatedResponse channelResponse) { + JsonElement[] items = channelResponse.items(); + assertEquals("Verify no further channels are returned", items.length, 0); + waiter.resume(); + } + @Override + public void onError(ErrorInfo reason) { + waiter.fail("request_paginated_async: Unexpected exception"); + waiter.resume(); + } + }); + } + @Override + public void onError(ErrorInfo reason) { + waiter.fail("request_paginated_async: Unexpected exception"); + waiter.resume(); + } + }); + + try { + waiter.await(15000); + } catch (TimeoutException e) { + fail("request_paginated_async: Operation timed out"); + } + } catch(AblyException e) { + e.printStackTrace(); + fail("request_paginated_async: Unexpected exception"); + return; + } + } + + /** + * Get channel details using the paginatedRequest() API with a specified limit, + * checking pagination links + * Spec: HP2 + */ + @Test + public void request_paginated_limit() { + try { + DebugOptions opts = new DebugOptions(testVars.keys[0].keyStr); + fillInOptions(opts); + AblyRest ably = new AblyRest(opts); + + Param[] params = new Param[] { new Param("prefix", channelNamePrefix), new Param("limit", "1") }; + HttpPaginatedResponse channelsResponse = ably.request(HttpConstants.Methods.GET, channelsPath, params, null, null); + + /* check HttpPagninatedResponse details are present */ + assertEquals("Verify statusCode is present", channelsResponse.statusCode, 200); + assertTrue("Verify success is indicated", channelsResponse.success); + assertNull("Verify no error is indicated", channelsResponse.errorMessage); + Map headers = HttpUtils.indexParams(channelsResponse.headers); + assertEquals("Verify Content-Type header is present", headers.get("content-type").value, "application/json"); + + /* check it looks like an array of ChannelDetails */ + assertNotNull("Verify a result is returned", channelsResponse); + JsonElement[] items = channelsResponse.items(); + assertTrue("Verify one channel is returned", items.length == 1); + for(int i = 0; i < items.length; i++) { + assertTrue("Verify channelId member is a matching channelName", items[i].getAsJsonObject().get("channelId").getAsString().startsWith(channelNamePrefix)); + } + + /* get next page */ + channelsResponse = channelsResponse.next(); + assertNotNull("Verify a result is returned", channelsResponse); + items = channelsResponse.items(); + assertTrue("Verify one channel is returned", items.length == 1); + for(int i = 0; i < items.length; i++) { + assertTrue("Verify channelId member is a matching channelName", items[i].getAsJsonObject().get("channelId").getAsString().startsWith(channelNamePrefix)); + } + + /* get first page */ + HttpPaginatedResponse firstResponse = channelsResponse.first(); + assertNotNull("Verify a result is returned", firstResponse); + items = channelsResponse.items(); + assertTrue("Verify one channel is returned", items.length == 1); + for(int i = 0; i < items.length; i++) { + assertTrue("Verify channelId member is a matching channelName", items[i].getAsJsonObject().get("channelId").getAsString().startsWith(channelNamePrefix)); + } + + /* check that there is either no next link, or no results from it */ + if(channelsResponse.hasNext()) { + channelsResponse = channelsResponse.next(); + items = channelsResponse.items(); + assertEquals("Verify no further channels are returned", items.length, 0); + } + } catch(AblyException e) { + e.printStackTrace(); + fail("request_paginated_limit: Unexpected exception"); + return; + } + } + + /** + * Get channel details using the paginatedRequestAsync() API with a specified limit, + * checking pagination links + * Spec: HP2 + */ + @Test + public void request_paginated_async_limit() { + final Waiter waiter = new Waiter(); + try { + DebugOptions opts = new DebugOptions(testVars.keys[0].keyStr); + fillInOptions(opts); + AblyRest ably = new AblyRest(opts); + + Param[] params = new Param[] { new Param("prefix", channelNamePrefix), new Param("limit", "1") }; + ably.requestAsync(HttpConstants.Methods.GET, channelsPath, params, null, null, new AsyncHttpPaginatedResponse.Callback() { + @Override + public void onResponse(AsyncHttpPaginatedResponse channelsResponse) { + + /* check HttpPagninatedResponse details are present */ + assertEquals("Verify statusCode is present", channelsResponse.statusCode, 200); + assertTrue("Verify success is indicated", channelsResponse.success); + assertNull("Verify no error is indicated", channelsResponse.errorMessage); + Map headers = HttpUtils.indexParams(channelsResponse.headers); + assertEquals("Verify Content-Type header is present", headers.get("content-type").value, "application/json"); + + /* check it looks like an array of ChannelDetails */ + waiter.assertNotNull(channelsResponse); + JsonElement[] items = channelsResponse.items(); + waiter.assertTrue(items.length == 1); + for(int i = 0; i < items.length; i++) { + waiter.assertTrue(items[i].getAsJsonObject().get("channelId").getAsString().startsWith(channelNamePrefix)); + } + channelsResponse.next(new AsyncHttpPaginatedResponse.Callback() { + @Override + public void onResponse(final AsyncHttpPaginatedResponse channelsResponse) { + /* check it looks like an array of ChannelDetails */ + waiter.assertNotNull(channelsResponse); + JsonElement[] items = channelsResponse.items(); + waiter.assertTrue(items.length == 1); + for(int i = 0; i < items.length; i++) { + waiter.assertTrue(items[i].getAsJsonObject().get("channelId").getAsString().startsWith(channelNamePrefix)); + } + + /* check that there is a first link */ + channelsResponse.first(new AsyncHttpPaginatedResponse.Callback() { + @Override + public void onResponse(AsyncHttpPaginatedResponse firstResponse) { + waiter.assertNotNull(firstResponse); + JsonElement[] items = firstResponse.items(); + waiter.assertTrue(items.length == 1); + for(int i = 0; i < items.length; i++) { + waiter.assertTrue(items[i].getAsJsonObject().get("channelId").getAsString().startsWith(channelNamePrefix)); + } + + /* check that there is either no next link, or no results from it */ + if(!channelsResponse.hasNext()) { + waiter.resume(); + return; + } + channelsResponse.next(new AsyncHttpPaginatedResponse.Callback() { + @Override + public void onResponse(AsyncHttpPaginatedResponse result) { + JsonElement[] items = result.items(); + waiter.assertEquals(items.length, 0); + waiter.resume(); + } + @Override + public void onError(ErrorInfo reason) { + waiter.fail("request_paginated_async_limit: Unexpected exception"); + waiter.resume(); + } + }); + } + @Override + public void onError(ErrorInfo reason) { + waiter.fail("request_paginated_async_limit: Unexpected exception"); + waiter.resume(); + }}); + } + @Override + public void onError(ErrorInfo reason) { + waiter.fail("request_paginated_async_limit: Unexpected exception"); + waiter.resume(); + } + }); + } + @Override + public void onError(ErrorInfo reason) { + waiter.fail("request_paginated_async_limit: Unexpected exception"); + waiter.resume(); + } + }); + + try { + waiter.await(15000); + } catch (TimeoutException e) { + fail("request_paginated_async: Operation timed out"); + } + } catch(AblyException e) { + e.printStackTrace(); + fail("request_paginated_async_limit: Unexpected exception"); + return; + } + } + + /** + * Publish a message using the request() API + * Spec: RSC19a, RSC19b + * + */ + @Test + public void request_post() { + final String messageData = "Test data (request_post)"; + DebugOptions opts; + try { + opts = new DebugOptions(testVars.keys[0].keyStr); + fillInOptions(opts); + final RawHttpTracker httpListener = new RawHttpTracker(); + opts.httpListener = httpListener; + AblyRest ably = new AblyRest(opts); + + /* publish a message */ + Message message = new Message("Test event", messageData); + HttpUtils.JsonRequestBody requestBody = new HttpUtils.JsonRequestBody(message); + HttpPaginatedResponse publishResponse = ably.request(HttpConstants.Methods.POST, channelMessagesPath, null, requestBody, null); + RawHttpRequest req = httpListener.getLastRequest(); + + /* check HttpPagninatedResponse details are present */ + assertEquals("Verify statusCode is present", publishResponse.statusCode, 201); + assertTrue("Verify success is indicated", publishResponse.success); + assertNull("Verify no error is indicated", publishResponse.errorMessage); + + /* wait to persist */ + try { Thread.sleep(1000L); } catch(InterruptedException ie) {} + + /* get the history */ + Param[] params = new Param[] { new Param("limit", "1") }; + PaginatedResult resultPage = ably.channels.get(channelName).history(params); + + /* check it looks like a result page */ + assertNotNull("Verify a result is returned", resultPage); + assertTrue("Verify an single message is returned", resultPage.items().length == 1); + assertEquals("Verify returned message was the one posted", messageData, resultPage.items()[0].data); + + /* check request has expected attributes */ + /* Spec: RSC19b */ + assertNotNull("Verify Authorization header present", httpListener.getRequestHeader(req.id, "authorization")); + /* Spec: RSC19c */ + assertTrue("Verify Accept header present", httpListener.getRequestHeader(req.id, "Accept").contains("application/json")); + assertTrue("Verify Content-Type header present", httpListener.getRequestHeader(req.id, "Content-Type").contains("application/json")); + assertTrue("Verify Content-Type header present", httpListener.getResponseHeader(req.id, "Content-Type").contains("application/json")); + } catch(AblyException e) { + e.printStackTrace(); + fail("request_post: Unexpected exception"); + return; + } + } + + /** + * Publish a message using the requestAsync() API + * Spec: RSC19a, RSC19b + */ + @Test + public void request_post_async() { + final Waiter waiter = new Waiter(); + final String messageData = "Test data (request_post_async)"; + DebugOptions opts; + try { + opts = new DebugOptions(testVars.keys[0].keyStr); + fillInOptions(opts); + final RawHttpTracker httpListener = new RawHttpTracker(); + opts.httpListener = httpListener; + AblyRest ably = new AblyRest(opts); + + /* publish a message */ + Message message = new Message("Test event", messageData); + HttpUtils.JsonRequestBody requestBody = new HttpUtils.JsonRequestBody(message); + ably.requestAsync(HttpConstants.Methods.POST, channelMessagesPath, null, requestBody, null, new AsyncHttpPaginatedResponse.Callback() { + @Override + public void onResponse(AsyncHttpPaginatedResponse publishResponse) { + + /* check HttpPaginatedResponse details are present */ + assertEquals("Verify statusCode is present", publishResponse.statusCode, 201); + assertTrue("Verify success is indicated", publishResponse.success); + assertNull("Verify no error is indicated", publishResponse.errorMessage); + + /* wait to persist */ + try { Thread.sleep(1000L); } catch(InterruptedException ie) {} + + /* get the history */ + Param[] params = new Param[] { new Param("limit", "1") }; + PaginatedResult resultPage; + try { + resultPage = setupAbly.channels.get(channelName).history(params); + + /* check it looks like a result page */ + waiter.assertNotNull(resultPage); + waiter.assertTrue(resultPage.items().length == 1); + waiter.assertEquals(messageData, resultPage.items()[0].data); + + /* check request has expected attributes */ + RawHttpRequest req = httpListener.values().iterator().next(); + /* Spec: RSC19b */ + waiter.assertNotNull(httpListener.getRequestHeader(req.id, "Authorization")); + /* Spec: RSC19c */ + waiter.assertTrue(httpListener.getRequestHeader(req.id, "Accept").contains("application/json")); + waiter.assertTrue(httpListener.getRequestHeader(req.id, "Content-Type").contains("application/json")); + waiter.assertTrue(httpListener.getResponseHeader(req.id, "Content-Type").contains("application/json")); + waiter.resume(); + } catch (AblyException e) { + e.printStackTrace(); + waiter.fail("request_post_async: Unexpected exception"); + waiter.resume(); + } + } + @Override + public void onError(ErrorInfo reason) { + waiter.fail("request_post_async: Unexpected exception"); + waiter.resume(); + } + }); + + try { + waiter.await(15000); + } catch (TimeoutException e) { + fail("request_paginated_async: Operation timed out"); + } + } catch(AblyException e) { + e.printStackTrace(); + fail("request_post_async: Unexpected exception"); + return; + } + } + + /** + * Verify 400 error responses are indicated with an HttpPaginatedResponse + * Spec: RSC19e, HP4, HP5, HP6, HP7 + */ + @Test + public void request_404() { + try { + DebugOptions opts = new DebugOptions(testVars.keys[0].keyStr); + fillInOptions(opts); + AblyRest ably = new AblyRest(opts); + HttpPaginatedResponse errorResponse = ably.request(HttpConstants.Methods.GET, "/non-existent-path", null, null, null); + + /* check HttpPaginatedResponse details are present */ + assertEquals("Verify statusCode is present", errorResponse.statusCode, 404); + assertFalse("Verify non-success is indicated", errorResponse.success); + assertNotNull("Verify error is indicated", errorResponse.errorMessage); + Map headers = HttpUtils.indexParams(errorResponse.headers); + assertEquals("Verify Content-Type header is present", headers.get("content-type").value, "application/json"); + } catch(AblyException e) { + e.printStackTrace(); + fail("request_404: Unexpected exception"); + } + } + + /** + * Verify 400 error responses are indicated with an response callback + * Spec: RSC19e, HP4, HP5, HP6, HP7 + */ + @Test + public void request_404_async() { + try { + final Waiter waiter = new Waiter(); + DebugOptions opts = new DebugOptions(testVars.keys[0].keyStr); + fillInOptions(opts); + AblyRest ably = new AblyRest(opts); + + ably.requestAsync(HttpConstants.Methods.GET, "/non-existent-path", null, null, null, new AsyncHttpPaginatedResponse.Callback() { + @Override + public void onResponse(AsyncHttpPaginatedResponse response) { + + /* check HttpPaginatedResponse details are present */ + waiter.assertEquals(response.statusCode, 404); + waiter.assertFalse(response.success); + waiter.assertNotNull(response.errorMessage); + waiter.assertTrue(response.errorCode != 0); + Map headers = HttpUtils.indexParams(response.headers); + waiter.assertEquals(headers.get("content-type").value, "application/json"); + waiter.resume(); + } + @Override + public void onError(ErrorInfo reason) { + waiter.fail("request_404_async: Expected a response callback"); + waiter.resume(); + } + }); + + try { + waiter.await(15000); + } catch (TimeoutException e) { + fail("request_404_async: Operation timed out"); + } + } catch(AblyException e) { + e.printStackTrace(); + fail("request_404_async: Unexpected exception"); + return; + } + } + + + /** + * Verify 500 error responses are indicated with an exception + * Spec: RSC19e + */ + @Test + public void request_500() { + try { + DebugOptions opts = new DebugOptions(testVars.keys[0].keyStr); + fillInOptions(opts); + opts.environment = "non.existent.env"; + AblyRest ably = new AblyRest(opts); + + ably.request(HttpConstants.Methods.GET, "/", null, null, null); + fail("request_500: Expected an exception"); + } catch(AblyException e) { + assertEquals("Verify expected status code in error response", e.errorInfo.statusCode, 500); + return; + } + } + + /** + * Verify 500 error responses are indicated with an error callback + * Spec: RSC19e + */ + @Test + public void request_500_async() { + try { + final Waiter waiter = new Waiter(); + DebugOptions opts = new DebugOptions(testVars.keys[0].keyStr); + fillInOptions(opts); + opts.environment = "non.existent.env"; + AblyRest ably = new AblyRest(opts); + + ably.requestAsync(HttpConstants.Methods.GET, "/", null, null, null, new AsyncHttpPaginatedResponse.Callback() { + @Override + public void onResponse(AsyncHttpPaginatedResponse response) { + waiter.fail("request_500_async: Expected an error"); + waiter.resume(); + } + @Override + public void onError(ErrorInfo reason) { + waiter.assertEquals(reason.statusCode, 500); + waiter.resume(); + } + }); + + try { + waiter.await(15000); + } catch (TimeoutException e) { + fail("request_500_async: Operation timed out"); + } + } catch(AblyException e) { + e.printStackTrace(); + fail("request_500_async: Unexpected exception"); + return; + } + } } diff --git a/lib/src/test/java/io/ably/lib/test/rest/RestSuite.java b/lib/src/test/java/io/ably/lib/test/rest/RestSuite.java index c59e81c2b..68d3a6237 100644 --- a/lib/src/test/java/io/ably/lib/test/rest/RestSuite.java +++ b/lib/src/test/java/io/ably/lib/test/rest/RestSuite.java @@ -13,43 +13,43 @@ @RunWith(Suite.class) @SuiteClasses({ - HttpTest.class, - HttpHeaderTest.class, - RestRequestTest.class, - RestAppStatsTest.class, - RestInitTest.class, - RestTimeTest.class, - RestAuthTest.class, - RestAuthAttributeTest.class, - RestTokenTest.class, - RestJWTTest.class, - RestCapabilityTest.class, - RestChannelTest.class, - RestChannelHistoryTest.class, - RestChannelPublishTest.class, - RestChannelBulkPublishTest.class, - RestCryptoTest.class, - RestPresenceTest.class, - RestProxyTest.class, - RestErrorTest.class, - RestPushTest.class + HttpTest.class, + HttpHeaderTest.class, + RestRequestTest.class, + RestAppStatsTest.class, + RestInitTest.class, + RestTimeTest.class, + RestAuthTest.class, + RestAuthAttributeTest.class, + RestTokenTest.class, + RestJWTTest.class, + RestCapabilityTest.class, + RestChannelTest.class, + RestChannelHistoryTest.class, + RestChannelPublishTest.class, + RestChannelBulkPublishTest.class, + RestCryptoTest.class, + RestPresenceTest.class, + RestProxyTest.class, + RestErrorTest.class, + RestPushTest.class }) public class RestSuite { - @BeforeClass - public static void setUpBeforeClass() throws Exception { - Setup.getTestVars(); - } + @BeforeClass + public static void setUpBeforeClass() throws Exception { + Setup.getTestVars(); + } - @AfterClass - public static void tearDownAfterClass() throws Exception { - Setup.clearTestVars(); - } + @AfterClass + public static void tearDownAfterClass() throws Exception { + Setup.clearTestVars(); + } - public static void main(String[] args) { - Result result = JUnitCore.runClasses(RestSuite.class); - for(Failure failure : result.getFailures()) { - System.out.println(failure.toString()); - } - } + public static void main(String[] args) { + Result result = JUnitCore.runClasses(RestSuite.class); + for(Failure failure : result.getFailures()) { + System.out.println(failure.toString()); + } + } } diff --git a/lib/src/test/java/io/ably/lib/test/rest/RestTimeTest.java b/lib/src/test/java/io/ably/lib/test/rest/RestTimeTest.java index 654a261dc..bcec86d79 100644 --- a/lib/src/test/java/io/ably/lib/test/rest/RestTimeTest.java +++ b/lib/src/test/java/io/ably/lib/test/rest/RestTimeTest.java @@ -14,77 +14,77 @@ public class RestTimeTest extends ParameterizedTest { - /** - * Verify accuracy of time (to within 60 seconds of actual time) - */ - @Test - public void time0() { - try { - ClientOptions opts = createOptions(testVars.keys[0].keyStr); - AblyRest ably = new AblyRest(opts); - long reportedTime = ably.time(); - long actualTime = System.currentTimeMillis(); - assertTrue(Math.abs(actualTime - reportedTime) < 60000); - } catch (AblyException e) { - e.printStackTrace(); - fail("time0: Unexpected exception getting time"); - } - } + /** + * Verify accuracy of time (to within 60 seconds of actual time) + */ + @Test + public void time0() { + try { + ClientOptions opts = createOptions(testVars.keys[0].keyStr); + AblyRest ably = new AblyRest(opts); + long reportedTime = ably.time(); + long actualTime = System.currentTimeMillis(); + assertTrue(Math.abs(actualTime - reportedTime) < 60000); + } catch (AblyException e) { + e.printStackTrace(); + fail("time0: Unexpected exception getting time"); + } + } - /** - * Verify time can be obtained without any valid key or token - */ - @Test - public void time1() { - try { - ClientOptions opts = createOptions("not:a.key"); - AblyRest ablyNoAuth = new AblyRest(opts); - ablyNoAuth.time(); - } catch (AblyException e) { - e.printStackTrace(); - fail("time1: Unexpected exception getting time"); - } - } + /** + * Verify time can be obtained without any valid key or token + */ + @Test + public void time1() { + try { + ClientOptions opts = createOptions("not:a.key"); + AblyRest ablyNoAuth = new AblyRest(opts); + ablyNoAuth.time(); + } catch (AblyException e) { + e.printStackTrace(); + fail("time1: Unexpected exception getting time"); + } + } - /** - * Verify time fails without valid restHost - */ - @Test - public void time2() { - try { - ClientOptions opts = createOptions("not:a.key"); - opts.environment = null; - opts.restHost = "this.restHost.does.not.exist"; - AblyRest ably = new AblyRest(opts); - ably.time(); - fail("time2: Unexpected success getting time"); - } catch (AblyException e) { - assertEquals("time2: Unexpected error code", e.errorInfo.statusCode, 500); - } - } + /** + * Verify time fails without valid restHost + */ + @Test + public void time2() { + try { + ClientOptions opts = createOptions("not:a.key"); + opts.environment = null; + opts.restHost = "this.restHost.does.not.exist"; + AblyRest ably = new AblyRest(opts); + ably.time(); + fail("time2: Unexpected success getting time"); + } catch (AblyException e) { + assertEquals("time2: Unexpected error code", e.errorInfo.statusCode, 500); + } + } - /** - * Verify accuracy with async method - */ - @Test - public void time_async() { - try { - ClientOptions opts = createOptions(testVars.keys[0].keyStr); - final AblyRest ably = new AblyRest(opts); - AsyncWaiter callback = new AsyncWaiter(); - ably.timeAsync(callback); - callback.waitFor(); + /** + * Verify accuracy with async method + */ + @Test + public void time_async() { + try { + ClientOptions opts = createOptions(testVars.keys[0].keyStr); + final AblyRest ably = new AblyRest(opts); + AsyncWaiter callback = new AsyncWaiter(); + ably.timeAsync(callback); + callback.waitFor(); - if(callback.error != null) { - fail("time_async: Unexpected error getting time"); - } else if(callback.result == null) { - fail("time_async: No time value returned"); - } else { - long actualTime = System.currentTimeMillis(); - assertTrue(Math.abs(actualTime - callback.result) < 60000); - } - } catch(AblyException e) { - fail("time_async: Unexpected exception instancing Ably REST library"); - } - } + if(callback.error != null) { + fail("time_async: Unexpected error getting time"); + } else if(callback.result == null) { + fail("time_async: No time value returned"); + } else { + long actualTime = System.currentTimeMillis(); + assertTrue(Math.abs(actualTime - callback.result) < 60000); + } + } catch(AblyException e) { + fail("time_async: Unexpected exception instancing Ably REST library"); + } + } } diff --git a/lib/src/test/java/io/ably/lib/test/rest/RestTokenTest.java b/lib/src/test/java/io/ably/lib/test/rest/RestTokenTest.java index 852d5675f..f7b2d3ab7 100644 --- a/lib/src/test/java/io/ably/lib/test/rest/RestTokenTest.java +++ b/lib/src/test/java/io/ably/lib/test/rest/RestTokenTest.java @@ -19,220 +19,220 @@ public class RestTokenTest extends ParameterizedTest { - private static String permitAll; - private static AblyRest ably; - private static long timeOffset; - - @Before - public void setUpBefore() throws Exception { - Capability capability = new Capability(); - capability.addResource("*", "*"); - permitAll = capability.toString(); - ClientOptions opts = createOptions(testVars.keys[0].keyStr); - ably = new AblyRest(opts); - long timeFromService = ably.time(); - timeOffset = timeFromService - System.currentTimeMillis(); - } - - /** - * Base requestToken case with null params - */ - @Test - public void authrequesttoken0() { - try { - long requestTime = timeOffset + System.currentTimeMillis(); - TokenDetails tokenDetails = ably.auth.requestToken(null, null); - assertNotNull("Expected token value", tokenDetails.token); - assertTrue("Unexpected issued time", (tokenDetails.issued >= (requestTime - 2000)) && (tokenDetails.issued <= (requestTime + 2000))); - assertEquals("Unexpected expires time", tokenDetails.expires, tokenDetails.issued + 60*60*1000); - assertEquals("Unexpected capability", tokenDetails.capability, permitAll); - } catch (AblyException e) { - e.printStackTrace(); - fail("authrequesttoken0: Unexpected exception"); - } - } - - /** - * Base requestToken case with non-null but empty params - */ - @Test - public void authrequesttoken1() { - try { - long requestTime = timeOffset + System.currentTimeMillis(); - TokenDetails tokenDetails = ably.auth.requestToken(new TokenParams(), null); - assertNotNull("Expected token value", tokenDetails.token); - assertTrue("Unexpected issued time", (tokenDetails.issued >= (requestTime - 1000)) && (tokenDetails.issued <= (requestTime + 1000))); - assertEquals("Unexpected expires time", tokenDetails.expires, tokenDetails.issued + 60*60*1000); - assertEquals("Unexpected capability", tokenDetails.capability, permitAll); - } catch (AblyException e) { - e.printStackTrace(); - fail("authrequesttoken1: Unexpected exception"); - } - } - - /** - * requestToken with explicit timestamp - */ - @Test - public void authtime0() { - try { - long requestTime = timeOffset + System.currentTimeMillis(); - TokenParams tokenParams = new TokenParams(); - tokenParams.timestamp = requestTime; - TokenDetails tokenDetails = ably.auth.requestToken(tokenParams, null); - assertNotNull("Expected token value", tokenDetails.token); - assertTrue("Unexpected issued time", (tokenDetails.issued >= (requestTime - 1000)) && (tokenDetails.issued <= (requestTime + 1000))); - assertEquals("Unexpected expires time", tokenDetails.expires, tokenDetails.issued + 60*60*1000); - assertEquals("Unexpected capability", tokenDetails.capability, permitAll); - } catch (AblyException e) { - e.printStackTrace(); - fail("authtime0: Unexpected exception"); - } - } - - /** - * requestToken with explicit, invalid timestamp - */ - @Test - public void authtime1() { - long requestTime = timeOffset + System.currentTimeMillis(); - TokenParams tokenParams = new TokenParams(); - tokenParams.timestamp = requestTime - 30*60*1000; - try { - ably.auth.requestToken(tokenParams, null); - fail("Expected token request rejection"); - } catch(AblyException e) { - assertEquals("Unexpected error code", e.errorInfo.code, 40104); - } - } - - /** - * requestToken with system timestamp - */ - @Test - public void authtime2() { - try { - ably.auth.clearCachedServerTime(); - long requestTime = timeOffset + System.currentTimeMillis(); - AuthOptions authOptions = new AuthOptions(); - /* Unset fields in authOptions no longer inherit from stored values, - * so we need to set up authOptions.key manually. */ - authOptions.key = ably.options.key; - authOptions.queryTime = true; - TokenDetails tokenDetails = ably.auth.requestToken(null, authOptions); - assertNotNull("Expected token value", tokenDetails.token); - assertTrue("Unexpected issued time", (tokenDetails.issued >= (requestTime - 1000)) && (tokenDetails.issued <= (requestTime + 1000))); - assertEquals("Unexpected expires time", tokenDetails.expires, tokenDetails.issued + 60*60*1000); - assertEquals("Unexpected capability", tokenDetails.capability, permitAll); - } catch (AblyException e) { - e.printStackTrace(); - fail("authtime2: Unexpected exception"); - } - } - - /** - * Base requestToken case with non-null but empty params - */ - @Test - public void authclientid0() { - try { - long requestTime = timeOffset + System.currentTimeMillis(); - TokenParams tokenParams = new TokenParams(); - tokenParams.clientId = "test client id"; - TokenDetails tokenDetails = ably.auth.requestToken(tokenParams, null); - assertNotNull("Expected token value", tokenDetails.token); - assertTrue("Unexpected issued time", (tokenDetails.issued >= (requestTime - 2000)) && (tokenDetails.issued <= (requestTime + 2000))); - assertEquals("Unexpected expires time", tokenDetails.expires, tokenDetails.issued + 60*60*1000); - assertEquals("Unexpected capability", tokenDetails.capability, permitAll); - assertEquals("Unexpected clientId", tokenDetails.clientId, tokenParams.clientId); - } catch (AblyException e) { - e.printStackTrace(); - fail("authclientid0: Unexpected exception"); - } - } - - /** - * Token generation with capability that subsets key capability - */ - @Test - public void authcapability0() { - try { - TokenParams tokenParams = new TokenParams(); - Capability capability = new Capability(); - capability.addResource("onlythischannel", "subscribe"); - String capabilityText = tokenParams.capability = capability.toString(); - TokenDetails tokenDetails = ably.auth.requestToken(tokenParams, null); - assertNotNull("Expected token value", tokenDetails.token); - assertEquals("Unexpected capability", tokenDetails.capability, capabilityText); - } catch (AblyException e) { - e.printStackTrace(); - fail("authcapability0: Unexpected exception"); - } - } - - /** - * Token generation with specified key - */ - @Test - public void authkey0() { - try { - Key key = testVars.keys[1]; - AuthOptions authOptions = new AuthOptions(); - authOptions.key = key.keyStr; - TokenDetails tokenDetails = ably.auth.requestToken(null, authOptions); - assertNotNull("Expected token value", tokenDetails.token); - assertEquals("Unexpected capability", tokenDetails.capability, key.capability); - } catch (AblyException e) { - e.printStackTrace(); - fail("authkey0: Unexpected exception"); - } - } - - /** - * Token generation with specified ttl - */ - @Test - public void authttl0() { - try { - TokenParams tokenParams = new TokenParams(); - tokenParams.ttl = 100*1000; - TokenDetails tokenDetails = ably.auth.requestToken(tokenParams, null); - assertNotNull("Expected token value", tokenDetails.token); - assertEquals("Unexpected expires", tokenDetails.expires, tokenDetails.issued + 100*1000); - } catch (AblyException e) { - e.printStackTrace(); - fail("authttl0: Unexpected exception"); - } - } - - /** - * Token generation with excessive ttl - */ - @Test - public void authttl1() { - TokenParams tokenParams = new TokenParams(); - tokenParams.ttl = 365*24*60*60*1000; - try { - ably.auth.requestToken(tokenParams, null); - fail("Expected token request rejection"); - } catch(AblyException e) { - assertEquals("Unexpected error code", e.errorInfo.code, 40003); - } - } - - /** - * Token generation with invalid ttl - */ - @Test - public void authttl2() { - TokenParams tokenParams = new TokenParams(); - tokenParams.ttl = -1; - try { - ably.auth.requestToken(tokenParams, null); - fail("Expected token request rejection"); - } catch(AblyException e) { - assertEquals("Unexpected error code", e.errorInfo.code, 40003); - } - } + private static String permitAll; + private static AblyRest ably; + private static long timeOffset; + + @Before + public void setUpBefore() throws Exception { + Capability capability = new Capability(); + capability.addResource("*", "*"); + permitAll = capability.toString(); + ClientOptions opts = createOptions(testVars.keys[0].keyStr); + ably = new AblyRest(opts); + long timeFromService = ably.time(); + timeOffset = timeFromService - System.currentTimeMillis(); + } + + /** + * Base requestToken case with null params + */ + @Test + public void authrequesttoken0() { + try { + long requestTime = timeOffset + System.currentTimeMillis(); + TokenDetails tokenDetails = ably.auth.requestToken(null, null); + assertNotNull("Expected token value", tokenDetails.token); + assertTrue("Unexpected issued time", (tokenDetails.issued >= (requestTime - 2000)) && (tokenDetails.issued <= (requestTime + 2000))); + assertEquals("Unexpected expires time", tokenDetails.expires, tokenDetails.issued + 60*60*1000); + assertEquals("Unexpected capability", tokenDetails.capability, permitAll); + } catch (AblyException e) { + e.printStackTrace(); + fail("authrequesttoken0: Unexpected exception"); + } + } + + /** + * Base requestToken case with non-null but empty params + */ + @Test + public void authrequesttoken1() { + try { + long requestTime = timeOffset + System.currentTimeMillis(); + TokenDetails tokenDetails = ably.auth.requestToken(new TokenParams(), null); + assertNotNull("Expected token value", tokenDetails.token); + assertTrue("Unexpected issued time", (tokenDetails.issued >= (requestTime - 1000)) && (tokenDetails.issued <= (requestTime + 1000))); + assertEquals("Unexpected expires time", tokenDetails.expires, tokenDetails.issued + 60*60*1000); + assertEquals("Unexpected capability", tokenDetails.capability, permitAll); + } catch (AblyException e) { + e.printStackTrace(); + fail("authrequesttoken1: Unexpected exception"); + } + } + + /** + * requestToken with explicit timestamp + */ + @Test + public void authtime0() { + try { + long requestTime = timeOffset + System.currentTimeMillis(); + TokenParams tokenParams = new TokenParams(); + tokenParams.timestamp = requestTime; + TokenDetails tokenDetails = ably.auth.requestToken(tokenParams, null); + assertNotNull("Expected token value", tokenDetails.token); + assertTrue("Unexpected issued time", (tokenDetails.issued >= (requestTime - 1000)) && (tokenDetails.issued <= (requestTime + 1000))); + assertEquals("Unexpected expires time", tokenDetails.expires, tokenDetails.issued + 60*60*1000); + assertEquals("Unexpected capability", tokenDetails.capability, permitAll); + } catch (AblyException e) { + e.printStackTrace(); + fail("authtime0: Unexpected exception"); + } + } + + /** + * requestToken with explicit, invalid timestamp + */ + @Test + public void authtime1() { + long requestTime = timeOffset + System.currentTimeMillis(); + TokenParams tokenParams = new TokenParams(); + tokenParams.timestamp = requestTime - 30*60*1000; + try { + ably.auth.requestToken(tokenParams, null); + fail("Expected token request rejection"); + } catch(AblyException e) { + assertEquals("Unexpected error code", e.errorInfo.code, 40104); + } + } + + /** + * requestToken with system timestamp + */ + @Test + public void authtime2() { + try { + ably.auth.clearCachedServerTime(); + long requestTime = timeOffset + System.currentTimeMillis(); + AuthOptions authOptions = new AuthOptions(); + /* Unset fields in authOptions no longer inherit from stored values, + * so we need to set up authOptions.key manually. */ + authOptions.key = ably.options.key; + authOptions.queryTime = true; + TokenDetails tokenDetails = ably.auth.requestToken(null, authOptions); + assertNotNull("Expected token value", tokenDetails.token); + assertTrue("Unexpected issued time", (tokenDetails.issued >= (requestTime - 1000)) && (tokenDetails.issued <= (requestTime + 1000))); + assertEquals("Unexpected expires time", tokenDetails.expires, tokenDetails.issued + 60*60*1000); + assertEquals("Unexpected capability", tokenDetails.capability, permitAll); + } catch (AblyException e) { + e.printStackTrace(); + fail("authtime2: Unexpected exception"); + } + } + + /** + * Base requestToken case with non-null but empty params + */ + @Test + public void authclientid0() { + try { + long requestTime = timeOffset + System.currentTimeMillis(); + TokenParams tokenParams = new TokenParams(); + tokenParams.clientId = "test client id"; + TokenDetails tokenDetails = ably.auth.requestToken(tokenParams, null); + assertNotNull("Expected token value", tokenDetails.token); + assertTrue("Unexpected issued time", (tokenDetails.issued >= (requestTime - 2000)) && (tokenDetails.issued <= (requestTime + 2000))); + assertEquals("Unexpected expires time", tokenDetails.expires, tokenDetails.issued + 60*60*1000); + assertEquals("Unexpected capability", tokenDetails.capability, permitAll); + assertEquals("Unexpected clientId", tokenDetails.clientId, tokenParams.clientId); + } catch (AblyException e) { + e.printStackTrace(); + fail("authclientid0: Unexpected exception"); + } + } + + /** + * Token generation with capability that subsets key capability + */ + @Test + public void authcapability0() { + try { + TokenParams tokenParams = new TokenParams(); + Capability capability = new Capability(); + capability.addResource("onlythischannel", "subscribe"); + String capabilityText = tokenParams.capability = capability.toString(); + TokenDetails tokenDetails = ably.auth.requestToken(tokenParams, null); + assertNotNull("Expected token value", tokenDetails.token); + assertEquals("Unexpected capability", tokenDetails.capability, capabilityText); + } catch (AblyException e) { + e.printStackTrace(); + fail("authcapability0: Unexpected exception"); + } + } + + /** + * Token generation with specified key + */ + @Test + public void authkey0() { + try { + Key key = testVars.keys[1]; + AuthOptions authOptions = new AuthOptions(); + authOptions.key = key.keyStr; + TokenDetails tokenDetails = ably.auth.requestToken(null, authOptions); + assertNotNull("Expected token value", tokenDetails.token); + assertEquals("Unexpected capability", tokenDetails.capability, key.capability); + } catch (AblyException e) { + e.printStackTrace(); + fail("authkey0: Unexpected exception"); + } + } + + /** + * Token generation with specified ttl + */ + @Test + public void authttl0() { + try { + TokenParams tokenParams = new TokenParams(); + tokenParams.ttl = 100*1000; + TokenDetails tokenDetails = ably.auth.requestToken(tokenParams, null); + assertNotNull("Expected token value", tokenDetails.token); + assertEquals("Unexpected expires", tokenDetails.expires, tokenDetails.issued + 100*1000); + } catch (AblyException e) { + e.printStackTrace(); + fail("authttl0: Unexpected exception"); + } + } + + /** + * Token generation with excessive ttl + */ + @Test + public void authttl1() { + TokenParams tokenParams = new TokenParams(); + tokenParams.ttl = 365*24*60*60*1000; + try { + ably.auth.requestToken(tokenParams, null); + fail("Expected token request rejection"); + } catch(AblyException e) { + assertEquals("Unexpected error code", e.errorInfo.code, 40003); + } + } + + /** + * Token generation with invalid ttl + */ + @Test + public void authttl2() { + TokenParams tokenParams = new TokenParams(); + tokenParams.ttl = -1; + try { + ably.auth.requestToken(tokenParams, null); + fail("Expected token request rejection"); + } catch(AblyException e) { + assertEquals("Unexpected error code", e.errorInfo.code, 40003); + } + } } diff --git a/lib/src/test/java/io/ably/lib/test/util/MockWebsocketFactory.java b/lib/src/test/java/io/ably/lib/test/util/MockWebsocketFactory.java index 05073e437..0f59a2196 100644 --- a/lib/src/test/java/io/ably/lib/test/util/MockWebsocketFactory.java +++ b/lib/src/test/java/io/ably/lib/test/util/MockWebsocketFactory.java @@ -12,150 +12,150 @@ */ public class MockWebsocketFactory implements ITransport.Factory { - enum SendBehaviour { - allow, - block, - fail - } - enum ConnectBehaviour { - allow, - fail - } - - public interface MessageFilter { - boolean matches(ProtocolMessage message); - } - - public interface HostFilter { - boolean matches(String hostname); - } - - public interface HostTransform { - String transformHost(String givenHost); - } - - SendBehaviour sendBehaviour = SendBehaviour.allow; - ConnectBehaviour connectBehaviour = ConnectBehaviour.allow; - MessageFilter messageFilter = null; - HostFilter hostFilter = null; - HostTransform hostTransform = null; - - public ITransport lastCreatedTransport = null; - - public static class TransformParams extends ITransport.TransportParams { - private HostTransform hostTransform; - - TransformParams(ITransport.TransportParams src, HostTransform hostTransform) { - super(src.getClientOptions()); - this.hostTransform = hostTransform; - this.host = hostTransform.transformHost(src.getHost()); - this.port = src.getPort(); - } - } - - @Override - public ITransport getTransport(final ITransport.TransportParams transportParams, ConnectionManager connectionManager) { - ITransport.TransportParams transformParams = transportParams; - if(hostTransform != null) { - transformParams = new TransformParams(transportParams, hostTransform); - } - lastCreatedTransport = new MockWebsocketTransport(transportParams, transformParams, connectionManager); - return lastCreatedTransport; - } - - public void blockSend(MessageFilter filter) { - messageFilter = filter; - sendBehaviour = SendBehaviour.block; - } - public void blockSend() { blockSend(null); } - - public void allowSend(MessageFilter filter) { - messageFilter = filter; - sendBehaviour = SendBehaviour.allow; - } - public void allowSend() { allowSend(null);} - - public void failSend(MessageFilter filter) { - messageFilter = filter; - sendBehaviour = SendBehaviour.fail; - } - public void failSend() { failSend(null); } - - public void setMessageFilter(MessageFilter filter) { - messageFilter = filter; - } - - public void failConnect(HostFilter filter) { - hostFilter = filter; - connectBehaviour = ConnectBehaviour.fail; - } - public void failConnect() { failConnect(null); } - - public void setHostTransform(HostTransform transform) { - hostTransform = transform; - } - - /* - * Special transport class that allows blocking send() and other operations - */ - private class MockWebsocketTransport extends WebSocketTransport { - private final TransportParams givenTransportParams; - private final TransportParams transformedTransportParams; - - private MockWebsocketTransport(TransportParams givenTransportParams, TransportParams transformedTransportParams, ConnectionManager connectionManager) { - super(transformedTransportParams, connectionManager); - this.givenTransportParams = givenTransportParams; - this.transformedTransportParams = transformedTransportParams; - } - - @Override - public void send(ProtocolMessage msg) throws AblyException { - switch (sendBehaviour) { - case allow: - if (messageFilter == null || messageFilter.matches(msg)) { - super.send(msg); - } - break; - case block: - if (messageFilter == null || messageFilter.matches(msg)) { - /* do nothing */ - } else { - super.send(msg); - } - break; - case fail: - if (messageFilter == null || messageFilter.matches(msg)) { - throw AblyException.fromErrorInfo(new ErrorInfo("Mock", 40000)); - } else { - super.send(msg); - } - break; - } - } - - @Override - public void connect(ConnectListener connectListener) { - String host = givenTransportParams.getHost(); - switch (connectBehaviour) { - case allow: - if (hostFilter == null || hostFilter.matches(host)) { - System.out.println("MockWebsocketTransport: allowing " + host); - super.connect(connectListener); - } else { - System.out.println("MockWebsocketTransport: disallowing " + host); - connectListener.onTransportUnavailable(this, new ErrorInfo("MockWebsocketTransport: connection disallowed by hostFilter", 500, 50000)); - } - break; - case fail: - if (hostFilter == null || hostFilter.matches(host)) { - System.out.println("MockWebsocketTransport: failing " + host); - connectListener.onTransportUnavailable(this, new ErrorInfo("MockWebsocketTransport: connection failed by hostFilter", 500, 50000)); - } else { - System.out.println("MockWebsocketTransport: not failing " + host); - super.connect(connectListener); - } - break; - } - } - } + enum SendBehaviour { + allow, + block, + fail + } + enum ConnectBehaviour { + allow, + fail + } + + public interface MessageFilter { + boolean matches(ProtocolMessage message); + } + + public interface HostFilter { + boolean matches(String hostname); + } + + public interface HostTransform { + String transformHost(String givenHost); + } + + SendBehaviour sendBehaviour = SendBehaviour.allow; + ConnectBehaviour connectBehaviour = ConnectBehaviour.allow; + MessageFilter messageFilter = null; + HostFilter hostFilter = null; + HostTransform hostTransform = null; + + public ITransport lastCreatedTransport = null; + + public static class TransformParams extends ITransport.TransportParams { + private HostTransform hostTransform; + + TransformParams(ITransport.TransportParams src, HostTransform hostTransform) { + super(src.getClientOptions()); + this.hostTransform = hostTransform; + this.host = hostTransform.transformHost(src.getHost()); + this.port = src.getPort(); + } + } + + @Override + public ITransport getTransport(final ITransport.TransportParams transportParams, ConnectionManager connectionManager) { + ITransport.TransportParams transformParams = transportParams; + if(hostTransform != null) { + transformParams = new TransformParams(transportParams, hostTransform); + } + lastCreatedTransport = new MockWebsocketTransport(transportParams, transformParams, connectionManager); + return lastCreatedTransport; + } + + public void blockSend(MessageFilter filter) { + messageFilter = filter; + sendBehaviour = SendBehaviour.block; + } + public void blockSend() { blockSend(null); } + + public void allowSend(MessageFilter filter) { + messageFilter = filter; + sendBehaviour = SendBehaviour.allow; + } + public void allowSend() { allowSend(null);} + + public void failSend(MessageFilter filter) { + messageFilter = filter; + sendBehaviour = SendBehaviour.fail; + } + public void failSend() { failSend(null); } + + public void setMessageFilter(MessageFilter filter) { + messageFilter = filter; + } + + public void failConnect(HostFilter filter) { + hostFilter = filter; + connectBehaviour = ConnectBehaviour.fail; + } + public void failConnect() { failConnect(null); } + + public void setHostTransform(HostTransform transform) { + hostTransform = transform; + } + + /* + * Special transport class that allows blocking send() and other operations + */ + private class MockWebsocketTransport extends WebSocketTransport { + private final TransportParams givenTransportParams; + private final TransportParams transformedTransportParams; + + private MockWebsocketTransport(TransportParams givenTransportParams, TransportParams transformedTransportParams, ConnectionManager connectionManager) { + super(transformedTransportParams, connectionManager); + this.givenTransportParams = givenTransportParams; + this.transformedTransportParams = transformedTransportParams; + } + + @Override + public void send(ProtocolMessage msg) throws AblyException { + switch (sendBehaviour) { + case allow: + if (messageFilter == null || messageFilter.matches(msg)) { + super.send(msg); + } + break; + case block: + if (messageFilter == null || messageFilter.matches(msg)) { + /* do nothing */ + } else { + super.send(msg); + } + break; + case fail: + if (messageFilter == null || messageFilter.matches(msg)) { + throw AblyException.fromErrorInfo(new ErrorInfo("Mock", 40000)); + } else { + super.send(msg); + } + break; + } + } + + @Override + public void connect(ConnectListener connectListener) { + String host = givenTransportParams.getHost(); + switch (connectBehaviour) { + case allow: + if (hostFilter == null || hostFilter.matches(host)) { + System.out.println("MockWebsocketTransport: allowing " + host); + super.connect(connectListener); + } else { + System.out.println("MockWebsocketTransport: disallowing " + host); + connectListener.onTransportUnavailable(this, new ErrorInfo("MockWebsocketTransport: connection disallowed by hostFilter", 500, 50000)); + } + break; + case fail: + if (hostFilter == null || hostFilter.matches(host)) { + System.out.println("MockWebsocketTransport: failing " + host); + connectListener.onTransportUnavailable(this, new ErrorInfo("MockWebsocketTransport: connection failed by hostFilter", 500, 50000)); + } else { + System.out.println("MockWebsocketTransport: not failing " + host); + super.connect(connectListener); + } + break; + } + } + } } diff --git a/lib/src/test/java/io/ably/lib/test/util/StatsWriter.java b/lib/src/test/java/io/ably/lib/test/util/StatsWriter.java index 4ace9ef4c..1040d6c87 100644 --- a/lib/src/test/java/io/ably/lib/test/util/StatsWriter.java +++ b/lib/src/test/java/io/ably/lib/test/util/StatsWriter.java @@ -7,7 +7,7 @@ import io.ably.lib.util.Serialisation; public class StatsWriter { - public static HttpCore.RequestBody asJsonRequest(Stats[] stats) throws AblyException { - return new HttpUtils.JsonRequestBody(Serialisation.gson.toJson(stats)); - } + public static HttpCore.RequestBody asJsonRequest(Stats[] stats) throws AblyException { + return new HttpUtils.JsonRequestBody(Serialisation.gson.toJson(stats)); + } } diff --git a/lib/src/test/java/io/ably/lib/test/util/StatusHandler.java b/lib/src/test/java/io/ably/lib/test/util/StatusHandler.java index 86b8ad908..fb84338a6 100644 --- a/lib/src/test/java/io/ably/lib/test/util/StatusHandler.java +++ b/lib/src/test/java/io/ably/lib/test/util/StatusHandler.java @@ -14,40 +14,40 @@ */ public class StatusHandler extends RouterNanoHTTPD.DefaultStreamHandler { - @Override - public String getMimeType() { - return "application/json"; - } - - @Override - public NanoHTTPD.Response.IStatus getStatus() { - throw new IllegalStateException("this method should not be called in a status handler"); - } - - @Override - public NanoHTTPD.Response get(RouterNanoHTTPD.UriResource uriResource, Map urlParams, NanoHTTPD.IHTTPSession session) { - String codeParam = urlParams.get("code"); - int code = Integer.parseInt(codeParam); - - return newFixedLengthResponse(newStatus(code, ""), getMimeType(), "{code:" + codeParam + "}"); - } - - @Override - public InputStream getData() { - throw new IllegalStateException("this method should not be called in a status handler"); - } - - private static NanoHTTPD.Response.IStatus newStatus(final int status, final String description) { - return new NanoHTTPD.Response.IStatus() { - @Override - public String getDescription() { - return "" + status + " " + description; - } - - @Override - public int getRequestStatus() { - return status; - } - }; - } + @Override + public String getMimeType() { + return "application/json"; + } + + @Override + public NanoHTTPD.Response.IStatus getStatus() { + throw new IllegalStateException("this method should not be called in a status handler"); + } + + @Override + public NanoHTTPD.Response get(RouterNanoHTTPD.UriResource uriResource, Map urlParams, NanoHTTPD.IHTTPSession session) { + String codeParam = urlParams.get("code"); + int code = Integer.parseInt(codeParam); + + return newFixedLengthResponse(newStatus(code, ""), getMimeType(), "{code:" + codeParam + "}"); + } + + @Override + public InputStream getData() { + throw new IllegalStateException("this method should not be called in a status handler"); + } + + private static NanoHTTPD.Response.IStatus newStatus(final int status, final String description) { + return new NanoHTTPD.Response.IStatus() { + @Override + public String getDescription() { + return "" + status + " " + description; + } + + @Override + public int getRequestStatus() { + return status; + } + }; + } } diff --git a/lib/src/test/java/io/ably/lib/test/util/TestCases.java b/lib/src/test/java/io/ably/lib/test/util/TestCases.java index 4d898611b..41b45a2ae 100644 --- a/lib/src/test/java/io/ably/lib/test/util/TestCases.java +++ b/lib/src/test/java/io/ably/lib/test/util/TestCases.java @@ -11,65 +11,65 @@ import static org.junit.Assert.assertNotNull; public class TestCases { - final ArrayList testCases; + final ArrayList testCases; - public TestCases() { - testCases = new ArrayList(); - } + public TestCases() { + testCases = new ArrayList(); + } - public void add(Base testCase) { - testCases.add(testCase); - } + public void add(Base testCase) { + testCases.add(testCase); + } - public void run() throws Exception { - for (final Base testCase : testCases) { - try { - Log.i("TestCase", "starting: " + testCase.name); - Helpers.expectedError(new Helpers.AblyFunction() { - @Override - public Void apply(Void aVoid) throws AblyException { - try { - testCase.run(); - } catch (Exception e) { - if (e instanceof AblyException) { - throw (AblyException) e; - } - throw new RuntimeException(e); - } - return null; - } - }, testCase.expectedError, testCase.expectedStatusCode, testCase.expectedCode); - } catch (Exception e) { - throw new Exception("in test case \"" + testCase.name + "\"", e); - } catch (Error e) { - throw new Error("in test case \"" + testCase.name + "\"", e); - } finally { - Log.i("TestCase", "ended: " + testCase.name); - } - } - } + public void run() throws Exception { + for (final Base testCase : testCases) { + try { + Log.i("TestCase", "starting: " + testCase.name); + Helpers.expectedError(new Helpers.AblyFunction() { + @Override + public Void apply(Void aVoid) throws AblyException { + try { + testCase.run(); + } catch (Exception e) { + if (e instanceof AblyException) { + throw (AblyException) e; + } + throw new RuntimeException(e); + } + return null; + } + }, testCase.expectedError, testCase.expectedStatusCode, testCase.expectedCode); + } catch (Exception e) { + throw new Exception("in test case \"" + testCase.name + "\"", e); + } catch (Error e) { + throw new Error("in test case \"" + testCase.name + "\"", e); + } finally { + Log.i("TestCase", "ended: " + testCase.name); + } + } + } - public static abstract class Base { - final protected String name; - final protected String expectedError; - final protected int expectedCode; - final protected int expectedStatusCode; + public static abstract class Base { + final protected String name; + final protected String expectedError; + final protected int expectedCode; + final protected int expectedStatusCode; - public Base(String name, String expectedError) { - this(name, expectedError, 0); - } + public Base(String name, String expectedError) { + this(name, expectedError, 0); + } - public Base(String name, String expectedError, int expectedStatusCode) { - this(name, expectedError, expectedStatusCode, 0); - } + public Base(String name, String expectedError, int expectedStatusCode) { + this(name, expectedError, expectedStatusCode, 0); + } - public Base(String name, String expectedError, int expectedStatusCode, int expectedCode) { - this.name = name; - this.expectedError = expectedError; - this.expectedStatusCode = expectedStatusCode; - this.expectedCode = expectedCode; - } + public Base(String name, String expectedError, int expectedStatusCode, int expectedCode) { + this.name = name; + this.expectedError = expectedError; + this.expectedStatusCode = expectedStatusCode; + this.expectedCode = expectedCode; + } - public abstract void run() throws Exception; - } + public abstract void run() throws Exception; + } } diff --git a/lib/src/test/java/io/ably/lib/test/util/TimeHandler.java b/lib/src/test/java/io/ably/lib/test/util/TimeHandler.java index 53ffc075d..d66e82bab 100644 --- a/lib/src/test/java/io/ably/lib/test/util/TimeHandler.java +++ b/lib/src/test/java/io/ably/lib/test/util/TimeHandler.java @@ -9,29 +9,29 @@ import static fi.iki.elonen.NanoHTTPD.newFixedLengthResponse; public class TimeHandler extends RouterNanoHTTPD.DefaultStreamHandler { - @Override - public String getMimeType() { - return "application/json"; - } - - @Override - public NanoHTTPD.Response.IStatus getStatus() { - throw new IllegalStateException("this method should not be called in a time handler"); - } - - @Override - public InputStream getData() { - throw new IllegalStateException("this method should not be called in a time handler"); - } - - @Override - public NanoHTTPD.Response get(RouterNanoHTTPD.UriResource uriResource, Map urlParams, NanoHTTPD.IHTTPSession session) { - try { - long delay = 5000L; - Thread.sleep(delay); - } catch(InterruptedException ie) {} - - return newFixedLengthResponse(NanoHTTPD.Response.Status.OK, getMimeType(), "[" + String.valueOf(System.currentTimeMillis()) + "]"); - } + @Override + public String getMimeType() { + return "application/json"; + } + + @Override + public NanoHTTPD.Response.IStatus getStatus() { + throw new IllegalStateException("this method should not be called in a time handler"); + } + + @Override + public InputStream getData() { + throw new IllegalStateException("this method should not be called in a time handler"); + } + + @Override + public NanoHTTPD.Response get(RouterNanoHTTPD.UriResource uriResource, Map urlParams, NanoHTTPD.IHTTPSession session) { + try { + long delay = 5000L; + Thread.sleep(delay); + } catch(InterruptedException ie) {} + + return newFixedLengthResponse(NanoHTTPD.Response.Status.OK, getMimeType(), "[" + String.valueOf(System.currentTimeMillis()) + "]"); + } } diff --git a/lib/src/test/java/io/ably/lib/test/util/TokenServer.java b/lib/src/test/java/io/ably/lib/test/util/TokenServer.java index e7a2a9a74..6aa2fac74 100644 --- a/lib/src/test/java/io/ably/lib/test/util/TokenServer.java +++ b/lib/src/test/java/io/ably/lib/test/util/TokenServer.java @@ -45,124 +45,124 @@ public class TokenServer extends NanoHTTPD { - public TokenServer(AblyRest ably, int port) { - super(port); - this.ably = ably; - } - - @Override - public Response serve(IHTTPSession session) { - Method method = session.getMethod(); - String target = session.getUri(); - Map headers = session.getHeaders(); - - if (method.equals(Method.POST)) { - try { - session.parseBody(new HashMap()); - } catch (IOException | ResponseException e) { - return error2Response(new ErrorInfo("Bad POST token request", 400, 40000)); - } - } - - Map params = session.getParms(); - - if ((method.equals(Method.POST) && target.equals("/post-token-request")) || - (method.equals(Method.GET) && target.equals("/get-token-request"))) { - TokenParams tokenParams = params2TokenParams(params); - try { - TokenRequest tokenRequest = ably.auth.createTokenRequest(tokenParams, null); - return json2Response(tokenRequest.asJsonElement()); - } catch (AblyException e) { - e.printStackTrace(); - return error2Response(e.errorInfo); - } - } - - if (!method.equals(Method.GET)) { - return newFixedLengthResponse(Response.Status.METHOD_NOT_ALLOWED, MIME_PLAINTEXT, "Method not supported"); - } - - if(target.equals("/get-token")) { - TokenParams tokenParams = params2TokenParams(params); - try { - TokenDetails token = ably.auth.requestToken(tokenParams, null); - return json2Response(token); - } catch (AblyException e) { - e.printStackTrace(); - return error2Response(e.errorInfo); - } - } - else if(target.equals("/404")) { - return error2Response(new ErrorInfo("Not found", 404, 0)); - } - else if(target.equals("/wait")) { - long delay = 30000; - try { - String strDelayMillis = params.get("delay"); - if(strDelayMillis != null) { - delay = Long.valueOf(strDelayMillis); - } - } catch(NumberFormatException nfe) {} - try { - Thread.sleep(delay); - } catch(InterruptedException ie) {} - return newFixedLengthResponse(Response.Status.NO_CONTENT, MIME_JSON, ""); - } - else { - return newFixedLengthResponse(Response.Status.NOT_FOUND, MIME_PLAINTEXT, "Unexpected path: " + target); - } - } - - private static Response params2ErrorResponse(Map params, Response.Status status) { - StringBuilder builder = new StringBuilder(); - for(Entry entry : params.entrySet()) { - if(builder.length() != 0) builder.append('&'); - builder.append(entry.getKey()).append('=').append(entry.getValue()); - } - return error2Response(new ErrorInfo(builder.toString(), status.getRequestStatus(), 0)); - } - - private static TokenParams params2TokenParams(Map params) { - TokenParams tokenParams = new TokenParams(); - if(params.containsKey("client_id")) - tokenParams.clientId = params.get("client_id"); - if(params.containsKey("clientId")) - tokenParams.clientId = params.get("clientId"); - if(params.containsKey("timestamp")) - tokenParams.timestamp = Long.valueOf(params.get("timestamp")); - if(params.containsKey("ttl")) - tokenParams.ttl = Long.valueOf(params.get("ttl")); - if(params.containsKey("capability")) - tokenParams.capability = params.get("capability"); - return tokenParams; - } - - private static Response json2Response(Object obj) { - return newFixedLengthResponse(Response.Status.OK, MIME_JSON, Serialisation.gson.toJson(obj)); - } - - private static Response.Status getStatus(int statusCode) { - switch(statusCode) { - case 200: - return Response.Status.OK; - case 400: - return Response.Status.BAD_REQUEST; - case 401: - return Response.Status.UNAUTHORIZED; - case 404: - return Response.Status.NOT_FOUND; - case 500: - default: - return Response.Status.INTERNAL_ERROR; - } - } - - private static Response error2Response(ErrorInfo errorInfo) { - ErrorResponse err = new ErrorResponse(); - err.error = errorInfo; - return newFixedLengthResponse(getStatus(errorInfo.statusCode), MIME_JSON, Serialisation.gson.toJson(err)); - } - - private final AblyRest ably; - private static final String MIME_JSON = "application/json"; + public TokenServer(AblyRest ably, int port) { + super(port); + this.ably = ably; + } + + @Override + public Response serve(IHTTPSession session) { + Method method = session.getMethod(); + String target = session.getUri(); + Map headers = session.getHeaders(); + + if (method.equals(Method.POST)) { + try { + session.parseBody(new HashMap()); + } catch (IOException | ResponseException e) { + return error2Response(new ErrorInfo("Bad POST token request", 400, 40000)); + } + } + + Map params = session.getParms(); + + if ((method.equals(Method.POST) && target.equals("/post-token-request")) || + (method.equals(Method.GET) && target.equals("/get-token-request"))) { + TokenParams tokenParams = params2TokenParams(params); + try { + TokenRequest tokenRequest = ably.auth.createTokenRequest(tokenParams, null); + return json2Response(tokenRequest.asJsonElement()); + } catch (AblyException e) { + e.printStackTrace(); + return error2Response(e.errorInfo); + } + } + + if (!method.equals(Method.GET)) { + return newFixedLengthResponse(Response.Status.METHOD_NOT_ALLOWED, MIME_PLAINTEXT, "Method not supported"); + } + + if(target.equals("/get-token")) { + TokenParams tokenParams = params2TokenParams(params); + try { + TokenDetails token = ably.auth.requestToken(tokenParams, null); + return json2Response(token); + } catch (AblyException e) { + e.printStackTrace(); + return error2Response(e.errorInfo); + } + } + else if(target.equals("/404")) { + return error2Response(new ErrorInfo("Not found", 404, 0)); + } + else if(target.equals("/wait")) { + long delay = 30000; + try { + String strDelayMillis = params.get("delay"); + if(strDelayMillis != null) { + delay = Long.valueOf(strDelayMillis); + } + } catch(NumberFormatException nfe) {} + try { + Thread.sleep(delay); + } catch(InterruptedException ie) {} + return newFixedLengthResponse(Response.Status.NO_CONTENT, MIME_JSON, ""); + } + else { + return newFixedLengthResponse(Response.Status.NOT_FOUND, MIME_PLAINTEXT, "Unexpected path: " + target); + } + } + + private static Response params2ErrorResponse(Map params, Response.Status status) { + StringBuilder builder = new StringBuilder(); + for(Entry entry : params.entrySet()) { + if(builder.length() != 0) builder.append('&'); + builder.append(entry.getKey()).append('=').append(entry.getValue()); + } + return error2Response(new ErrorInfo(builder.toString(), status.getRequestStatus(), 0)); + } + + private static TokenParams params2TokenParams(Map params) { + TokenParams tokenParams = new TokenParams(); + if(params.containsKey("client_id")) + tokenParams.clientId = params.get("client_id"); + if(params.containsKey("clientId")) + tokenParams.clientId = params.get("clientId"); + if(params.containsKey("timestamp")) + tokenParams.timestamp = Long.valueOf(params.get("timestamp")); + if(params.containsKey("ttl")) + tokenParams.ttl = Long.valueOf(params.get("ttl")); + if(params.containsKey("capability")) + tokenParams.capability = params.get("capability"); + return tokenParams; + } + + private static Response json2Response(Object obj) { + return newFixedLengthResponse(Response.Status.OK, MIME_JSON, Serialisation.gson.toJson(obj)); + } + + private static Response.Status getStatus(int statusCode) { + switch(statusCode) { + case 200: + return Response.Status.OK; + case 400: + return Response.Status.BAD_REQUEST; + case 401: + return Response.Status.UNAUTHORIZED; + case 404: + return Response.Status.NOT_FOUND; + case 500: + default: + return Response.Status.INTERNAL_ERROR; + } + } + + private static Response error2Response(ErrorInfo errorInfo) { + ErrorResponse err = new ErrorResponse(); + err.error = errorInfo; + return newFixedLengthResponse(getStatus(errorInfo.statusCode), MIME_JSON, Serialisation.gson.toJson(err)); + } + + private final AblyRest ably; + private static final String MIME_JSON = "application/json"; } diff --git a/lib/src/test/java/io/ably/lib/transport/DefaultsTest.java b/lib/src/test/java/io/ably/lib/transport/DefaultsTest.java index f33623636..7db44f8e9 100644 --- a/lib/src/test/java/io/ably/lib/transport/DefaultsTest.java +++ b/lib/src/test/java/io/ably/lib/transport/DefaultsTest.java @@ -5,8 +5,8 @@ import static org.junit.Assert.assertEquals; public class DefaultsTest { - @Test - public void versions() { - assertEquals("1.2", Defaults.ABLY_VERSION); - } + @Test + public void versions() { + assertEquals("1.2", Defaults.ABLY_VERSION); + } } diff --git a/lib/src/test/java/io/ably/lib/types/MessageExtrasTest.java b/lib/src/test/java/io/ably/lib/types/MessageExtrasTest.java index df5328e67..5472d9f61 100644 --- a/lib/src/test/java/io/ably/lib/types/MessageExtrasTest.java +++ b/lib/src/test/java/io/ably/lib/types/MessageExtrasTest.java @@ -15,51 +15,51 @@ import static org.junit.Assert.assertNull; public class MessageExtrasTest { - /** - * Construct an instance from a JSON source and validate that the - * serialised JSON is the same. - */ - @Test - public void raw() { - final JsonObject objectA = new JsonObject(); - objectA.addProperty("someKey", "someValue"); + /** + * Construct an instance from a JSON source and validate that the + * serialised JSON is the same. + */ + @Test + public void raw() { + final JsonObject objectA = new JsonObject(); + objectA.addProperty("someKey", "someValue"); - final JsonObject objectB = new JsonObject(); - objectB.addProperty("someOtherKey", "someValue"); + final JsonObject objectB = new JsonObject(); + objectB.addProperty("someOtherKey", "someValue"); - final MessageExtras messageExtras = new MessageExtras(objectA); - assertNull(messageExtras.getDelta()); + final MessageExtras messageExtras = new MessageExtras(objectA); + assertNull(messageExtras.getDelta()); - final MessageExtras.Serializer serializer = new MessageExtras.Serializer(); - final JsonElement serialised = serializer.serialize(messageExtras, null, null); + final MessageExtras.Serializer serializer = new MessageExtras.Serializer(); + final JsonElement serialised = serializer.serialize(messageExtras, null, null); - assertEquals(objectA, serialised); - assertNotEquals(objectB, serialised); - assertNotEquals(objectB, objectA); - } + assertEquals(objectA, serialised); + assertNotEquals(objectB, serialised); + assertNotEquals(objectB, objectA); + } - @Test - public void rawViaMessagePack() throws IOException { - final JsonObject object = new JsonObject(); - object.addProperty("foo", "bar"); - object.addProperty("cliché", "cache"); - final MessageExtras messageExtras = new MessageExtras(object); + @Test + public void rawViaMessagePack() throws IOException { + final JsonObject object = new JsonObject(); + object.addProperty("foo", "bar"); + object.addProperty("cliché", "cache"); + final MessageExtras messageExtras = new MessageExtras(object); - // Encode to MessagePack - final ByteArrayOutputStream out = new ByteArrayOutputStream(); - final MessagePacker packer = Serialisation.msgpackPackerConfig.newPacker(out); - messageExtras.write(packer); - packer.flush(); + // Encode to MessagePack + final ByteArrayOutputStream out = new ByteArrayOutputStream(); + final MessagePacker packer = Serialisation.msgpackPackerConfig.newPacker(out); + messageExtras.write(packer); + packer.flush(); - // Decode from MessagePack - MessageUnpacker unpacker = Serialisation.msgpackUnpackerConfig.newUnpacker(out.toByteArray()); - final MessageExtras unpacked = MessageExtras.read(unpacker); + // Decode from MessagePack + MessageUnpacker unpacker = Serialisation.msgpackUnpackerConfig.newUnpacker(out.toByteArray()); + final MessageExtras unpacked = MessageExtras.read(unpacker); - assertEquals(messageExtras, unpacked); - } + assertEquals(messageExtras, unpacked); + } - @Test(expected = NullPointerException.class) - public void rawNullArgument() { - new MessageExtras((JsonObject)null); - } + @Test(expected = NullPointerException.class) + public void rawNullArgument() { + new MessageExtras((JsonObject)null); + } } diff --git a/lib/src/test/java/io/ably/lib/util/CryptoMessageTest.java b/lib/src/test/java/io/ably/lib/util/CryptoMessageTest.java index 205996380..0412bdaeb 100644 --- a/lib/src/test/java/io/ably/lib/util/CryptoMessageTest.java +++ b/lib/src/test/java/io/ably/lib/util/CryptoMessageTest.java @@ -24,130 +24,130 @@ @RunWith(Parameterized.class) public class CryptoMessageTest { - public enum FixtureSet { - AES128(16), - AES256(32); - - public final byte[] key; - public final byte[] iv; - private final String fileName; - public final String cipherName; - - private FixtureSet(final int keySize) { - if (keySize < 1) { - throw new IllegalArgumentException("keySize"); - } - - final int keyLength = keySize * 8; // bytes to bits - fileName = "crypto-data-" + keyLength; - cipherName = "cipher+aes-" + keyLength + "-cbc"; - - CryptoTestData testData; - try { - testData = loadTestData(); - } catch (IOException e) { - throw new Error(e); // caught to uncaught - } - key = Base64Coder.decode(testData.key); - iv = Base64Coder.decode(testData.iv); - - if (keySize != this.key.length) { - throw new IllegalArgumentException("key"); - } - if (16 != this.iv.length) { - throw new IllegalArgumentException("iv"); - } - } - - private CryptoTestData loadTestData() throws IOException { - return (CryptoTestData)Setup.loadJson( - "ably-common/test-resources/" + fileName + ".json", - CryptoTestData.class); - } - } - - @Parameters(name= "{0}") - public static Object[][] data() { - return new Object[][] { - { FixtureSet.AES128 }, - { FixtureSet.AES256 }, - }; - } - - private final FixtureSet fixtureSet; - - public CryptoMessageTest(final FixtureSet fixtureSet) { - this.fixtureSet = fixtureSet; - } - - @Test - public void testDecrypt() throws NoSuchAlgorithmException, CloneNotSupportedException, AblyException, IOException { - final CryptoTestData testData = fixtureSet.loadTestData(); - final String algorithm = testData.algorithm; - - final CipherParams params = Crypto.getParams(algorithm, fixtureSet.key, fixtureSet.iv); - final ChannelOptions options = new ChannelOptions() {{encrypted = true; cipherParams = params;}}; - - for(final CryptoTestItem item : testData.items) { - final Message plain = item.encoded; - final Message encrypted = item.encrypted; - assertThat(encrypted.encoding, endsWith(fixtureSet.cipherName + "/base64")); - - // if necessary, remove base64 encoding from plain-'text' message - plain.decode(null); - assertEquals(null, plain.encoding); - - // perform the decryption (via decode) which is the thing we need to test - encrypted.decode(options); - assertEquals(null, encrypted.encoding); - - // compare the expected plain-'text' bytes with those decrypted above - assertMessagesEqual(plain, encrypted); - } - } - - @Test - public void testEncrypt() throws NoSuchAlgorithmException, CloneNotSupportedException, AblyException, IOException { - final CryptoTestData testData = fixtureSet.loadTestData(); - final String algorithm = testData.algorithm; - - final CipherParams params = Crypto.getParams(algorithm, fixtureSet.key, fixtureSet.iv); - - for(final CryptoTestItem item : testData.items) { - final ChannelOptions options = new ChannelOptions() {{encrypted = true; cipherParams = params;}}; - final Message plain = item.encoded; - final Message encrypted = item.encrypted; - assertThat(encrypted.encoding, endsWith(fixtureSet.cipherName + "/base64")); - - // if necessary, remove base64 encoding from plain-'text' message - plain.decode(null); - assertEquals(null, plain.encoding); - - // perform the encryption (via encode) which is the thing we need to test - plain.encode(options); - assertThat(plain.encoding, endsWith(fixtureSet.cipherName)); - - // our test fixture always provides a string for the encrypted data, which means - // that it's base64 encoded - so we need to base64 decode it to get the encrypted bytes - final byte[] expected = Base64Coder.decode((String)encrypted.data); - - // compare the expected encrypted bytes with those encrypted above - final byte[] actual = (byte[])plain.data; - assertArrayEquals(expected, actual); - } - } - - static class CryptoTestData { - public String algorithm; - public String mode; - public int keylength; - public String key; - public String iv; - public CryptoTestItem[] items; - } - - static class CryptoTestItem { - public Message encoded; - public Message encrypted; - } + public enum FixtureSet { + AES128(16), + AES256(32); + + public final byte[] key; + public final byte[] iv; + private final String fileName; + public final String cipherName; + + private FixtureSet(final int keySize) { + if (keySize < 1) { + throw new IllegalArgumentException("keySize"); + } + + final int keyLength = keySize * 8; // bytes to bits + fileName = "crypto-data-" + keyLength; + cipherName = "cipher+aes-" + keyLength + "-cbc"; + + CryptoTestData testData; + try { + testData = loadTestData(); + } catch (IOException e) { + throw new Error(e); // caught to uncaught + } + key = Base64Coder.decode(testData.key); + iv = Base64Coder.decode(testData.iv); + + if (keySize != this.key.length) { + throw new IllegalArgumentException("key"); + } + if (16 != this.iv.length) { + throw new IllegalArgumentException("iv"); + } + } + + private CryptoTestData loadTestData() throws IOException { + return (CryptoTestData)Setup.loadJson( + "ably-common/test-resources/" + fileName + ".json", + CryptoTestData.class); + } + } + + @Parameters(name= "{0}") + public static Object[][] data() { + return new Object[][] { + { FixtureSet.AES128 }, + { FixtureSet.AES256 }, + }; + } + + private final FixtureSet fixtureSet; + + public CryptoMessageTest(final FixtureSet fixtureSet) { + this.fixtureSet = fixtureSet; + } + + @Test + public void testDecrypt() throws NoSuchAlgorithmException, CloneNotSupportedException, AblyException, IOException { + final CryptoTestData testData = fixtureSet.loadTestData(); + final String algorithm = testData.algorithm; + + final CipherParams params = Crypto.getParams(algorithm, fixtureSet.key, fixtureSet.iv); + final ChannelOptions options = new ChannelOptions() {{encrypted = true; cipherParams = params;}}; + + for(final CryptoTestItem item : testData.items) { + final Message plain = item.encoded; + final Message encrypted = item.encrypted; + assertThat(encrypted.encoding, endsWith(fixtureSet.cipherName + "/base64")); + + // if necessary, remove base64 encoding from plain-'text' message + plain.decode(null); + assertEquals(null, plain.encoding); + + // perform the decryption (via decode) which is the thing we need to test + encrypted.decode(options); + assertEquals(null, encrypted.encoding); + + // compare the expected plain-'text' bytes with those decrypted above + assertMessagesEqual(plain, encrypted); + } + } + + @Test + public void testEncrypt() throws NoSuchAlgorithmException, CloneNotSupportedException, AblyException, IOException { + final CryptoTestData testData = fixtureSet.loadTestData(); + final String algorithm = testData.algorithm; + + final CipherParams params = Crypto.getParams(algorithm, fixtureSet.key, fixtureSet.iv); + + for(final CryptoTestItem item : testData.items) { + final ChannelOptions options = new ChannelOptions() {{encrypted = true; cipherParams = params;}}; + final Message plain = item.encoded; + final Message encrypted = item.encrypted; + assertThat(encrypted.encoding, endsWith(fixtureSet.cipherName + "/base64")); + + // if necessary, remove base64 encoding from plain-'text' message + plain.decode(null); + assertEquals(null, plain.encoding); + + // perform the encryption (via encode) which is the thing we need to test + plain.encode(options); + assertThat(plain.encoding, endsWith(fixtureSet.cipherName)); + + // our test fixture always provides a string for the encrypted data, which means + // that it's base64 encoded - so we need to base64 decode it to get the encrypted bytes + final byte[] expected = Base64Coder.decode((String)encrypted.data); + + // compare the expected encrypted bytes with those encrypted above + final byte[] actual = (byte[])plain.data; + assertArrayEquals(expected, actual); + } + } + + static class CryptoTestData { + public String algorithm; + public String mode; + public int keylength; + public String key; + public String iv; + public CryptoTestItem[] items; + } + + static class CryptoTestItem { + public Message encoded; + public Message encrypted; + } } diff --git a/lib/src/test/java/io/ably/lib/util/CryptoTest.java b/lib/src/test/java/io/ably/lib/util/CryptoTest.java index c6c6efdb0..60673bbf1 100644 --- a/lib/src/test/java/io/ably/lib/util/CryptoTest.java +++ b/lib/src/test/java/io/ably/lib/util/CryptoTest.java @@ -23,178 +23,178 @@ import io.ably.lib.util.CryptoMessageTest.FixtureSet; public class CryptoTest { - /** - * Test Crypto.getDefaultParams. - * @see RSE1 - */ - @Test - public void cipher_params() throws AblyException, NoSuchAlgorithmException { - /* 128-bit key */ - /* {0xFF, 0xFE, 0xFD, 0xFC, 0xFB, 0xFA, 0xF9, 0xF8, 0xF7, 0xF6, 0xF5, 0xF4, 0xF3, 0xF2, 0xF1, 0xF0}; */ - byte[] key = {-1, -2, -3, -4, -5, -6, -7, -8, -9, -10, -11, -12, -13, -14, -15, -16}; - /* Same key but encoded with Base64 */ - String base64key = "//79/Pv6+fj39vX08/Lx8A=="; - /* Same key but encoded in URL style (RFC 4648 s.5) */ - String base64key2 = "__79_Pv6-fj39vX08_Lx8A=="; - - /* IV */ - byte[] iv = {16, 15, 14, 13, 12, 11, 10, 9, 8, 7, 6, 5, 4, 3, 2, 1}; - - final CipherParams params1 = Crypto.getDefaultParams(key, iv); - final CipherParams params2 = Crypto.getDefaultParams(base64key, iv); - final CipherParams params3 = new CipherParams(null, key, iv); - final CipherParams params4 = Crypto.getDefaultParams(base64key2, iv); - - assertEquals("aes", params1.getAlgorithm()); - - assertTrue( - "Key length is incorrect", - params1.getKeyLength() == key.length*8 && - params2.getKeyLength() == key.length*8 && - params3.getKeyLength() == key.length*8 && - params4.getKeyLength() == key.length*8 - ); - - byte[] plaintext = {1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16}; - ChannelCipher channelCipher1 = Crypto.getCipher(new ChannelOptions() {{ encrypted=true; cipherParams=params1; }}); - ChannelCipher channelCipher2 = Crypto.getCipher(new ChannelOptions() {{ encrypted=true; cipherParams=params2; }}); - ChannelCipher channelCipher3 = Crypto.getCipher(new ChannelOptions() {{ encrypted=true; cipherParams=params3; }}); - ChannelCipher channelCipher4 = Crypto.getCipher(new ChannelOptions() {{ encrypted=true; cipherParams=params4; }}); - - byte[] ciphertext1 = channelCipher1.encrypt(plaintext); - byte[] ciphertext2 = channelCipher2.encrypt(plaintext); - byte[] ciphertext3 = channelCipher3.encrypt(plaintext); - byte[] ciphertext4 = channelCipher4.encrypt(plaintext); - - assertTrue( - "All ciphertexts should be the same.", - Arrays.equals(ciphertext1, ciphertext2) && - Arrays.equals(ciphertext1, ciphertext3) && - Arrays.equals(ciphertext1, ciphertext4) - ); - } - - /** - * Test encryption using a 256 bit key and varying lengths of data. - * - * The key, IV and message data are the same for every test run so that the - * encrypted data may be exported from the console output for consumption by tests - * run on other platforms. This output is manually merged into the file - * test-resources/crypto-data-256.json in ably-common. - * - * Equivalent to the following in ably-cocoa: - * testEncryptAndDecrypt in Spec/CryptoTest.m - * @throws IOException - */ - @Test - public void encryptAndDecrypt() throws NoSuchAlgorithmException, AblyException, IOException { - final FixtureSet fixtureSet = FixtureSet.AES256; - final CipherParams params = Crypto.getDefaultParams(fixtureSet.key, fixtureSet.iv); - - // Prepare message data. - final int maxLength = 70; - final byte[] message = new byte[maxLength]; - for (byte value=1; value<=maxLength; value++) { - message[value - 1] = value; - } - - final StringWriter target = new StringWriter(); - final JsonWriter writer = new JsonWriter(target); - writer.setIndent(" "); - writer.beginObject(); - - writer.name("algorithm"); - writer.value("aes"); - - writer.name("mode"); - writer.value("cbc"); - - writer.name("keyLength"); - writer.value(256); - - writer.name("key"); - writer.value(Base64Coder.encodeToString(fixtureSet.key)); - - writer.name("iv"); - writer.value(Base64Coder.encodeToString(fixtureSet.iv)); - - // Perform encrypt and decrypt on message data trimmed at all lengths up - // to and including maxLength. - writer.name("items"); - writer.beginArray(); - for (int i=1; i<=maxLength; i++) { - // We need to create a new ChannelCipher for each message we encode, - // so that our IV gets used (being start of CBC chain). - final ChannelCipher cipher = Crypto.getCipher(new ChannelOptions() {{ encrypted=true; cipherParams=params; }}); - - // Encrypt i bytes from the start of the message data. - final byte[] encoded = Arrays.copyOfRange(message, 0, i); - final byte[] encrypted = cipher.encrypt(encoded); - - // Add encryption result to results in format ready for fixture. - writeResult(writer, "byte 1 to " + i, encoded, encrypted, fixtureSet.cipherName); - - // Decrypt the encrypted data and verify the result is the same as what - // we submitted for encryption. - final byte[] verify = cipher.decrypt(encrypted); - assertArrayEquals(verify, encoded); - } - writer.endArray(); - - writer.endObject(); - - System.out.println("Fixture JSON for test-resources:\n" + target.toString()); - } - - private static void writeResult(final JsonWriter writer, final String name, final byte[] encoded, final byte[] encrypted, final String cipherName) throws IOException { - writer.beginObject(); - - writer.name("encoded"); - writeData(writer, name, encoded, null); - - writer.name("encrypted"); - writeData(writer, name, encrypted, cipherName); - - writer.name("msgpack"); - writer.value(Base64Coder.encodeToString(msgPacked(name, encrypted, cipherName))); - - writer.endObject(); - } - - private static final String BASE64 = "base64"; - private static void writeData(final JsonWriter writer, final String name, final byte[] data, final String encoding) throws IOException { - writer.beginObject(); - - writer.name("name"); - writer.value(name); - - writer.name("data"); - writer.value(Base64Coder.encodeToString(data)); - - writer.name("encoding"); - writer.value(null == encoding ? BASE64 : encoding + "/" + BASE64); - - writer.endObject(); - } - - private static byte[] msgPacked(final String name, final byte[] data, final String encoding) throws IOException { - final ByteArrayOutputStream out = new ByteArrayOutputStream(); - final MessagePacker packer = MessagePack.DEFAULT_PACKER_CONFIG.newPacker(out); - - packer.packMapHeader(3); + /** + * Test Crypto.getDefaultParams. + * @see RSE1 + */ + @Test + public void cipher_params() throws AblyException, NoSuchAlgorithmException { + /* 128-bit key */ + /* {0xFF, 0xFE, 0xFD, 0xFC, 0xFB, 0xFA, 0xF9, 0xF8, 0xF7, 0xF6, 0xF5, 0xF4, 0xF3, 0xF2, 0xF1, 0xF0}; */ + byte[] key = {-1, -2, -3, -4, -5, -6, -7, -8, -9, -10, -11, -12, -13, -14, -15, -16}; + /* Same key but encoded with Base64 */ + String base64key = "//79/Pv6+fj39vX08/Lx8A=="; + /* Same key but encoded in URL style (RFC 4648 s.5) */ + String base64key2 = "__79_Pv6-fj39vX08_Lx8A=="; + + /* IV */ + byte[] iv = {16, 15, 14, 13, 12, 11, 10, 9, 8, 7, 6, 5, 4, 3, 2, 1}; + + final CipherParams params1 = Crypto.getDefaultParams(key, iv); + final CipherParams params2 = Crypto.getDefaultParams(base64key, iv); + final CipherParams params3 = new CipherParams(null, key, iv); + final CipherParams params4 = Crypto.getDefaultParams(base64key2, iv); + + assertEquals("aes", params1.getAlgorithm()); + + assertTrue( + "Key length is incorrect", + params1.getKeyLength() == key.length*8 && + params2.getKeyLength() == key.length*8 && + params3.getKeyLength() == key.length*8 && + params4.getKeyLength() == key.length*8 + ); + + byte[] plaintext = {1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16}; + ChannelCipher channelCipher1 = Crypto.getCipher(new ChannelOptions() {{ encrypted=true; cipherParams=params1; }}); + ChannelCipher channelCipher2 = Crypto.getCipher(new ChannelOptions() {{ encrypted=true; cipherParams=params2; }}); + ChannelCipher channelCipher3 = Crypto.getCipher(new ChannelOptions() {{ encrypted=true; cipherParams=params3; }}); + ChannelCipher channelCipher4 = Crypto.getCipher(new ChannelOptions() {{ encrypted=true; cipherParams=params4; }}); + + byte[] ciphertext1 = channelCipher1.encrypt(plaintext); + byte[] ciphertext2 = channelCipher2.encrypt(plaintext); + byte[] ciphertext3 = channelCipher3.encrypt(plaintext); + byte[] ciphertext4 = channelCipher4.encrypt(plaintext); + + assertTrue( + "All ciphertexts should be the same.", + Arrays.equals(ciphertext1, ciphertext2) && + Arrays.equals(ciphertext1, ciphertext3) && + Arrays.equals(ciphertext1, ciphertext4) + ); + } + + /** + * Test encryption using a 256 bit key and varying lengths of data. + * + * The key, IV and message data are the same for every test run so that the + * encrypted data may be exported from the console output for consumption by tests + * run on other platforms. This output is manually merged into the file + * test-resources/crypto-data-256.json in ably-common. + * + * Equivalent to the following in ably-cocoa: + * testEncryptAndDecrypt in Spec/CryptoTest.m + * @throws IOException + */ + @Test + public void encryptAndDecrypt() throws NoSuchAlgorithmException, AblyException, IOException { + final FixtureSet fixtureSet = FixtureSet.AES256; + final CipherParams params = Crypto.getDefaultParams(fixtureSet.key, fixtureSet.iv); + + // Prepare message data. + final int maxLength = 70; + final byte[] message = new byte[maxLength]; + for (byte value=1; value<=maxLength; value++) { + message[value - 1] = value; + } + + final StringWriter target = new StringWriter(); + final JsonWriter writer = new JsonWriter(target); + writer.setIndent(" "); + writer.beginObject(); + + writer.name("algorithm"); + writer.value("aes"); + + writer.name("mode"); + writer.value("cbc"); + + writer.name("keyLength"); + writer.value(256); + + writer.name("key"); + writer.value(Base64Coder.encodeToString(fixtureSet.key)); + + writer.name("iv"); + writer.value(Base64Coder.encodeToString(fixtureSet.iv)); + + // Perform encrypt and decrypt on message data trimmed at all lengths up + // to and including maxLength. + writer.name("items"); + writer.beginArray(); + for (int i=1; i<=maxLength; i++) { + // We need to create a new ChannelCipher for each message we encode, + // so that our IV gets used (being start of CBC chain). + final ChannelCipher cipher = Crypto.getCipher(new ChannelOptions() {{ encrypted=true; cipherParams=params; }}); + + // Encrypt i bytes from the start of the message data. + final byte[] encoded = Arrays.copyOfRange(message, 0, i); + final byte[] encrypted = cipher.encrypt(encoded); + + // Add encryption result to results in format ready for fixture. + writeResult(writer, "byte 1 to " + i, encoded, encrypted, fixtureSet.cipherName); + + // Decrypt the encrypted data and verify the result is the same as what + // we submitted for encryption. + final byte[] verify = cipher.decrypt(encrypted); + assertArrayEquals(verify, encoded); + } + writer.endArray(); + + writer.endObject(); + + System.out.println("Fixture JSON for test-resources:\n" + target.toString()); + } + + private static void writeResult(final JsonWriter writer, final String name, final byte[] encoded, final byte[] encrypted, final String cipherName) throws IOException { + writer.beginObject(); + + writer.name("encoded"); + writeData(writer, name, encoded, null); + + writer.name("encrypted"); + writeData(writer, name, encrypted, cipherName); + + writer.name("msgpack"); + writer.value(Base64Coder.encodeToString(msgPacked(name, encrypted, cipherName))); + + writer.endObject(); + } + + private static final String BASE64 = "base64"; + private static void writeData(final JsonWriter writer, final String name, final byte[] data, final String encoding) throws IOException { + writer.beginObject(); + + writer.name("name"); + writer.value(name); + + writer.name("data"); + writer.value(Base64Coder.encodeToString(data)); + + writer.name("encoding"); + writer.value(null == encoding ? BASE64 : encoding + "/" + BASE64); + + writer.endObject(); + } + + private static byte[] msgPacked(final String name, final byte[] data, final String encoding) throws IOException { + final ByteArrayOutputStream out = new ByteArrayOutputStream(); + final MessagePacker packer = MessagePack.DEFAULT_PACKER_CONFIG.newPacker(out); + + packer.packMapHeader(3); - packer.packString("name"); - packer.packString(name); + packer.packString("name"); + packer.packString(name); - packer.packString("data"); - packer.packBinaryHeader(data.length); - packer.writePayload(data); + packer.packString("data"); + packer.packBinaryHeader(data.length); + packer.writePayload(data); - packer.packString("encoding"); - packer.packString(encoding); + packer.packString("encoding"); + packer.packString(encoding); - packer.close(); + packer.close(); - return out.toByteArray(); - } + return out.toByteArray(); + } } From b04cf948985fa8871bae374fe5f168f0f39dbb55 Mon Sep 17 00:00:00 2001 From: Quintin Willison Date: Thu, 1 Oct 2020 15:43:09 +0100 Subject: [PATCH 2/3] Convert tabs to spaces where they appear in the middle of a line in java files. --- .../java/io/ably/lib/http/HttpPaginatedQuery.java | 2 +- lib/src/main/java/io/ably/lib/realtime/Presence.java | 2 +- lib/src/main/java/io/ably/lib/rest/Auth.java | 12 ++++++------ .../io/ably/lib/transport/ConnectionManager.java | 4 ++-- .../main/java/io/ably/lib/transport/Defaults.java | 2 +- .../io/ably/lib/transport/WebSocketTransport.java | 4 ++-- 6 files changed, 13 insertions(+), 13 deletions(-) diff --git a/lib/src/main/java/io/ably/lib/http/HttpPaginatedQuery.java b/lib/src/main/java/io/ably/lib/http/HttpPaginatedQuery.java index 5112bf8b2..f2fe97984 100644 --- a/lib/src/main/java/io/ably/lib/http/HttpPaginatedQuery.java +++ b/lib/src/main/java/io/ably/lib/http/HttpPaginatedQuery.java @@ -132,7 +132,7 @@ private HttpPaginatedResponse execRel(String linkUrl) throws AblyException { @Override public boolean isLast() { return relNext == null; - } + } } static final HttpCore.BodyHandler jsonArrayResponseHandler = new HttpCore.BodyHandler() { diff --git a/lib/src/main/java/io/ably/lib/realtime/Presence.java b/lib/src/main/java/io/ably/lib/realtime/Presence.java index d9ef91462..e78532504 100644 --- a/lib/src/main/java/io/ably/lib/realtime/Presence.java +++ b/lib/src/main/java/io/ably/lib/realtime/Presence.java @@ -785,7 +785,7 @@ private class PresenceMap { * state other than attached or attaching */ synchronized void waitForSync() throws AblyException, InterruptedException { - boolean syncIsComplete = false; /* temporary variable to avoid potential race conditions */ + boolean syncIsComplete = false; /* temporary variable to avoid potential race conditions */ while((channel.state == ChannelState.attached || channel.state == ChannelState.attaching) && /* = (and not ==) is intentional */ !(syncIsComplete = (!syncInProgress && syncComplete))) diff --git a/lib/src/main/java/io/ably/lib/rest/Auth.java b/lib/src/main/java/io/ably/lib/rest/Auth.java index 5a11b13fa..a03e622bf 100644 --- a/lib/src/main/java/io/ably/lib/rest/Auth.java +++ b/lib/src/main/java/io/ably/lib/rest/Auth.java @@ -768,12 +768,12 @@ else if(!request.keyName.equals(keyName)) request.nonce = random(); String signText - = request.keyName + '\n' - + ttlText + '\n' - + capabilityText + '\n' - + clientIdText + '\n' - + request.timestamp + '\n' - + request.nonce + '\n'; + = request.keyName + '\n' + + ttlText + '\n' + + capabilityText + '\n' + + clientIdText + '\n' + + request.timestamp + '\n' + + request.nonce + '\n'; request.mac = hmac(signText, keySecret); diff --git a/lib/src/main/java/io/ably/lib/transport/ConnectionManager.java b/lib/src/main/java/io/ably/lib/transport/ConnectionManager.java index 1f10e06ee..9a77d58ef 100644 --- a/lib/src/main/java/io/ably/lib/transport/ConnectionManager.java +++ b/lib/src/main/java/io/ably/lib/transport/ConnectionManager.java @@ -1564,8 +1564,8 @@ public synchronized void nack(long serial, int count, ErrorInfo reason) { * reset the pending message queue, failing any currently pending messages. * Used when a resume fails and we get a different connection id. * @param oldMsgSerial the next message serial number for the old - * connection, and thus one more than the highest message serial - * in the queue. + * connection, and thus one more than the highest message serial + * in the queue. */ public synchronized void reset(long oldMsgSerial, ErrorInfo err) { nack(startSerial, (int)(oldMsgSerial - startSerial), err); diff --git a/lib/src/main/java/io/ably/lib/transport/Defaults.java b/lib/src/main/java/io/ably/lib/transport/Defaults.java index 9feaca4ff..2345388a4 100644 --- a/lib/src/main/java/io/ably/lib/transport/Defaults.java +++ b/lib/src/main/java/io/ably/lib/transport/Defaults.java @@ -29,7 +29,7 @@ public class Defaults { /* Timeouts */ public static int TIMEOUT_CONNECT = 15000; public static int TIMEOUT_DISCONNECT = 15000; - public static int TIMEOUT_CHANNEL_RETRY = 15000; + public static int TIMEOUT_CHANNEL_RETRY = 15000; /* TO313 */ public static int TIMEOUT_HTTP_OPEN = 4000; diff --git a/lib/src/main/java/io/ably/lib/transport/WebSocketTransport.java b/lib/src/main/java/io/ably/lib/transport/WebSocketTransport.java index c733703a7..9bc5e2975 100644 --- a/lib/src/main/java/io/ably/lib/transport/WebSocketTransport.java +++ b/lib/src/main/java/io/ably/lib/transport/WebSocketTransport.java @@ -329,8 +329,8 @@ public String getURL() { private static final int GOING_AWAY = 1001; private static final int CLOSE_PROTOCOL_ERROR = 1002; private static final int REFUSE = 1003; -/* private static final int UNUSED = 1004; */ -/* private static final int NOCODE = 1005; */ +/* private static final int UNUSED = 1004; */ +/* private static final int NOCODE = 1005; */ private static final int ABNORMAL_CLOSE = 1006; private static final int NO_UTF8 = 1007; private static final int POLICY_VALIDATION = 1008; From aec446d5a0bc447afb0623272e509963c7b68119 Mon Sep 17 00:00:00 2001 From: Quintin Willison Date: Thu, 1 Oct 2020 15:43:47 +0100 Subject: [PATCH 3/3] Add rule enforcing use of spaces, not tabs. --- config/checkstyle/checkstyle.xml | 2 ++ 1 file changed, 2 insertions(+) diff --git a/config/checkstyle/checkstyle.xml b/config/checkstyle/checkstyle.xml index b64bc8a87..fb2bf6dfd 100644 --- a/config/checkstyle/checkstyle.xml +++ b/config/checkstyle/checkstyle.xml @@ -6,6 +6,8 @@ + +