From e1bfc3b6c725823f8c5c958fbc38f3d6c3843c62 Mon Sep 17 00:00:00 2001 From: Forrest Hopkins Date: Mon, 31 Jul 2017 17:00:12 -0700 Subject: [PATCH 01/33] Removing Retrolambda and adding support for Native Java 8 compilation --- .gitignore | 3 +++ build.gradle | 8 +++++--- example-client/build.gradle | 7 +++---- gradle/wrapper/gradle-wrapper.properties | 4 ++-- lib/build.gradle | 7 +++---- 5 files changed, 16 insertions(+), 13 deletions(-) diff --git a/.gitignore b/.gitignore index c6cbe56..a814809 100644 --- a/.gitignore +++ b/.gitignore @@ -3,6 +3,9 @@ /local.properties /.idea/workspace.xml /.idea/libraries +/.idea/misc.xml +/.idea/modules.xml +/.idea/vcs.xml .DS_Store /build /captures diff --git a/build.gradle b/build.gradle index de159c8..4b06603 100644 --- a/build.gradle +++ b/build.gradle @@ -3,11 +3,13 @@ buildscript { repositories { jcenter() + maven { + url "https://maven.google.com" + } } dependencies { - classpath 'com.android.tools.build:gradle:2.3.0' - classpath 'me.tatarka:gradle-retrolambda:3.4.0' - classpath 'com.github.dcendents:android-maven-gradle-plugin:1.4.1' + classpath 'com.android.tools.build:gradle:3.0.0-alpha8' + classpath 'com.github.dcendents:android-maven-gradle-plugin:1.5' // NOTE: Do not place your application dependencies here; they belong // in the individual module build.gradle files diff --git a/example-client/build.gradle b/example-client/build.gradle index f254f38..b12b778 100644 --- a/example-client/build.gradle +++ b/example-client/build.gradle @@ -1,5 +1,4 @@ apply plugin: 'com.android.application' -apply plugin: 'me.tatarka.retrolambda' android { compileSdkVersion 25 @@ -32,8 +31,8 @@ dependencies { compile 'org.java-websocket:java-websocket:1.3.2' compile 'com.android.support:recyclerview-v7:25.2.0' compile 'io.reactivex:rxandroid:1.2.1' - compile 'com.squareup.retrofit2:converter-gson:2.1.0' - compile 'com.squareup.retrofit2:adapter-rxjava:2.1.0' - compile 'com.squareup.retrofit2:retrofit:2.1.0' + compile 'com.squareup.retrofit2:converter-gson:2.3.0' + compile 'com.squareup.retrofit2:adapter-rxjava:2.3.0' + compile 'com.squareup.retrofit2:retrofit:2.3.0' compile project(':lib') } diff --git a/gradle/wrapper/gradle-wrapper.properties b/gradle/wrapper/gradle-wrapper.properties index 16f16db..3eecfb3 100644 --- a/gradle/wrapper/gradle-wrapper.properties +++ b/gradle/wrapper/gradle-wrapper.properties @@ -1,6 +1,6 @@ -#Thu Feb 23 17:37:13 EET 2017 +#Mon Jul 31 15:30:48 MST 2017 distributionBase=GRADLE_USER_HOME distributionPath=wrapper/dists zipStoreBase=GRADLE_USER_HOME zipStorePath=wrapper/dists -distributionUrl=https\://services.gradle.org/distributions/gradle-3.3-all.zip +distributionUrl=https\://services.gradle.org/distributions/gradle-4.1-milestone-1-all.zip diff --git a/lib/build.gradle b/lib/build.gradle index 183b041..4401dad 100644 --- a/lib/build.gradle +++ b/lib/build.gradle @@ -1,12 +1,11 @@ apply plugin: 'com.android.library' -apply plugin: 'me.tatarka.retrolambda' apply plugin: 'com.github.dcendents.android-maven' group='com.github.NaikSoftware' android { compileSdkVersion 25 - buildToolsVersion "25.0.1" + buildToolsVersion "25.0.2" defaultConfig { minSdkVersion 16 @@ -32,10 +31,10 @@ android { dependencies { compile fileTree(include: ['*.jar'], dir: 'libs') testCompile 'junit:junit:4.12' - compile 'io.reactivex:rxjava:1.2.0' + compile 'io.reactivex:rxjava:1.3.0' // Supported transports provided "org.java-websocket:java-websocket:1.3.2" - provided 'com.squareup.okhttp3:okhttp:3.8.0' + provided 'com.squareup.okhttp3:okhttp:3.8.1' } task sourcesJar(type: Jar) { From 36ba99c8f46afc421cc39f79970714d3f8f65d5f Mon Sep 17 00:00:00 2001 From: Forrest Hopkins Date: Tue, 1 Aug 2017 08:08:34 -0700 Subject: [PATCH 02/33] Update to Canary 9 --- build.gradle | 2 +- gradle/wrapper/gradle-wrapper.properties | 4 ++-- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/build.gradle b/build.gradle index 4b06603..4a30413 100644 --- a/build.gradle +++ b/build.gradle @@ -8,7 +8,7 @@ buildscript { } } dependencies { - classpath 'com.android.tools.build:gradle:3.0.0-alpha8' + classpath 'com.android.tools.build:gradle:3.0.0-alpha9' classpath 'com.github.dcendents:android-maven-gradle-plugin:1.5' // NOTE: Do not place your application dependencies here; they belong diff --git a/gradle/wrapper/gradle-wrapper.properties b/gradle/wrapper/gradle-wrapper.properties index 3eecfb3..168b761 100644 --- a/gradle/wrapper/gradle-wrapper.properties +++ b/gradle/wrapper/gradle-wrapper.properties @@ -1,6 +1,6 @@ -#Mon Jul 31 15:30:48 MST 2017 +#Tue Aug 01 08:03:03 MST 2017 distributionBase=GRADLE_USER_HOME distributionPath=wrapper/dists zipStoreBase=GRADLE_USER_HOME zipStorePath=wrapper/dists -distributionUrl=https\://services.gradle.org/distributions/gradle-4.1-milestone-1-all.zip +distributionUrl=https\://services.gradle.org/distributions/gradle-4.1-rc-1-all.zip From 1952ea1396ec5c48888c1bc97bb4426e2a251118 Mon Sep 17 00:00:00 2001 From: Forrest Hopkins Date: Tue, 1 Aug 2017 12:43:10 -0700 Subject: [PATCH 03/33] Fixed Gradle expecting both OkHttp and JWS, and updated documentation --- README.md | 119 ++++++++++++++++++++++++++++++----------------- lib/build.gradle | 4 +- 2 files changed, 79 insertions(+), 44 deletions(-) diff --git a/README.md b/README.md index 38fa3e0..0f6ab24 100644 --- a/README.md +++ b/README.md @@ -1,14 +1,17 @@ # STOMP protocol via WebSocket for Android -[![Release](https://jitpack.io/v/NaikSoftware/StompProtocolAndroid.svg)](https://jitpack.io/#NaikSoftware/StompProtocolAndroid) +[![Release](https://jitpack.io/v/forresthopkinsa/StompProtocolAndroid.svg)](https://jitpack.io/#forresthopkinsa/StompProtocolAndroid) ## Overview -This library provide support for STOMP protocol https://stomp.github.io/ -At now library works only as client for backend with support STOMP, such as -NodeJS (stompjs or other) or Spring Boot (SockJS). +**Note that this is a FORK of a project by NaikSoftware! This version is purely to avoid using RetroLambda!** -Add library as gradle dependency +This library provides support for [STOMP protocol](https://stomp.github.io/) over Websockets. + +At now library works only as client for any backend that supports STOMP, such as +NodeJS (e.g. using StompJS) or Spring Boot ([with WebSocket support](https://spring.io/guides/gs/messaging-stomp-websocket/)). + +Add library as gradle dependency (Versioning info [here](https://jitpack.io/#forresthopkinsa/StompProtocolAndroid)): ```gradle repositories { @@ -16,45 +19,54 @@ repositories { maven { url "https://jitpack.io" } } dependencies { - compile 'com.github.NaikSoftware:StompProtocolAndroid:{latest version}' + compile 'com.github.forresthopkinsa:StompProtocolAndroid:{latest version}' } ``` +You can use this library two ways: + +- Using the old JACK toolchain + - If you have Java 8 compatiblity and Jack enabled, this library will work for you +- Using the new Native Java 8 support + - As of this writing, you must be using Android Studio Canary to use this feature. + - You can find more info on the [Releases Page](https://github.com/forresthopkinsa/StompProtocolAndroid/releases) + +However, *this fork is NOT compatible with Retrolambda.* +If you have RL as a dependency, then you should be using the [upstream version](https://github.com/NaikSoftware/StompProtocolAndroid) of this project! + ## Example backend (Spring Boot) -**WebSocketConfig.groovy** -```groovy +**WebSocketConfig.java** +```java @Configuration @EnableWebSocket @EnableWebSocketMessageBroker class WebSocketConfig extends AbstractWebSocketMessageBrokerConfigurer { @Override - void configureMessageBroker(MessageBrokerRegistry config) { - config.enableSimpleBroker("/topic", "/queue", "/exchange"); -// config.enableStompBrokerRelay("/topic", "/queue", "/exchange"); // Uncomment for external message broker (ActiveMQ, RabbitMQ) - config.setApplicationDestinationPrefixes("/topic", "/queue"); // prefix in client queries - config.setUserDestinationPrefix("/user"); + public void configureMessageBroker(MessageBrokerRegistry config) { + // We're using Spring's built-in message broker, with prefix "/topic" + config.enableSimpleBroker("/topic"); + // This is the prefix for client requests + config.setApplicationDestinationPrefixes("/app"); } @Override - void registerStompEndpoints(StompEndpointRegistry registry) { - registry.addEndpoint("/example-endpoint").withSockJS() + public void registerStompEndpoints(StompEndpointRegistry registry) { + registry.addEndpoint("/example-endpoint"); } } ``` -**SocketController.groovy** -``` groovy -@Log4j +**SocketController.java** +``` java @RestController class SocketController { - @MessageMapping('/hello-msg-mapping') + @MessageMapping('/hello') @SendTo('/topic/greetings') - EchoModel echoMessageMapping(String message) { - log.debug("React to hello-msg-mapping") - return new EchoModel(message.trim()) + public String greeting(String name) { + return "Hello, " + name + "!"; } } ``` @@ -65,24 +77,30 @@ Check out the full example server https://github.com/NaikSoftware/stomp-protocol **Basic usage** ``` java - import org.java_websocket.WebSocket; - - private StompClient mStompClient; + import okhttp3.OkHttpClient; + import okhttp3.Request; + import okhttp3.Response; + import okhttp3.WebSocket; + import okhttp3.WebSocketListener; // ... - mStompClient = Stomp.over(WebSocket.class, "ws://10.0.2.2:8080/example-endpoint/websocket"); - mStompClient.connect(); + StompClient client = Stomp.over(WebSocket.class, "http://localhost/example-endpoint"); + client.connect(); - mStompClient.topic("/topic/greetings").subscribe(topicMessage -> { - Log.d(TAG, topicMessage.getPayload()); + client.topic("/topic/greetings").subscribe(message -> { + Log.i(TAG, "Received message: " + message.getPayload()); }); - mStompClient.send("/topic/hello-msg-mapping", "My first STOMP message!").subscribe(); + client.send("/app/hello", "world").subscribe( + aVoid -> Log.d(TAG, "Sent data!"), + error -> Log.e(TAG, "Encountered error while sending data!", error) + ); // ... - - mStompClient.disconnect(); + + // close socket connection when finished or exiting + client.disconnect(); ``` @@ -91,30 +109,47 @@ See the full example https://github.com/NaikSoftware/StompProtocolAndroid/tree/m Method `Stomp.over` consume class for create connection as first parameter. You must provide dependency for lib and pass class. At now supported connection providers: -- `org.java_websocket.WebSocket.class` ('org.java-websocket:Java-WebSocket:1.3.0') -- `okhttp3.WebSocket.class` ('com.squareup.okhttp3:okhttp:3.8.0') +- `org.java_websocket.WebSocket.class` ('org.java-websocket:Java-WebSocket:1.3.2') +- `okhttp3.WebSocket.class` ('com.squareup.okhttp3:okhttp:3.8.1') You can add own connection provider. Just implement interface `ConnectionProvider`. If you implement new provider, please create pull request :) **Subscribe lifecycle connection** ``` java -mStompClient.lifecycle().subscribe(lifecycleEvent -> { +client.lifecycle().subscribe(lifecycleEvent -> { switch (lifecycleEvent.getType()) { - case OPENED: Log.d(TAG, "Stomp connection opened"); break; - - case ERROR: - Log.e(TAG, "Error", lifecycleEvent.getException()); - break; - case CLOSED: Log.d(TAG, "Stomp connection closed"); break; + case ERROR: + Log.e(TAG, "Stomp connection error", lifecycleEvent.getException()); + break; } }); ``` -Library support just send & receive messages. ACK messages, transactions not implemented yet. +**Custom client** + +You can use a custom HttpClient (for example, if you want to allow untrusted HTTPS) using the four-argument overload of Stomp.over, like so: + +``` java +client = Stomp.over(WebSocket.class, address, null, unsafeClient); +``` + +Yes, it's safe to pass `null` for either (or both) of the last two arguments. That's exactly what the shorter overloads do. + +**Support** + +Right now, the library only supports sending and receiving messages. ACK messages and transactions are not implemented yet. + +**Additional Reading** + +- [Spring + Websockets + STOMP](https://spring.io/guides/gs/messaging-stomp-websocket/) +- [STOMP Protocol](http://stomp.github.io/) +- [Spring detailed documentation](https://docs.spring.io/spring/docs/current/spring-framework-reference/html/websocket.html#websocket-stomp) +- [Create an Unsafe OkHttp Client](https://gist.github.com/grow2014/b6969d8f0cfc0f0a1b2bf12f84973dec) + - (for developing with invalid SSL certs) \ No newline at end of file diff --git a/lib/build.gradle b/lib/build.gradle index 4401dad..141d5c5 100644 --- a/lib/build.gradle +++ b/lib/build.gradle @@ -33,8 +33,8 @@ dependencies { testCompile 'junit:junit:4.12' compile 'io.reactivex:rxjava:1.3.0' // Supported transports - provided "org.java-websocket:java-websocket:1.3.2" - provided 'com.squareup.okhttp3:okhttp:3.8.1' + compile 'org.java-websocket:java-websocket:1.3.2' + compile 'com.squareup.okhttp3:okhttp:3.8.1' } task sourcesJar(type: Jar) { From 98e578a704ce7bebcffcb85b4206d22032acd459 Mon Sep 17 00:00:00 2001 From: Forrest Hopkins Date: Wed, 2 Aug 2017 16:58:23 -0700 Subject: [PATCH 04/33] Added disconnect() to interface, cleaned up some Rx methods --- .../stomp/ConnectionProvider.java | 7 +++ .../stomp/OkHttpConnectionProvider.java | 47 +++++++++++++++++-- .../stomp/WebSocketsConnectionProvider.java | 9 ++++ .../stomp/client/StompClient.java | 14 +++--- 4 files changed, 66 insertions(+), 11 deletions(-) diff --git a/lib/src/main/java/ua/naiksoftware/stomp/ConnectionProvider.java b/lib/src/main/java/ua/naiksoftware/stomp/ConnectionProvider.java index bf39020..28a0460 100644 --- a/lib/src/main/java/ua/naiksoftware/stomp/ConnectionProvider.java +++ b/lib/src/main/java/ua/naiksoftware/stomp/ConnectionProvider.java @@ -1,5 +1,6 @@ package ua.naiksoftware.stomp; +import rx.Completable; import rx.Observable; /** @@ -23,4 +24,10 @@ public interface ConnectionProvider { * Subscribe this for receive #LifecycleEvent events */ Observable getLifecycleReceiver(); + + /** + * Disconnects from server. This is basically a Callable. + * Automatically emits Lifecycle.CLOSE + */ + Completable disconnect(); } diff --git a/lib/src/main/java/ua/naiksoftware/stomp/OkHttpConnectionProvider.java b/lib/src/main/java/ua/naiksoftware/stomp/OkHttpConnectionProvider.java index 7e9e7e9..0625ef0 100644 --- a/lib/src/main/java/ua/naiksoftware/stomp/OkHttpConnectionProvider.java +++ b/lib/src/main/java/ua/naiksoftware/stomp/OkHttpConnectionProvider.java @@ -16,8 +16,10 @@ import okhttp3.WebSocket; import okhttp3.WebSocketListener; import okio.ByteString; +import rx.Completable; import rx.Observable; import rx.Subscriber; +import rx.subjects.PublishSubject; /* package */ class OkHttpConnectionProvider implements ConnectionProvider { @@ -28,7 +30,9 @@ private final OkHttpClient mOkHttpClient; private final List> mLifecycleSubscribers; + private final PublishSubject mLifecycleStream; private final List> mMessagesSubscribers; + private final PublishSubject mMessagesStream; private WebSocket openedSocked; @@ -39,10 +43,20 @@ mLifecycleSubscribers = new ArrayList<>(); mMessagesSubscribers = new ArrayList<>(); mOkHttpClient = okHttpClient; + + mLifecycleStream = PublishSubject.create(); + mMessagesStream = PublishSubject.create(); } @Override public Observable messages() { + createWebSocketConnection(); + // By using Subjects, we can leave the tracking of Subscribers to Rx. + // Additionally, server disconnection is now handled manually + // (instead of trying to support disconnecting just by unsubscribing) + return mMessagesStream; + + /* Observable observable = Observable.create(subscriber -> { mMessagesSubscribers.add(subscriber); @@ -61,6 +75,14 @@ public Observable messages() { createWebSocketConnection(); return observable; + */ + } + + // this used to be done automatically whenever the "subscriber list" was empty + // this way is more discrete + @Override + public Completable disconnect() { + return Completable.fromAction(() -> openedSocked.close(1000, "")); } private void createWebSocketConnection() { @@ -71,9 +93,9 @@ private void createWebSocketConnection() { Request.Builder requestBuilder = new Request.Builder() .url(mUri); - + addConnectionHeadersToBuilder(requestBuilder, mConnectHttpHeaders); - + openedSocked = mOkHttpClient.newWebSocket(requestBuilder.build(), new WebSocketListener() { @Override @@ -113,19 +135,24 @@ public void onFailure(WebSocket webSocket, Throwable t, Response response) { @Override public Observable send(String stompMessage) { - return Observable.create(subscriber -> { + // .create(onSubscribe) is deprecated because it's unsafe + return Observable.fromCallable(() -> { if (openedSocked == null) { - subscriber.onError(new IllegalStateException("Not connected yet")); + throw new IllegalStateException("Not connected yet"); } else { Log.d(TAG, "Send STOMP message: " + stompMessage); openedSocked.send(stompMessage); - subscriber.onCompleted(); + return null; } }); } @Override public Observable getLifecycleReceiver() { + // Once again, opting to leave Subscriber tracking to Rx + return mLifecycleStream; + + /* return Observable.create(subscriber -> { mLifecycleSubscribers.add(subscriber); @@ -135,6 +162,7 @@ public Observable getLifecycleReceiver() { if (iterator.next().isUnsubscribed()) iterator.remove(); } }); + */ } private TreeMap headersAsMap(Response response) { @@ -154,15 +182,24 @@ private void addConnectionHeadersToBuilder(Request.Builder requestBuilder, Map subscriber : mLifecycleSubscribers) { subscriber.onNext(lifecycleEvent); } + */ } private void emitMessage(String stompMessage) { Log.d(TAG, "Emit STOMP message: " + stompMessage); + mMessagesStream.onNext(stompMessage); + + /* for (Subscriber subscriber : mMessagesSubscribers) { subscriber.onNext(stompMessage); } + */ } } diff --git a/lib/src/main/java/ua/naiksoftware/stomp/WebSocketsConnectionProvider.java b/lib/src/main/java/ua/naiksoftware/stomp/WebSocketsConnectionProvider.java index 9c23c65..d5bc991 100644 --- a/lib/src/main/java/ua/naiksoftware/stomp/WebSocketsConnectionProvider.java +++ b/lib/src/main/java/ua/naiksoftware/stomp/WebSocketsConnectionProvider.java @@ -20,6 +20,7 @@ import javax.net.ssl.SSLContext; import javax.net.ssl.SSLSocketFactory; +import rx.Completable; import rx.Observable; import rx.Subscriber; @@ -145,6 +146,14 @@ public Observable send(String stompMessage) { }); } + // Just to appease javac + @Override + public Completable disconnect() { + return Completable.fromAction(() -> { + throw new UnsupportedOperationException("JAVA WEB SOCKETS ARE NOT YET SUPPORTED IN THIS VERSION"); + }); + } + private void emitLifecycleEvent(LifecycleEvent lifecycleEvent) { Log.d(TAG, "Emit lifecycle event: " + lifecycleEvent.getType().name()); for (Subscriber subscriber : mLifecycleSubscribers) { diff --git a/lib/src/main/java/ua/naiksoftware/stomp/client/StompClient.java b/lib/src/main/java/ua/naiksoftware/stomp/client/StompClient.java index cb84462..187119d 100644 --- a/lib/src/main/java/ua/naiksoftware/stomp/client/StompClient.java +++ b/lib/src/main/java/ua/naiksoftware/stomp/client/StompClient.java @@ -72,7 +72,8 @@ public void connect(List _headers) { public void connect(List _headers, boolean reconnect) { if (reconnect) disconnect(); if (mConnected) return; - mConnectionProvider.getLifecycleReceiver() + // why wasn't this DRY before? + lifecycle() .subscribe(lifecycleEvent -> { switch (lifecycleEvent.getType()) { case OPENED: @@ -128,9 +129,10 @@ public Observable send(String destination, String data) { public Observable send(StompMessage stompMessage) { Observable observable = mConnectionProvider.send(stompMessage.compile()); if (!mConnected) { - ConnectableObservable deffered = observable.publish(); - mWaitConnectionObservables.add(deffered); - return deffered; + // my inner grammar nazi + ConnectableObservable deferred = observable.publish(); + mWaitConnectionObservables.add(deferred); + return deferred; } else { return observable; } @@ -153,8 +155,8 @@ public Observable lifecycle() { } public void disconnect() { - if (mMessagesSubscription != null) mMessagesSubscription.unsubscribe(); - mConnected = false; + // the other things are now taken care of downstream + mConnectionProvider.disconnect().subscribe(); } public Observable topic(String destinationPath) { From 24f989f5375a9f2948a4e3097c4f44710ef19362 Mon Sep 17 00:00:00 2001 From: Forrest Hopkins Date: Thu, 3 Aug 2017 09:21:42 -0700 Subject: [PATCH 05/33] Cleaning up, restoring JWS support --- README.md | 2 +- .../stomp/ConnectionProvider.java | 2 +- .../stomp/OkHttpConnectionProvider.java | 70 +---------------- .../stomp/WebSocketsConnectionProvider.java | 76 ++++++------------- .../stomp/client/StompClient.java | 5 +- 5 files changed, 30 insertions(+), 125 deletions(-) diff --git a/README.md b/README.md index 0f6ab24..9402e78 100644 --- a/README.md +++ b/README.md @@ -146,7 +146,7 @@ Yes, it's safe to pass `null` for either (or both) of the last two arguments. Th Right now, the library only supports sending and receiving messages. ACK messages and transactions are not implemented yet. -**Additional Reading** +## Additional Reading - [Spring + Websockets + STOMP](https://spring.io/guides/gs/messaging-stomp-websocket/) - [STOMP Protocol](http://stomp.github.io/) diff --git a/lib/src/main/java/ua/naiksoftware/stomp/ConnectionProvider.java b/lib/src/main/java/ua/naiksoftware/stomp/ConnectionProvider.java index 28a0460..7866527 100644 --- a/lib/src/main/java/ua/naiksoftware/stomp/ConnectionProvider.java +++ b/lib/src/main/java/ua/naiksoftware/stomp/ConnectionProvider.java @@ -18,7 +18,7 @@ public interface ConnectionProvider { * onError if not connected or error detected will be called, or onCompleted id sending started * TODO: send messages with ACK */ - Observable send(String stompMessage); + Completable send(String stompMessage); /** * Subscribe this for receive #LifecycleEvent events diff --git a/lib/src/main/java/ua/naiksoftware/stomp/OkHttpConnectionProvider.java b/lib/src/main/java/ua/naiksoftware/stomp/OkHttpConnectionProvider.java index 0625ef0..ffa79cc 100644 --- a/lib/src/main/java/ua/naiksoftware/stomp/OkHttpConnectionProvider.java +++ b/lib/src/main/java/ua/naiksoftware/stomp/OkHttpConnectionProvider.java @@ -2,10 +2,7 @@ import android.util.Log; -import java.util.ArrayList; import java.util.HashMap; -import java.util.Iterator; -import java.util.List; import java.util.Map; import java.util.TreeMap; @@ -18,10 +15,9 @@ import okio.ByteString; import rx.Completable; import rx.Observable; -import rx.Subscriber; import rx.subjects.PublishSubject; -/* package */ class OkHttpConnectionProvider implements ConnectionProvider { +class OkHttpConnectionProvider implements ConnectionProvider { private static final String TAG = WebSocketsConnectionProvider.class.getSimpleName(); @@ -29,19 +25,14 @@ private final Map mConnectHttpHeaders; private final OkHttpClient mOkHttpClient; - private final List> mLifecycleSubscribers; private final PublishSubject mLifecycleStream; - private final List> mMessagesSubscribers; private final PublishSubject mMessagesStream; private WebSocket openedSocked; - - /* package */ OkHttpConnectionProvider(String uri, Map connectHttpHeaders, OkHttpClient okHttpClient) { + OkHttpConnectionProvider(String uri, Map connectHttpHeaders, OkHttpClient okHttpClient) { mUri = uri; mConnectHttpHeaders = connectHttpHeaders != null ? connectHttpHeaders : new HashMap<>(); - mLifecycleSubscribers = new ArrayList<>(); - mMessagesSubscribers = new ArrayList<>(); mOkHttpClient = okHttpClient; mLifecycleStream = PublishSubject.create(); @@ -51,35 +42,9 @@ @Override public Observable messages() { createWebSocketConnection(); - // By using Subjects, we can leave the tracking of Subscribers to Rx. - // Additionally, server disconnection is now handled manually - // (instead of trying to support disconnecting just by unsubscribing) return mMessagesStream; - - /* - Observable observable = Observable.create(subscriber -> { - mMessagesSubscribers.add(subscriber); - - }).doOnUnsubscribe(() -> { - Iterator> iterator = mMessagesSubscribers.iterator(); - while (iterator.hasNext()) { - if (iterator.next().isUnsubscribed()) iterator.remove(); - } - - if (mMessagesSubscribers.size() < 1) { - Log.d(TAG, "Close web socket connection now in thread " + Thread.currentThread()); - openedSocked.close(1000, ""); - openedSocked = null; - } - }); - - createWebSocketConnection(); - return observable; - */ } - // this used to be done automatically whenever the "subscriber list" was empty - // this way is more discrete @Override public Completable disconnect() { return Completable.fromAction(() -> openedSocked.close(1000, "")); @@ -134,9 +99,8 @@ public void onFailure(WebSocket webSocket, Throwable t, Response response) { } @Override - public Observable send(String stompMessage) { - // .create(onSubscribe) is deprecated because it's unsafe - return Observable.fromCallable(() -> { + public Completable send(String stompMessage) { + return Completable.fromCallable(() -> { if (openedSocked == null) { throw new IllegalStateException("Not connected yet"); } else { @@ -149,20 +113,7 @@ public Observable send(String stompMessage) { @Override public Observable getLifecycleReceiver() { - // Once again, opting to leave Subscriber tracking to Rx return mLifecycleStream; - - /* - return Observable.create(subscriber -> { - mLifecycleSubscribers.add(subscriber); - - }).doOnUnsubscribe(() -> { - Iterator> iterator = mLifecycleSubscribers.iterator(); - while (iterator.hasNext()) { - if (iterator.next().isUnsubscribed()) iterator.remove(); - } - }); - */ } private TreeMap headersAsMap(Response response) { @@ -182,24 +133,11 @@ private void addConnectionHeadersToBuilder(Request.Builder requestBuilder, Map subscriber : mLifecycleSubscribers) { - subscriber.onNext(lifecycleEvent); - } - */ } private void emitMessage(String stompMessage) { Log.d(TAG, "Emit STOMP message: " + stompMessage); mMessagesStream.onNext(stompMessage); - - /* - for (Subscriber subscriber : mMessagesSubscribers) { - subscriber.onNext(stompMessage); - } - */ } } diff --git a/lib/src/main/java/ua/naiksoftware/stomp/WebSocketsConnectionProvider.java b/lib/src/main/java/ua/naiksoftware/stomp/WebSocketsConnectionProvider.java index d5bc991..4c6f94f 100644 --- a/lib/src/main/java/ua/naiksoftware/stomp/WebSocketsConnectionProvider.java +++ b/lib/src/main/java/ua/naiksoftware/stomp/WebSocketsConnectionProvider.java @@ -10,10 +10,8 @@ import org.java_websocket.handshake.ServerHandshake; import java.net.URI; -import java.util.ArrayList; import java.util.HashMap; import java.util.Iterator; -import java.util.List; import java.util.Map; import java.util.TreeMap; @@ -22,55 +20,47 @@ import rx.Completable; import rx.Observable; -import rx.Subscriber; +import rx.subjects.PublishSubject; /** * Created by naik on 05.05.16. */ -/* package */ class WebSocketsConnectionProvider implements ConnectionProvider { + +class WebSocketsConnectionProvider implements ConnectionProvider { private static final String TAG = WebSocketsConnectionProvider.class.getSimpleName(); private final String mUri; private final Map mConnectHttpHeaders; - private final List> mLifecycleSubscribers; - private final List> mMessagesSubscribers; - private WebSocketClient mWebSocketClient; private boolean haveConnection; private TreeMap mServerHandshakeHeaders; + private final PublishSubject mLifecycleStream; + private final PublishSubject mMessagesStream; + /** * Support UIR scheme ws://host:port/path * @param connectHttpHeaders may be null */ - /* package */ WebSocketsConnectionProvider(String uri, Map connectHttpHeaders) { + WebSocketsConnectionProvider(String uri, Map connectHttpHeaders) { mUri = uri; mConnectHttpHeaders = connectHttpHeaders != null ? connectHttpHeaders : new HashMap<>(); - mLifecycleSubscribers = new ArrayList<>(); - mMessagesSubscribers = new ArrayList<>(); + + mLifecycleStream = PublishSubject.create(); + mMessagesStream = PublishSubject.create(); } @Override public Observable messages() { - Observable observable = Observable.create(subscriber -> { - mMessagesSubscribers.add(subscriber); - - }).doOnUnsubscribe(() -> { - Iterator> iterator = mMessagesSubscribers.iterator(); - while (iterator.hasNext()) { - if (iterator.next().isUnsubscribed()) iterator.remove(); - } - - if (mMessagesSubscribers.size() < 1) { - Log.d(TAG, "Close web socket connection now in thread " + Thread.currentThread()); - mWebSocketClient.close(); - } - }); - createWebSocketConnection(); - return observable; + return mMessagesStream; + } + + @Override + public Completable disconnect() { + return Completable.fromAction(() -> mWebSocketClient.close()); } private void createWebSocketConnection() { @@ -134,50 +124,30 @@ public void onError(Exception ex) { } @Override - public Observable send(String stompMessage) { - return Observable.create(subscriber -> { + public Completable send(String stompMessage) { + return Completable.fromCallable(() -> { if (mWebSocketClient == null) { - subscriber.onError(new IllegalStateException("Not connected yet")); + throw new IllegalStateException("Not connected yet"); } else { Log.d(TAG, "Send STOMP message: " + stompMessage); mWebSocketClient.send(stompMessage); - subscriber.onCompleted(); + return null; } }); } - // Just to appease javac - @Override - public Completable disconnect() { - return Completable.fromAction(() -> { - throw new UnsupportedOperationException("JAVA WEB SOCKETS ARE NOT YET SUPPORTED IN THIS VERSION"); - }); - } - private void emitLifecycleEvent(LifecycleEvent lifecycleEvent) { Log.d(TAG, "Emit lifecycle event: " + lifecycleEvent.getType().name()); - for (Subscriber subscriber : mLifecycleSubscribers) { - subscriber.onNext(lifecycleEvent); - } + mLifecycleStream.onNext(lifecycleEvent); } private void emitMessage(String stompMessage) { Log.d(TAG, "Emit STOMP message: " + stompMessage); - for (Subscriber subscriber : mMessagesSubscribers) { - subscriber.onNext(stompMessage); - } + mMessagesStream.onNext(stompMessage); } @Override public Observable getLifecycleReceiver() { - return Observable.create(subscriber -> { - mLifecycleSubscribers.add(subscriber); - - }).doOnUnsubscribe(() -> { - Iterator> iterator = mLifecycleSubscribers.iterator(); - while (iterator.hasNext()) { - if (iterator.next().isUnsubscribed()) iterator.remove(); - } - }); + return mLifecycleStream; } } diff --git a/lib/src/main/java/ua/naiksoftware/stomp/client/StompClient.java b/lib/src/main/java/ua/naiksoftware/stomp/client/StompClient.java index 187119d..bef1299 100644 --- a/lib/src/main/java/ua/naiksoftware/stomp/client/StompClient.java +++ b/lib/src/main/java/ua/naiksoftware/stomp/client/StompClient.java @@ -72,7 +72,6 @@ public void connect(List _headers) { public void connect(List _headers, boolean reconnect) { if (reconnect) disconnect(); if (mConnected) return; - // why wasn't this DRY before? lifecycle() .subscribe(lifecycleEvent -> { switch (lifecycleEvent.getType()) { @@ -127,9 +126,8 @@ public Observable send(String destination, String data) { } public Observable send(StompMessage stompMessage) { - Observable observable = mConnectionProvider.send(stompMessage.compile()); + Observable observable = mConnectionProvider.send(stompMessage.compile()).toObservable(); if (!mConnected) { - // my inner grammar nazi ConnectableObservable deferred = observable.publish(); mWaitConnectionObservables.add(deferred); return deferred; @@ -155,7 +153,6 @@ public Observable lifecycle() { } public void disconnect() { - // the other things are now taken care of downstream mConnectionProvider.disconnect().subscribe(); } From a5c7586a9824aa1209f1921ab7e0af5799ae667b Mon Sep 17 00:00:00 2001 From: forresthopkinsa Date: Fri, 4 Aug 2017 09:16:14 -0700 Subject: [PATCH 06/33] Working on StompClient. This version is not stable --- README.md | 75 ++++++++++- .../stomp/client/StompClient.java | 119 +++++++++++++----- 2 files changed, 158 insertions(+), 36 deletions(-) diff --git a/README.md b/README.md index 9402e78..5dc7c79 100644 --- a/README.md +++ b/README.md @@ -134,18 +134,89 @@ client.lifecycle().subscribe(lifecycleEvent -> { **Custom client** -You can use a custom HttpClient (for example, if you want to allow untrusted HTTPS) using the four-argument overload of Stomp.over, like so: +You can use a custom OkHttpClient (for example, [if you want to allow untrusted HTTPS](getUnsafeOkHttpClient())) using the four-argument overload of Stomp.over, like so: ``` java -client = Stomp.over(WebSocket.class, address, null, unsafeClient); +client = Stomp.over(WebSocket.class, address, null, getUnsafeOkHttpClient()); ``` Yes, it's safe to pass `null` for either (or both) of the last two arguments. That's exactly what the shorter overloads do. +Note: This method is only supported using OkHttp, not JWS. + **Support** Right now, the library only supports sending and receiving messages. ACK messages and transactions are not implemented yet. +## Changes in this fork + +**Build changes** + +The upstream master is based on Retrolambda. This version is based on Native Java 8 compilation, +although it should also work with Jack. It will *not* work with Retrolambda. + +**Code changes** + +These are the possible changes you need to make to your code for this branch, if you were using the upstream before: + +- Disconnecting is now mandatory + - Previously, the socket would automatically disconnect if nothing was listening to it + - Now, it will not disconnect unless you explicitly run client.disconnect() +- send() now returns a Completable + - Previously, it returned an Observable\ + - Now, it and all its overloads return a Completable, which is functionally about the same + - **However**, Completable does not inherit from Observable, so there are a couple differences: + - Of course, if you were storing send() in an Observable, you'll have to change that + - Additionally, if you were inheriting onNext before, you're going to have to adjust it: + - Old way, deprecated: + ``` java + client.send("/app/hello", "world").subscribe( + aVoid -> Log.d(TAG, "Sent data!"), + error -> Log.e(TAG, "Encountered error while sending data!", error) + ); + ``` + !! -v: + ``` java + client.send("/app/hello", "world").subscribe(new Subscriber() { + @Override + public void onNext(Void aVoid) { + Log.d(TAG, "Sent data!"); + } + @Override + public void onError(Throwable error) { + Log.e(TAG, "Encountered error while sending data!", error); + } + @Override + public void onCompleted() {} // useless + }); + ``` + - New way of handling it: + ``` java + client.send("/app/hello", "world").subscribe( + () -> Log.d(TAG, "Sent data!"), + error -> Log.e(TAG, "Encountered error while sending data!", error) + ); + ``` + Or if you just can't get enough of anonymous classes: + ``` java + client.send("/app/hello", "world").subscribe(new Subscriber() { + @Override + public void onCompleted() { + Log.d(TAG, "Sent data!"); + } + @Override + public void onError(Throwable error) { + Log.e(TAG, "Encountered error while sending data!", error); + } + @Override + public void onNext(Object o) {} // useless + }); + ``` + - Be sure to implement this change, because the IDE might not catch the error. +- Passing null as the topic path now throws an exception + - Previously, it was supposed to silently fail, although it would probably hit a NPE first (untested) + - Now it throws an IllegalArgumentException + ## Additional Reading - [Spring + Websockets + STOMP](https://spring.io/guides/gs/messaging-stomp-websocket/) diff --git a/lib/src/main/java/ua/naiksoftware/stomp/client/StompClient.java b/lib/src/main/java/ua/naiksoftware/stomp/client/StompClient.java index bef1299..1b20093 100644 --- a/lib/src/main/java/ua/naiksoftware/stomp/client/StompClient.java +++ b/lib/src/main/java/ua/naiksoftware/stomp/client/StompClient.java @@ -13,10 +13,12 @@ import java.util.UUID; import java.util.concurrent.CopyOnWriteArrayList; +import rx.Completable; import rx.Observable; import rx.Subscriber; import rx.Subscription; import rx.observables.ConnectableObservable; +import rx.subjects.PublishSubject; import ua.naiksoftware.stomp.ConnectionProvider; import ua.naiksoftware.stomp.LifecycleEvent; import ua.naiksoftware.stomp.StompHeader; @@ -31,17 +33,22 @@ public class StompClient { public static final String SUPPORTED_VERSIONS = "1.1,1.0"; public static final String DEFAULT_ACK = "auto"; + /* private Subscription mMessagesSubscription; private Map>> mSubscribers = new HashMap<>(); - private List> mWaitConnectionObservables; + */ + private List mWaitConnectionCompletables; private final ConnectionProvider mConnectionProvider; private HashMap mTopics; private boolean mConnected; private boolean isConnecting; + private PublishSubject mMessageStream; + public StompClient(ConnectionProvider connectionProvider) { mConnectionProvider = connectionProvider; - mWaitConnectionObservables = new CopyOnWriteArrayList<>(); + mWaitConnectionCompletables = new CopyOnWriteArrayList<>(); + mMessageStream = PublishSubject.create(); } /** @@ -96,46 +103,43 @@ public void connect(List _headers, boolean reconnect) { }); isConnecting = true; - mMessagesSubscription = mConnectionProvider.messages() + mConnectionProvider.messages() .map(StompMessage::from) + .doOnNext(this::callSubscribers) + .filter(msg -> msg.getStompCommand().equals(StompCommand.CONNECTED)) .subscribe(stompMessage -> { - if (stompMessage.getStompCommand().equals(StompCommand.CONNECTED)) { - mConnected = true; - isConnecting = false; - for (ConnectableObservable observable : mWaitConnectionObservables) { - observable.connect(); - } - mWaitConnectionObservables.clear(); + mConnected = true; + isConnecting = false; + for (Completable completable : mWaitConnectionCompletables) { + completable.subscribe(); } - callSubscribers(stompMessage); + mWaitConnectionCompletables.clear(); }); } - public Observable send(String destination) { + public Completable send(String destination) { return send(new StompMessage( StompCommand.SEND, Collections.singletonList(new StompHeader(StompHeader.DESTINATION, destination)), null)); } - public Observable send(String destination, String data) { + public Completable send(String destination, String data) { return send(new StompMessage( StompCommand.SEND, Collections.singletonList(new StompHeader(StompHeader.DESTINATION, destination)), data)); } - public Observable send(StompMessage stompMessage) { - Observable observable = mConnectionProvider.send(stompMessage.compile()).toObservable(); + public Completable send(StompMessage stompMessage) { + Completable completable = mConnectionProvider.send(stompMessage.compile()); if (!mConnected) { - ConnectableObservable deferred = observable.publish(); - mWaitConnectionObservables.add(deferred); - return deferred; - } else { - return observable; + mWaitConnectionCompletables.add(completable); } + return completable; } + /* private void callSubscribers(StompMessage stompMessage) { String messageDestination = stompMessage.findHeader(StompHeader.DESTINATION); for (String dest : mSubscribers.keySet()) { @@ -147,6 +151,11 @@ private void callSubscribers(StompMessage stompMessage) { } } } + */ + + private void callSubscribers(StompMessage stompMessage) { + mMessageStream.onNext(stompMessage); + } public Observable lifecycle() { return mConnectionProvider.getLifecycleReceiver(); @@ -160,28 +169,65 @@ public Observable topic(String destinationPath) { return topic(destinationPath, null); } + public Observable topic(String destPath, List headerList) { + Observable ret; + + if (destPath == null) + ret = Observable.error(new IllegalArgumentException("Topic path cannot be null")); + else + ret = mMessageStream + .filter(msg -> destPath.equals(msg.findHeader(StompHeader.DESTINATION))) + .doOnSubscribe(() -> subscribePath(destPath, headerList)); + // still need to figure out how to do the unsubscribes reactively... more difficult than it sounds + return ret; + } + + /* public Observable topic(String destinationPath, List headerList) { + // basically: + // on SUBSCRIBE, add the observer to the Set in the mSubscribers map that's associated with the specified topic, + // and send a subscribe message IF WE HAVEN'T ALREADY SUBSCRIBED TO THE TOPIC + // + // on UNSUBSCRIBE, remove unsubscribed observers, and remove unobserved topics + + // on observer subscribe... return Observable.create(subscriber -> { + // get list of other subscribers to topic Set> subscribersSet = mSubscribers.get(destinationPath); + // if there are no other subscribers on topic... if (subscribersSet == null) { + // create new subscriber list, subscribersSet = new HashSet<>(); + // and add the list to the map mSubscribers.put(destinationPath, subscribersSet); + // send SUBSCRIBE message and add topic to mTopics subscribePath(destinationPath, headerList).subscribe(); } + // finally, now that we know that there is a list for this topic, add observer to it subscribersSet.add(subscriber); }).doOnUnsubscribe(() -> { + // on unsubscribe... Iterator mapIterator = mSubscribers.keySet().iterator(); + // for each topic in the map, while (mapIterator.hasNext()) { + // get topic path String destinationUrl = mapIterator.next(); + // get observers subscribed to this topic Set> set = mSubscribers.get(destinationUrl); Iterator> setIterator = set.iterator(); + // for each observer subscribed to this topic, while (setIterator.hasNext()) { Subscriber subscriber = setIterator.next(); + // if observer is no longer subscribed, if (subscriber.isUnsubscribed()) { + // remove it from the set setIterator.remove(); + // if there are no observers subscribed to this topic anymore... if (set.size() < 1) { + // remote the set from the map mapIterator.remove(); + // send UNSUBSCRIBE message unsubscribePath(destinationUrl).subscribe(); } } @@ -189,24 +235,29 @@ public Observable topic(String destinationPath, List } }); } + */ + + private Completable subscribePath(String destinationPath, List headerList) { + String topicId = UUID.randomUUID().toString(); - private Observable subscribePath(String destinationPath, List headerList) { - if (destinationPath == null) return Observable.empty(); - String topicId = UUID.randomUUID().toString(); + if (mTopics == null) mTopics = new HashMap<>(); - if (mTopics == null) mTopics = new HashMap<>(); - mTopics.put(destinationPath, topicId); - List headers = new ArrayList<>(); - headers.add(new StompHeader(StompHeader.ID, topicId)); - headers.add(new StompHeader(StompHeader.DESTINATION, destinationPath)); - headers.add(new StompHeader(StompHeader.ACK, DEFAULT_ACK)); - if (headerList != null) headers.addAll(headerList); - return send(new StompMessage(StompCommand.SUBSCRIBE, - headers, null)); - } + // Only continue if we don't already have a subscription to the topic + if (mTopics.containsKey(destinationPath)) + return Completable.complete(); + + mTopics.put(destinationPath, topicId); + List headers = new ArrayList<>(); + headers.add(new StompHeader(StompHeader.ID, topicId)); + headers.add(new StompHeader(StompHeader.DESTINATION, destinationPath)); + headers.add(new StompHeader(StompHeader.ACK, DEFAULT_ACK)); + if (headerList != null) headers.addAll(headerList); + return send(new StompMessage(StompCommand.SUBSCRIBE, + headers, null)); + } - private Observable unsubscribePath(String dest) { + private Completable unsubscribePath(String dest) { String topicId = mTopics.get(dest); Log.d(TAG, "Unsubscribe path: " + dest + " id: " + topicId); From da0be9c4d64e1f0862e734c6be75b367c6f385cf Mon Sep 17 00:00:00 2001 From: forresthopkinsa Date: Mon, 7 Aug 2017 16:42:07 -0700 Subject: [PATCH 07/33] Successfully replaced the old mechanism for caching pre-connect sends. There is a lot of debug logging in this build, but everything now seems to be working correctly. --- lib/build.gradle | 2 ++ .../stomp/OkHttpConnectionProvider.java | 2 +- .../stomp/client/StompClient.java | 28 +++++++++++++------ 3 files changed, 22 insertions(+), 10 deletions(-) diff --git a/lib/build.gradle b/lib/build.gradle index 141d5c5..4d3f9a5 100644 --- a/lib/build.gradle +++ b/lib/build.gradle @@ -32,6 +32,8 @@ dependencies { compile fileTree(include: ['*.jar'], dir: 'libs') testCompile 'junit:junit:4.12' compile 'io.reactivex:rxjava:1.3.0' + compile 'net.sourceforge.streamsupport:streamsupport:1.5.5' + compile 'net.sourceforge.streamsupport:streamsupport-cfuture:1.5.5' // Supported transports compile 'org.java-websocket:java-websocket:1.3.2' compile 'com.squareup.okhttp3:okhttp:3.8.1' diff --git a/lib/src/main/java/ua/naiksoftware/stomp/OkHttpConnectionProvider.java b/lib/src/main/java/ua/naiksoftware/stomp/OkHttpConnectionProvider.java index ffa79cc..ef014eb 100644 --- a/lib/src/main/java/ua/naiksoftware/stomp/OkHttpConnectionProvider.java +++ b/lib/src/main/java/ua/naiksoftware/stomp/OkHttpConnectionProvider.java @@ -19,7 +19,7 @@ class OkHttpConnectionProvider implements ConnectionProvider { - private static final String TAG = WebSocketsConnectionProvider.class.getSimpleName(); + private static final String TAG = OkHttpConnectionProvider.class.getSimpleName(); private final String mUri; private final Map mConnectHttpHeaders; diff --git a/lib/src/main/java/ua/naiksoftware/stomp/client/StompClient.java b/lib/src/main/java/ua/naiksoftware/stomp/client/StompClient.java index 1b20093..0e1ff26 100644 --- a/lib/src/main/java/ua/naiksoftware/stomp/client/StompClient.java +++ b/lib/src/main/java/ua/naiksoftware/stomp/client/StompClient.java @@ -13,11 +13,13 @@ import java.util.UUID; import java.util.concurrent.CopyOnWriteArrayList; +import java8.util.concurrent.CompletableFuture; import rx.Completable; import rx.Observable; import rx.Subscriber; import rx.Subscription; import rx.observables.ConnectableObservable; +import rx.schedulers.Schedulers; import rx.subjects.PublishSubject; import ua.naiksoftware.stomp.ConnectionProvider; import ua.naiksoftware.stomp.LifecycleEvent; @@ -37,18 +39,23 @@ public class StompClient { private Subscription mMessagesSubscription; private Map>> mSubscribers = new HashMap<>(); */ - private List mWaitConnectionCompletables; +// private List mWaitConnectionCompletables; private final ConnectionProvider mConnectionProvider; private HashMap mTopics; private boolean mConnected; private boolean isConnecting; private PublishSubject mMessageStream; + private CompletableFuture connectionStatus; + private Completable waitForConnect; public StompClient(ConnectionProvider connectionProvider) { mConnectionProvider = connectionProvider; - mWaitConnectionCompletables = new CopyOnWriteArrayList<>(); +// mWaitConnectionCompletables = new CopyOnWriteArrayList<>(); mMessageStream = PublishSubject.create(); + connectionStatus = new CompletableFuture<>(); + waitForConnect = Completable.fromFuture(connectionStatus).subscribeOn(Schedulers.newThread()); + waitForConnect.subscribe(() -> Log.d(TAG, "waitForConnect completed")); } /** @@ -87,7 +94,7 @@ public void connect(List _headers, boolean reconnect) { headers.add(new StompHeader(StompHeader.VERSION, SUPPORTED_VERSIONS)); if (_headers != null) headers.addAll(_headers); mConnectionProvider.send(new StompMessage(StompCommand.CONNECT, headers, null).compile()) - .subscribe(); + .subscribe(() -> Log.d(TAG, "CONNECT command sent!")); break; case CLOSED: @@ -110,18 +117,18 @@ public void connect(List _headers, boolean reconnect) { .subscribe(stompMessage -> { mConnected = true; isConnecting = false; + connectionStatus.complete(true); + /* for (Completable completable : mWaitConnectionCompletables) { completable.subscribe(); } mWaitConnectionCompletables.clear(); + */ }); } public Completable send(String destination) { - return send(new StompMessage( - StompCommand.SEND, - Collections.singletonList(new StompHeader(StompHeader.DESTINATION, destination)), - null)); + return send(destination, null); } public Completable send(String destination, String data) { @@ -133,10 +140,13 @@ public Completable send(String destination, String data) { public Completable send(StompMessage stompMessage) { Completable completable = mConnectionProvider.send(stompMessage.compile()); + /* if (!mConnected) { mWaitConnectionCompletables.add(completable); } - return completable; + */ + waitForConnect.subscribe(() -> Log.d(TAG, "SEND waitForConnect complete, continuing!")); + return completable.startWith(waitForConnect); } /* @@ -177,7 +187,7 @@ public Observable topic(String destPath, List headerL else ret = mMessageStream .filter(msg -> destPath.equals(msg.findHeader(StompHeader.DESTINATION))) - .doOnSubscribe(() -> subscribePath(destPath, headerList)); + .doOnSubscribe(() -> subscribePath(destPath, headerList).subscribe()); // still need to figure out how to do the unsubscribes reactively... more difficult than it sounds return ret; } From 8a4177054670d7eb43a40d21d312a0e1457e5edb Mon Sep 17 00:00:00 2001 From: forresthopkinsa Date: Tue, 8 Aug 2017 11:31:05 -0700 Subject: [PATCH 08/33] Now we have Topic Unsubscribe working as intended. Needs to be tested --- .../stomp/client/StompClient.java | 27 ++++++++++++------- 1 file changed, 17 insertions(+), 10 deletions(-) diff --git a/lib/src/main/java/ua/naiksoftware/stomp/client/StompClient.java b/lib/src/main/java/ua/naiksoftware/stomp/client/StompClient.java index 0e1ff26..00f1e29 100644 --- a/lib/src/main/java/ua/naiksoftware/stomp/client/StompClient.java +++ b/lib/src/main/java/ua/naiksoftware/stomp/client/StompClient.java @@ -48,6 +48,7 @@ public class StompClient { private PublishSubject mMessageStream; private CompletableFuture connectionStatus; private Completable waitForConnect; + private HashMap> msgStreams; public StompClient(ConnectionProvider connectionProvider) { mConnectionProvider = connectionProvider; @@ -56,6 +57,7 @@ public StompClient(ConnectionProvider connectionProvider) { connectionStatus = new CompletableFuture<>(); waitForConnect = Completable.fromFuture(connectionStatus).subscribeOn(Schedulers.newThread()); waitForConnect.subscribe(() -> Log.d(TAG, "waitForConnect completed")); + msgStreams = new HashMap<>(); } /** @@ -180,16 +182,17 @@ public Observable topic(String destinationPath) { } public Observable topic(String destPath, List headerList) { - Observable ret; - if (destPath == null) - ret = Observable.error(new IllegalArgumentException("Topic path cannot be null")); - else - ret = mMessageStream - .filter(msg -> destPath.equals(msg.findHeader(StompHeader.DESTINATION))) - .doOnSubscribe(() -> subscribePath(destPath, headerList).subscribe()); - // still need to figure out how to do the unsubscribes reactively... more difficult than it sounds - return ret; + return Observable.error(new IllegalArgumentException("Topic path cannot be null")); + else if (!msgStreams.containsKey(destPath)) + msgStreams.put(destPath, + mMessageStream + .filter(msg -> destPath.equals(msg.findHeader(StompHeader.DESTINATION))) + .doOnSubscribe(() -> subscribePath(destPath, headerList).subscribe()) + .doOnUnsubscribe(() -> unsubscribePath(destPath).subscribe()) + .share() + ); + return msgStreams.get(destPath); } /* @@ -253,8 +256,10 @@ private Completable subscribePath(String destinationPath, List head if (mTopics == null) mTopics = new HashMap<>(); // Only continue if we don't already have a subscription to the topic - if (mTopics.containsKey(destinationPath)) + if (mTopics.containsKey(destinationPath)) { + Log.d(TAG, "Attempted to subscribe to already-subscribed path!"); return Completable.complete(); + } mTopics.put(destinationPath, topicId); List headers = new ArrayList<>(); @@ -268,6 +273,8 @@ private Completable subscribePath(String destinationPath, List head private Completable unsubscribePath(String dest) { + msgStreams.remove(dest); + String topicId = mTopics.get(dest); Log.d(TAG, "Unsubscribe path: " + dest + " id: " + topicId); From 0e82ce91827586e05d1c6d2bbbdea378ac553d96 Mon Sep 17 00:00:00 2001 From: forresthopkinsa Date: Tue, 8 Aug 2017 12:35:03 -0700 Subject: [PATCH 09/33] Did some cleanup, created another ConnectionProvider abstraction --- .../stomp/AbstractConnectionProvider.java | 101 ++++++++++++++ .../stomp/OkHttpConnectionProvider.java | 50 ++----- .../java/ua/naiksoftware/stomp/Stomp.java | 4 +- .../stomp/WebSocketsConnectionProvider.java | 45 +------ .../stomp/client/StompClient.java | 123 ++---------------- 5 files changed, 132 insertions(+), 191 deletions(-) create mode 100644 lib/src/main/java/ua/naiksoftware/stomp/AbstractConnectionProvider.java diff --git a/lib/src/main/java/ua/naiksoftware/stomp/AbstractConnectionProvider.java b/lib/src/main/java/ua/naiksoftware/stomp/AbstractConnectionProvider.java new file mode 100644 index 0000000..3d4afd0 --- /dev/null +++ b/lib/src/main/java/ua/naiksoftware/stomp/AbstractConnectionProvider.java @@ -0,0 +1,101 @@ +package ua.naiksoftware.stomp; + +import android.util.Log; + +import rx.Completable; +import rx.Observable; +import rx.subjects.PublishSubject; + +/** + * Created by forresthopkinsa on 8/8/2017. + *

+ * Created because there was a lot of shared code between JWS and OkHttp connection providers. + */ + +abstract class AbstractConnectionProvider implements ConnectionProvider { + + private static final String TAG = AbstractConnectionProvider.class.getSimpleName(); + + private final PublishSubject mLifecycleStream; + private final PublishSubject mMessagesStream; + + AbstractConnectionProvider() { + mLifecycleStream = PublishSubject.create(); + mMessagesStream = PublishSubject.create(); + } + + @Override + public Observable messages() { + createWebSocketConnection(); + return mMessagesStream; + } + + /** + * Completable to close socket. + *

+ * For example: + *

+     * return Completable.fromAction(() -> webSocket.close());
+     * 
+ */ + @Override + public abstract Completable disconnect(); + + /** + * Most important method: connects to websocket and notifies program of messages. + *

+ * See implementations in OkHttpConnectionProvider and WebSocketsConnectionProvider. + */ + abstract void createWebSocketConnection(); + + @Override + public Completable send(String stompMessage) { + return Completable.fromCallable(() -> { + if (getSocket() == null) { + throw new IllegalStateException("Not connected yet"); + } else { + Log.d(TAG, "Send STOMP message: " + stompMessage); + bareSend(stompMessage); + return null; + } + }); + } + + /** + * Just a simple message send. + *

+ * For example: + *

+     * webSocket.send(stompMessage);
+     * 
+ * + * @param stompMessage message to send + */ + abstract void bareSend(String stompMessage); + + /** + * Get socket object. + * Used for null checking; this object is expected to be null when the connection is not yet established. + *

+ * For example: + *

+     * return webSocket;
+     * 
+ */ + abstract Object getSocket(); + + void emitLifecycleEvent(LifecycleEvent lifecycleEvent) { + Log.d(TAG, "Emit lifecycle event: " + lifecycleEvent.getType().name()); + mLifecycleStream.onNext(lifecycleEvent); + } + + void emitMessage(String stompMessage) { + Log.d(TAG, "Emit STOMP message: " + stompMessage); + mMessagesStream.onNext(stompMessage); + } + + @Override + public Observable getLifecycleReceiver() { + return mLifecycleStream; + } +} diff --git a/lib/src/main/java/ua/naiksoftware/stomp/OkHttpConnectionProvider.java b/lib/src/main/java/ua/naiksoftware/stomp/OkHttpConnectionProvider.java index ef014eb..1e23b7e 100644 --- a/lib/src/main/java/ua/naiksoftware/stomp/OkHttpConnectionProvider.java +++ b/lib/src/main/java/ua/naiksoftware/stomp/OkHttpConnectionProvider.java @@ -1,7 +1,5 @@ package ua.naiksoftware.stomp; -import android.util.Log; - import java.util.HashMap; import java.util.Map; import java.util.TreeMap; @@ -14,35 +12,20 @@ import okhttp3.WebSocketListener; import okio.ByteString; import rx.Completable; -import rx.Observable; -import rx.subjects.PublishSubject; - -class OkHttpConnectionProvider implements ConnectionProvider { - private static final String TAG = OkHttpConnectionProvider.class.getSimpleName(); +class OkHttpConnectionProvider extends AbstractConnectionProvider { private final String mUri; private final Map mConnectHttpHeaders; private final OkHttpClient mOkHttpClient; - private final PublishSubject mLifecycleStream; - private final PublishSubject mMessagesStream; - private WebSocket openedSocked; OkHttpConnectionProvider(String uri, Map connectHttpHeaders, OkHttpClient okHttpClient) { + super(); mUri = uri; mConnectHttpHeaders = connectHttpHeaders != null ? connectHttpHeaders : new HashMap<>(); mOkHttpClient = okHttpClient; - - mLifecycleStream = PublishSubject.create(); - mMessagesStream = PublishSubject.create(); - } - - @Override - public Observable messages() { - createWebSocketConnection(); - return mMessagesStream; } @Override @@ -50,7 +33,8 @@ public Completable disconnect() { return Completable.fromAction(() -> openedSocked.close(1000, "")); } - private void createWebSocketConnection() { + @Override + void createWebSocketConnection() { if (openedSocked != null) { throw new IllegalStateException("Already have connection to web socket"); @@ -99,21 +83,13 @@ public void onFailure(WebSocket webSocket, Throwable t, Response response) { } @Override - public Completable send(String stompMessage) { - return Completable.fromCallable(() -> { - if (openedSocked == null) { - throw new IllegalStateException("Not connected yet"); - } else { - Log.d(TAG, "Send STOMP message: " + stompMessage); - openedSocked.send(stompMessage); - return null; - } - }); + void bareSend(String stompMessage) { + openedSocked.send(stompMessage); } @Override - public Observable getLifecycleReceiver() { - return mLifecycleStream; + Object getSocket() { + return openedSocked; } private TreeMap headersAsMap(Response response) { @@ -130,14 +106,4 @@ private void addConnectionHeadersToBuilder(Request.Builder requestBuilder, Map mServerHandshakeHeaders; - private final PublishSubject mLifecycleStream; - private final PublishSubject mMessagesStream; - /** * Support UIR scheme ws://host:port/path * @param connectHttpHeaders may be null @@ -47,15 +42,6 @@ class WebSocketsConnectionProvider implements ConnectionProvider { WebSocketsConnectionProvider(String uri, Map connectHttpHeaders) { mUri = uri; mConnectHttpHeaders = connectHttpHeaders != null ? connectHttpHeaders : new HashMap<>(); - - mLifecycleStream = PublishSubject.create(); - mMessagesStream = PublishSubject.create(); - } - - @Override - public Observable messages() { - createWebSocketConnection(); - return mMessagesStream; } @Override @@ -63,7 +49,8 @@ public Completable disconnect() { return Completable.fromAction(() -> mWebSocketClient.close()); } - private void createWebSocketConnection() { + @Override + void createWebSocketConnection() { if (haveConnection) throw new IllegalStateException("Already have connection to web socket"); @@ -124,30 +111,12 @@ public void onError(Exception ex) { } @Override - public Completable send(String stompMessage) { - return Completable.fromCallable(() -> { - if (mWebSocketClient == null) { - throw new IllegalStateException("Not connected yet"); - } else { - Log.d(TAG, "Send STOMP message: " + stompMessage); - mWebSocketClient.send(stompMessage); - return null; - } - }); - } - - private void emitLifecycleEvent(LifecycleEvent lifecycleEvent) { - Log.d(TAG, "Emit lifecycle event: " + lifecycleEvent.getType().name()); - mLifecycleStream.onNext(lifecycleEvent); - } - - private void emitMessage(String stompMessage) { - Log.d(TAG, "Emit STOMP message: " + stompMessage); - mMessagesStream.onNext(stompMessage); + void bareSend(String stompMessage) { + mWebSocketClient.send(stompMessage); } @Override - public Observable getLifecycleReceiver() { - return mLifecycleStream; + Object getSocket() { + return mWebSocketClient; } } diff --git a/lib/src/main/java/ua/naiksoftware/stomp/client/StompClient.java b/lib/src/main/java/ua/naiksoftware/stomp/client/StompClient.java index 00f1e29..2c2b968 100644 --- a/lib/src/main/java/ua/naiksoftware/stomp/client/StompClient.java +++ b/lib/src/main/java/ua/naiksoftware/stomp/client/StompClient.java @@ -5,20 +5,12 @@ import java.util.ArrayList; import java.util.Collections; import java.util.HashMap; -import java.util.HashSet; -import java.util.Iterator; import java.util.List; -import java.util.Map; -import java.util.Set; import java.util.UUID; -import java.util.concurrent.CopyOnWriteArrayList; import java8.util.concurrent.CompletableFuture; import rx.Completable; import rx.Observable; -import rx.Subscriber; -import rx.Subscription; -import rx.observables.ConnectableObservable; import rx.schedulers.Schedulers; import rx.subjects.PublishSubject; import ua.naiksoftware.stomp.ConnectionProvider; @@ -35,29 +27,22 @@ public class StompClient { public static final String SUPPORTED_VERSIONS = "1.1,1.0"; public static final String DEFAULT_ACK = "auto"; - /* - private Subscription mMessagesSubscription; - private Map>> mSubscribers = new HashMap<>(); - */ -// private List mWaitConnectionCompletables; private final ConnectionProvider mConnectionProvider; private HashMap mTopics; private boolean mConnected; private boolean isConnecting; private PublishSubject mMessageStream; - private CompletableFuture connectionStatus; - private Completable waitForConnect; - private HashMap> msgStreams; + private CompletableFuture mConnectionFuture; + private Completable mConnectionComplete; + private HashMap> mStreamMap; public StompClient(ConnectionProvider connectionProvider) { mConnectionProvider = connectionProvider; -// mWaitConnectionCompletables = new CopyOnWriteArrayList<>(); mMessageStream = PublishSubject.create(); - connectionStatus = new CompletableFuture<>(); - waitForConnect = Completable.fromFuture(connectionStatus).subscribeOn(Schedulers.newThread()); - waitForConnect.subscribe(() -> Log.d(TAG, "waitForConnect completed")); - msgStreams = new HashMap<>(); + mConnectionFuture = new CompletableFuture<>(); + mConnectionComplete = Completable.fromFuture(mConnectionFuture).subscribeOn(Schedulers.newThread()); + mStreamMap = new HashMap<>(); } /** @@ -96,7 +81,7 @@ public void connect(List _headers, boolean reconnect) { headers.add(new StompHeader(StompHeader.VERSION, SUPPORTED_VERSIONS)); if (_headers != null) headers.addAll(_headers); mConnectionProvider.send(new StompMessage(StompCommand.CONNECT, headers, null).compile()) - .subscribe(() -> Log.d(TAG, "CONNECT command sent!")); + .subscribe(); break; case CLOSED: @@ -119,13 +104,7 @@ public void connect(List _headers, boolean reconnect) { .subscribe(stompMessage -> { mConnected = true; isConnecting = false; - connectionStatus.complete(true); - /* - for (Completable completable : mWaitConnectionCompletables) { - completable.subscribe(); - } - mWaitConnectionCompletables.clear(); - */ + mConnectionFuture.complete(true); }); } @@ -142,29 +121,10 @@ public Completable send(String destination, String data) { public Completable send(StompMessage stompMessage) { Completable completable = mConnectionProvider.send(stompMessage.compile()); - /* - if (!mConnected) { - mWaitConnectionCompletables.add(completable); - } - */ - waitForConnect.subscribe(() -> Log.d(TAG, "SEND waitForConnect complete, continuing!")); - return completable.startWith(waitForConnect); + mConnectionComplete.subscribe(); + return completable.startWith(mConnectionComplete); } - /* - private void callSubscribers(StompMessage stompMessage) { - String messageDestination = stompMessage.findHeader(StompHeader.DESTINATION); - for (String dest : mSubscribers.keySet()) { - if (dest.equals(messageDestination)) { - for (Subscriber subscriber : mSubscribers.get(dest)) { - subscriber.onNext(stompMessage); - } - return; - } - } - } - */ - private void callSubscribers(StompMessage stompMessage) { mMessageStream.onNext(stompMessage); } @@ -184,72 +144,17 @@ public Observable topic(String destinationPath) { public Observable topic(String destPath, List headerList) { if (destPath == null) return Observable.error(new IllegalArgumentException("Topic path cannot be null")); - else if (!msgStreams.containsKey(destPath)) - msgStreams.put(destPath, + else if (!mStreamMap.containsKey(destPath)) + mStreamMap.put(destPath, mMessageStream .filter(msg -> destPath.equals(msg.findHeader(StompHeader.DESTINATION))) .doOnSubscribe(() -> subscribePath(destPath, headerList).subscribe()) .doOnUnsubscribe(() -> unsubscribePath(destPath).subscribe()) .share() ); - return msgStreams.get(destPath); + return mStreamMap.get(destPath); } - /* - public Observable topic(String destinationPath, List headerList) { - // basically: - // on SUBSCRIBE, add the observer to the Set in the mSubscribers map that's associated with the specified topic, - // and send a subscribe message IF WE HAVEN'T ALREADY SUBSCRIBED TO THE TOPIC - // - // on UNSUBSCRIBE, remove unsubscribed observers, and remove unobserved topics - - // on observer subscribe... - return Observable.create(subscriber -> { - // get list of other subscribers to topic - Set> subscribersSet = mSubscribers.get(destinationPath); - // if there are no other subscribers on topic... - if (subscribersSet == null) { - // create new subscriber list, - subscribersSet = new HashSet<>(); - // and add the list to the map - mSubscribers.put(destinationPath, subscribersSet); - // send SUBSCRIBE message and add topic to mTopics - subscribePath(destinationPath, headerList).subscribe(); - } - // finally, now that we know that there is a list for this topic, add observer to it - subscribersSet.add(subscriber); - - }).doOnUnsubscribe(() -> { - // on unsubscribe... - Iterator mapIterator = mSubscribers.keySet().iterator(); - // for each topic in the map, - while (mapIterator.hasNext()) { - // get topic path - String destinationUrl = mapIterator.next(); - // get observers subscribed to this topic - Set> set = mSubscribers.get(destinationUrl); - Iterator> setIterator = set.iterator(); - // for each observer subscribed to this topic, - while (setIterator.hasNext()) { - Subscriber subscriber = setIterator.next(); - // if observer is no longer subscribed, - if (subscriber.isUnsubscribed()) { - // remove it from the set - setIterator.remove(); - // if there are no observers subscribed to this topic anymore... - if (set.size() < 1) { - // remote the set from the map - mapIterator.remove(); - // send UNSUBSCRIBE message - unsubscribePath(destinationUrl).subscribe(); - } - } - } - } - }); - } - */ - private Completable subscribePath(String destinationPath, List headerList) { String topicId = UUID.randomUUID().toString(); @@ -273,7 +178,7 @@ private Completable subscribePath(String destinationPath, List head private Completable unsubscribePath(String dest) { - msgStreams.remove(dest); + mStreamMap.remove(dest); String topicId = mTopics.get(dest); Log.d(TAG, "Unsubscribe path: " + dest + " id: " + topicId); From de27e320358b98ec30b24580d56375b68776389e Mon Sep 17 00:00:00 2001 From: forresthopkinsa Date: Tue, 8 Aug 2017 13:58:59 -0700 Subject: [PATCH 10/33] Preparing for release --- README.md | 16 ++++-- lib/build.gradle | 3 +- .../java/ua/naiksoftware/stomp/Stomp.java | 57 +++++++------------ 3 files changed, 33 insertions(+), 43 deletions(-) diff --git a/README.md b/README.md index 5dc7c79..e2fb713 100644 --- a/README.md +++ b/README.md @@ -4,7 +4,7 @@ ## Overview -**Note that this is a FORK of a project by NaikSoftware! This version is purely to avoid using RetroLambda!** +**Note that this is a FORK of a project by NaikSoftware! This version is made to avoid using RetroLambda! (Scroll down to see other changes)** This library provides support for [STOMP protocol](https://stomp.github.io/) over Websockets. @@ -19,7 +19,7 @@ repositories { maven { url "https://jitpack.io" } } dependencies { - compile 'com.github.forresthopkinsa:StompProtocolAndroid:{latest version}' + compile 'com.github.forresthopkinsa:StompProtocolAndroid:1.4.0' } ``` @@ -29,11 +29,17 @@ You can use this library two ways: - If you have Java 8 compatiblity and Jack enabled, this library will work for you - Using the new Native Java 8 support - As of this writing, you must be using Android Studio Canary to use this feature. + - Has been tested in the following environments: + - Canary 9, Gradle plugin v3.0.0-alpha9 + - Canary 8, Gradle plugin v3.0.0-alpha8 + - It *should* work in all 3.0.0+ versions - You can find more info on the [Releases Page](https://github.com/forresthopkinsa/StompProtocolAndroid/releases) However, *this fork is NOT compatible with Retrolambda.* If you have RL as a dependency, then you should be using the [upstream version](https://github.com/NaikSoftware/StompProtocolAndroid) of this project! +Finally, please take bugs and questions to the [Issues page](https://github.com/forresthopkinsa/StompProtocolAndroid/issues)! I'll try to answer within one business day. + ## Example backend (Spring Boot) **WebSocketConfig.java** @@ -71,7 +77,7 @@ class SocketController { } ``` -Check out the full example server https://github.com/NaikSoftware/stomp-protocol-example-server +Check out the [full example server](https://github.com/NaikSoftware/stomp-protocol-example-server) ## Example library usage @@ -104,7 +110,7 @@ Check out the full example server https://github.com/NaikSoftware/stomp-protocol ``` -See the full example https://github.com/NaikSoftware/StompProtocolAndroid/tree/master/example-client +See the [full example](https://github.com/NaikSoftware/StompProtocolAndroid/tree/master/example-client) Method `Stomp.over` consume class for create connection as first parameter. You must provide dependency for lib and pass class. @@ -134,7 +140,7 @@ client.lifecycle().subscribe(lifecycleEvent -> { **Custom client** -You can use a custom OkHttpClient (for example, [if you want to allow untrusted HTTPS](getUnsafeOkHttpClient())) using the four-argument overload of Stomp.over, like so: +You can use a custom OkHttpClient (for example, [if you want to allow untrusted HTTPS](https://gist.github.com/grow2014/b6969d8f0cfc0f0a1b2bf12f84973dec)) using the four-argument overload of Stomp.over, like so: ``` java client = Stomp.over(WebSocket.class, address, null, getUnsafeOkHttpClient()); diff --git a/lib/build.gradle b/lib/build.gradle index 4d3f9a5..1c4e625 100644 --- a/lib/build.gradle +++ b/lib/build.gradle @@ -1,7 +1,7 @@ apply plugin: 'com.android.library' apply plugin: 'com.github.dcendents.android-maven' -group='com.github.NaikSoftware' +group='com.github.forresthopkinsa' android { compileSdkVersion 25 @@ -37,6 +37,7 @@ dependencies { // Supported transports compile 'org.java-websocket:java-websocket:1.3.2' compile 'com.squareup.okhttp3:okhttp:3.8.1' + implementation 'com.android.support:support-annotations:24.2.0' } task sourcesJar(type: Jar) { diff --git a/lib/src/main/java/ua/naiksoftware/stomp/Stomp.java b/lib/src/main/java/ua/naiksoftware/stomp/Stomp.java index ed4fef5..57508ef 100644 --- a/lib/src/main/java/ua/naiksoftware/stomp/Stomp.java +++ b/lib/src/main/java/ua/naiksoftware/stomp/Stomp.java @@ -1,6 +1,6 @@ package ua.naiksoftware.stomp; -import org.java_websocket.WebSocket; +import android.support.annotation.Nullable; import java.util.Map; @@ -19,19 +19,19 @@ */ public class Stomp { - public static StompClient over(Class clazz, String uri) { - return over(clazz, uri, null, null); + public static StompClient over(Transport transport, String uri) { + return over(transport, uri, null, null); } /** * - * @param clazz class for using as transport + * @param transport transport method * @param uri URI to connect * @param connectHttpHeaders HTTP headers, will be passed with handshake query, may be null * @return StompClient for receiving and sending messages. Call #StompClient.connect */ - public static StompClient over(Class clazz, String uri, Map connectHttpHeaders) { - return over(clazz, uri, connectHttpHeaders, null); + public static StompClient over(Transport transport, String uri, Map connectHttpHeaders) { + return over(transport, uri, connectHttpHeaders, null); } /** @@ -40,49 +40,32 @@ public static StompClient over(Class clazz, String uri, Map conn *
  • {@code org.java_websocket.WebSocket}: cannot accept an existing client
  • *
  • {@code okhttp3.WebSocket}: can accept a non-null instance of {@code okhttp3.OkHttpClient}
  • * - * @param clazz class for using as transport + * @param transport transport method * @param uri URI to connect * @param connectHttpHeaders HTTP headers, will be passed with handshake query, may be null - * @param webSocketClient Existing client that will be used to open the WebSocket connection, may be null to use default client + * @param okHttpClient Existing client that will be used to open the WebSocket connection, may be null to use default client * @return StompClient for receiving and sending messages. Call #StompClient.connect */ - public static StompClient over(Class clazz, String uri, Map connectHttpHeaders, Object webSocketClient) { - try { - if (Class.forName("org.java_websocket.WebSocket") != null && clazz == WebSocket.class) { - - if (webSocketClient != null) { - throw new IllegalArgumentException("You cannot pass a webSocketClient with 'org.java_websocket.WebSocket'. use null instead."); - } - - return createStompClient(new WebSocketsConnectionProvider(uri, connectHttpHeaders)); + public static StompClient over(Transport transport, String uri, @Nullable Map connectHttpHeaders, @Nullable OkHttpClient okHttpClient) { + if (transport == Transport.JWS) { + if (okHttpClient != null) { + throw new IllegalArgumentException("You cannot pass a webSocketClient with 'org.java_websocket.WebSocket'. use null instead."); } - } catch (ClassNotFoundException e) {} - try { - if (Class.forName("okhttp3.WebSocket") != null && clazz == okhttp3.WebSocket.class) { - - OkHttpClient okHttpClient = getOkHttpClient(webSocketClient); + return createStompClient(new WebSocketsConnectionProvider(uri, connectHttpHeaders)); + } - return createStompClient(new OkHttpConnectionProvider(uri, connectHttpHeaders, okHttpClient)); - } - } catch (ClassNotFoundException e) {} + if (transport == Transport.OKHTTP) { + return createStompClient(new OkHttpConnectionProvider(uri, connectHttpHeaders, (okHttpClient == null) ? new OkHttpClient() : okHttpClient)); + } - throw new RuntimeException("Not supported overlay transport: " + clazz.getName()); + throw new IllegalArgumentException("Transport type not supported: " + transport.toString()); } private static StompClient createStompClient(ConnectionProvider connectionProvider) { return new StompClient(connectionProvider); } - private static OkHttpClient getOkHttpClient(Object webSocketClient) { - if (webSocketClient != null) { - if (webSocketClient instanceof OkHttpClient) { - return (OkHttpClient) webSocketClient; - } else { - throw new IllegalArgumentException("You must pass a non-null instance of an 'okhttp3.OkHttpClient'. Or pass null to use a default websocket client."); - } - } else { - // default http client - return new OkHttpClient(); - } + public enum Transport { + OKHTTP, JWS } } From 14e9dd65f3a0f522e333db4cf7dcb32b279b98b4 Mon Sep 17 00:00:00 2001 From: forresthopkinsa Date: Tue, 8 Aug 2017 14:10:23 -0700 Subject: [PATCH 11/33] Nullity inference and documentation update Also, it seems that "Transport" isn't a legal name for an inner enum --- README.md | 21 +++++++++------- .../stomp/AbstractConnectionProvider.java | 10 +++++++- .../stomp/OkHttpConnectionProvider.java | 18 +++++++++---- .../java/ua/naiksoftware/stomp/Stomp.java | 25 ++++++++++--------- .../stomp/WebSocketsConnectionProvider.java | 10 +++++--- .../stomp/client/StompClient.java | 10 +++++--- .../stomp/client/StompMessage.java | 7 +++++- 7 files changed, 66 insertions(+), 35 deletions(-) diff --git a/README.md b/README.md index e2fb713..b7647f4 100644 --- a/README.md +++ b/README.md @@ -77,7 +77,7 @@ class SocketController { } ``` -Check out the [full example server](https://github.com/NaikSoftware/stomp-protocol-example-server) +Check out the [upstream example server](https://github.com/NaikSoftware/stomp-protocol-example-server) ## Example library usage @@ -91,7 +91,7 @@ Check out the [full example server](https://github.com/NaikSoftware/stomp-protoc // ... - StompClient client = Stomp.over(WebSocket.class, "http://localhost/example-endpoint"); + StompClient client = Stomp.over(Stomp.ConnectionProvider.OKHTTP, "http://localhost/example-endpoint"); client.connect(); client.topic("/topic/greetings").subscribe(message -> { @@ -110,13 +110,13 @@ Check out the [full example server](https://github.com/NaikSoftware/stomp-protoc ``` -See the [full example](https://github.com/NaikSoftware/StompProtocolAndroid/tree/master/example-client) +See the [upstream example](https://github.com/NaikSoftware/StompProtocolAndroid/tree/master/example-client) -Method `Stomp.over` consume class for create connection as first parameter. -You must provide dependency for lib and pass class. -At now supported connection providers: -- `org.java_websocket.WebSocket.class` ('org.java-websocket:Java-WebSocket:1.3.2') -- `okhttp3.WebSocket.class` ('com.squareup.okhttp3:okhttp:3.8.1') +Method `Stomp.over` uses an enum to know what connection provider to use. + +Currently supported connection providers: +- `Stomp.ConnectionProvider.JWS` ('org.java-websocket:Java-WebSocket:1.3.2') +- `Stomp.ConnectionProvider.OKHTTP` ('com.squareup.okhttp3:okhttp:3.8.1') You can add own connection provider. Just implement interface `ConnectionProvider`. If you implement new provider, please create pull request :) @@ -143,7 +143,7 @@ client.lifecycle().subscribe(lifecycleEvent -> { You can use a custom OkHttpClient (for example, [if you want to allow untrusted HTTPS](https://gist.github.com/grow2014/b6969d8f0cfc0f0a1b2bf12f84973dec)) using the four-argument overload of Stomp.over, like so: ``` java -client = Stomp.over(WebSocket.class, address, null, getUnsafeOkHttpClient()); +client = Stomp.over(Stomp.ConnectionProvider.OKHTTP, address, null, getUnsafeOkHttpClient()); ``` Yes, it's safe to pass `null` for either (or both) of the last two arguments. That's exactly what the shorter overloads do. @@ -222,6 +222,9 @@ These are the possible changes you need to make to your code for this branch, if - Passing null as the topic path now throws an exception - Previously, it was supposed to silently fail, although it would probably hit a NPE first (untested) - Now it throws an IllegalArgumentException +- Connection Provider is selected by an enum + - It used to be done by passing it a WebSocket class from either JWS or OkHttp + - New way of using it can be seen in the examples above ## Additional Reading diff --git a/lib/src/main/java/ua/naiksoftware/stomp/AbstractConnectionProvider.java b/lib/src/main/java/ua/naiksoftware/stomp/AbstractConnectionProvider.java index 3d4afd0..be5eb51 100644 --- a/lib/src/main/java/ua/naiksoftware/stomp/AbstractConnectionProvider.java +++ b/lib/src/main/java/ua/naiksoftware/stomp/AbstractConnectionProvider.java @@ -1,5 +1,7 @@ package ua.naiksoftware.stomp; +import android.support.annotation.NonNull; +import android.support.annotation.Nullable; import android.util.Log; import rx.Completable; @@ -16,7 +18,9 @@ abstract class AbstractConnectionProvider implements ConnectionProvider { private static final String TAG = AbstractConnectionProvider.class.getSimpleName(); + @NonNull private final PublishSubject mLifecycleStream; + @NonNull private final PublishSubject mMessagesStream; AbstractConnectionProvider() { @@ -24,6 +28,7 @@ abstract class AbstractConnectionProvider implements ConnectionProvider { mMessagesStream = PublishSubject.create(); } + @NonNull @Override public Observable messages() { createWebSocketConnection(); @@ -48,6 +53,7 @@ public Observable messages() { */ abstract void createWebSocketConnection(); + @NonNull @Override public Completable send(String stompMessage) { return Completable.fromCallable(() -> { @@ -82,9 +88,10 @@ public Completable send(String stompMessage) { * return webSocket; * */ + @Nullable abstract Object getSocket(); - void emitLifecycleEvent(LifecycleEvent lifecycleEvent) { + void emitLifecycleEvent(@NonNull LifecycleEvent lifecycleEvent) { Log.d(TAG, "Emit lifecycle event: " + lifecycleEvent.getType().name()); mLifecycleStream.onNext(lifecycleEvent); } @@ -94,6 +101,7 @@ void emitMessage(String stompMessage) { mMessagesStream.onNext(stompMessage); } + @NonNull @Override public Observable getLifecycleReceiver() { return mLifecycleStream; diff --git a/lib/src/main/java/ua/naiksoftware/stomp/OkHttpConnectionProvider.java b/lib/src/main/java/ua/naiksoftware/stomp/OkHttpConnectionProvider.java index 1e23b7e..39c21ed 100644 --- a/lib/src/main/java/ua/naiksoftware/stomp/OkHttpConnectionProvider.java +++ b/lib/src/main/java/ua/naiksoftware/stomp/OkHttpConnectionProvider.java @@ -1,5 +1,8 @@ package ua.naiksoftware.stomp; +import android.support.annotation.NonNull; +import android.support.annotation.Nullable; + import java.util.HashMap; import java.util.Map; import java.util.TreeMap; @@ -16,18 +19,21 @@ class OkHttpConnectionProvider extends AbstractConnectionProvider { private final String mUri; + @NonNull private final Map mConnectHttpHeaders; private final OkHttpClient mOkHttpClient; + @Nullable private WebSocket openedSocked; - OkHttpConnectionProvider(String uri, Map connectHttpHeaders, OkHttpClient okHttpClient) { + OkHttpConnectionProvider(String uri, @Nullable Map connectHttpHeaders, OkHttpClient okHttpClient) { super(); mUri = uri; mConnectHttpHeaders = connectHttpHeaders != null ? connectHttpHeaders : new HashMap<>(); mOkHttpClient = okHttpClient; } + @NonNull @Override public Completable disconnect() { return Completable.fromAction(() -> openedSocked.close(1000, "")); @@ -48,7 +54,7 @@ void createWebSocketConnection() { openedSocked = mOkHttpClient.newWebSocket(requestBuilder.build(), new WebSocketListener() { @Override - public void onOpen(WebSocket webSocket, Response response) { + public void onOpen(WebSocket webSocket, @NonNull Response response) { LifecycleEvent openEvent = new LifecycleEvent(LifecycleEvent.Type.OPENED); TreeMap headersAsMap = headersAsMap(response); @@ -63,7 +69,7 @@ public void onMessage(WebSocket webSocket, String text) { } @Override - public void onMessage(WebSocket webSocket, ByteString bytes) { + public void onMessage(WebSocket webSocket, @NonNull ByteString bytes) { emitMessage(bytes.utf8()); } @@ -87,12 +93,14 @@ void bareSend(String stompMessage) { openedSocked.send(stompMessage); } + @Nullable @Override Object getSocket() { return openedSocked; } - private TreeMap headersAsMap(Response response) { + @NonNull + private TreeMap headersAsMap(@NonNull Response response) { TreeMap headersAsMap = new TreeMap<>(); Headers headers = response.headers(); for (String key : headers.names()) { @@ -101,7 +109,7 @@ private TreeMap headersAsMap(Response response) { return headersAsMap; } - private void addConnectionHeadersToBuilder(Request.Builder requestBuilder, Map mConnectHttpHeaders) { + private void addConnectionHeadersToBuilder(@NonNull Request.Builder requestBuilder, @NonNull Map mConnectHttpHeaders) { for (Map.Entry headerEntry : mConnectHttpHeaders.entrySet()) { requestBuilder.addHeader(headerEntry.getKey(), headerEntry.getValue()); } diff --git a/lib/src/main/java/ua/naiksoftware/stomp/Stomp.java b/lib/src/main/java/ua/naiksoftware/stomp/Stomp.java index 57508ef..31fba3e 100644 --- a/lib/src/main/java/ua/naiksoftware/stomp/Stomp.java +++ b/lib/src/main/java/ua/naiksoftware/stomp/Stomp.java @@ -1,5 +1,6 @@ package ua.naiksoftware.stomp; +import android.support.annotation.NonNull; import android.support.annotation.Nullable; import java.util.Map; @@ -19,19 +20,19 @@ */ public class Stomp { - public static StompClient over(Transport transport, String uri) { - return over(transport, uri, null, null); + public static StompClient over(@NonNull ConnectionProvider connectionProvider, String uri) { + return over(connectionProvider, uri, null, null); } /** * - * @param transport transport method + * @param connectionProvider connectionProvider method * @param uri URI to connect * @param connectHttpHeaders HTTP headers, will be passed with handshake query, may be null * @return StompClient for receiving and sending messages. Call #StompClient.connect */ - public static StompClient over(Transport transport, String uri, Map connectHttpHeaders) { - return over(transport, uri, connectHttpHeaders, null); + public static StompClient over(@NonNull ConnectionProvider connectionProvider, String uri, Map connectHttpHeaders) { + return over(connectionProvider, uri, connectHttpHeaders, null); } /** @@ -40,32 +41,32 @@ public static StompClient over(Transport transport, String uri, Map{@code org.java_websocket.WebSocket}: cannot accept an existing client *
  • {@code okhttp3.WebSocket}: can accept a non-null instance of {@code okhttp3.OkHttpClient}
  • * - * @param transport transport method + * @param connectionProvider connectionProvider method * @param uri URI to connect * @param connectHttpHeaders HTTP headers, will be passed with handshake query, may be null * @param okHttpClient Existing client that will be used to open the WebSocket connection, may be null to use default client * @return StompClient for receiving and sending messages. Call #StompClient.connect */ - public static StompClient over(Transport transport, String uri, @Nullable Map connectHttpHeaders, @Nullable OkHttpClient okHttpClient) { - if (transport == Transport.JWS) { + public static StompClient over(@NonNull ConnectionProvider connectionProvider, String uri, @Nullable Map connectHttpHeaders, @Nullable OkHttpClient okHttpClient) { + if (connectionProvider == ConnectionProvider.JWS) { if (okHttpClient != null) { throw new IllegalArgumentException("You cannot pass a webSocketClient with 'org.java_websocket.WebSocket'. use null instead."); } return createStompClient(new WebSocketsConnectionProvider(uri, connectHttpHeaders)); } - if (transport == Transport.OKHTTP) { + if (connectionProvider == ConnectionProvider.OKHTTP) { return createStompClient(new OkHttpConnectionProvider(uri, connectHttpHeaders, (okHttpClient == null) ? new OkHttpClient() : okHttpClient)); } - throw new IllegalArgumentException("Transport type not supported: " + transport.toString()); + throw new IllegalArgumentException("ConnectionProvider type not supported: " + connectionProvider.toString()); } - private static StompClient createStompClient(ConnectionProvider connectionProvider) { + private static StompClient createStompClient(ua.naiksoftware.stomp.ConnectionProvider connectionProvider) { return new StompClient(connectionProvider); } - public enum Transport { + public enum ConnectionProvider { OKHTTP, JWS } } diff --git a/lib/src/main/java/ua/naiksoftware/stomp/WebSocketsConnectionProvider.java b/lib/src/main/java/ua/naiksoftware/stomp/WebSocketsConnectionProvider.java index 48ad588..c672df4 100644 --- a/lib/src/main/java/ua/naiksoftware/stomp/WebSocketsConnectionProvider.java +++ b/lib/src/main/java/ua/naiksoftware/stomp/WebSocketsConnectionProvider.java @@ -1,5 +1,7 @@ package ua.naiksoftware.stomp; +import android.support.annotation.NonNull; +import android.support.annotation.Nullable; import android.util.Log; import org.java_websocket.WebSocket; @@ -29,6 +31,7 @@ class WebSocketsConnectionProvider extends AbstractConnectionProvider { private static final String TAG = WebSocketsConnectionProvider.class.getSimpleName(); private final String mUri; + @NonNull private final Map mConnectHttpHeaders; private WebSocketClient mWebSocketClient; @@ -39,11 +42,12 @@ class WebSocketsConnectionProvider extends AbstractConnectionProvider { * Support UIR scheme ws://host:port/path * @param connectHttpHeaders may be null */ - WebSocketsConnectionProvider(String uri, Map connectHttpHeaders) { + WebSocketsConnectionProvider(String uri, @Nullable Map connectHttpHeaders) { mUri = uri; mConnectHttpHeaders = connectHttpHeaders != null ? connectHttpHeaders : new HashMap<>(); } + @NonNull @Override public Completable disconnect() { return Completable.fromAction(() -> mWebSocketClient.close()); @@ -57,7 +61,7 @@ void createWebSocketConnection() { mWebSocketClient = new WebSocketClient(URI.create(mUri), new Draft_17(), mConnectHttpHeaders, 0) { @Override - public void onWebsocketHandshakeReceivedAsClient(WebSocket conn, ClientHandshake request, ServerHandshake response) throws InvalidDataException { + public void onWebsocketHandshakeReceivedAsClient(WebSocket conn, ClientHandshake request, @NonNull ServerHandshake response) throws InvalidDataException { Log.d(TAG, "onWebsocketHandshakeReceivedAsClient with response: " + response.getHttpStatus() + " " + response.getHttpStatusMessage()); mServerHandshakeHeaders = new TreeMap<>(); Iterator keys = response.iterateHttpFields(); @@ -68,7 +72,7 @@ public void onWebsocketHandshakeReceivedAsClient(WebSocket conn, ClientHandshake } @Override - public void onOpen(ServerHandshake handshakeData) { + public void onOpen(@NonNull ServerHandshake handshakeData) { Log.d(TAG, "onOpen with handshakeData: " + handshakeData.getHttpStatus() + " " + handshakeData.getHttpStatusMessage()); LifecycleEvent openEvent = new LifecycleEvent(LifecycleEvent.Type.OPENED); openEvent.setHandshakeResponseHeaders(mServerHandshakeHeaders); diff --git a/lib/src/main/java/ua/naiksoftware/stomp/client/StompClient.java b/lib/src/main/java/ua/naiksoftware/stomp/client/StompClient.java index 2c2b968..67c4428 100644 --- a/lib/src/main/java/ua/naiksoftware/stomp/client/StompClient.java +++ b/lib/src/main/java/ua/naiksoftware/stomp/client/StompClient.java @@ -1,5 +1,7 @@ package ua.naiksoftware.stomp.client; +import android.support.annotation.NonNull; +import android.support.annotation.Nullable; import android.util.Log; import java.util.ArrayList; @@ -70,7 +72,7 @@ public void connect(List _headers) { * * @param _headers might be null */ - public void connect(List _headers, boolean reconnect) { + public void connect(@Nullable List _headers, boolean reconnect) { if (reconnect) disconnect(); if (mConnected) return; lifecycle() @@ -119,7 +121,7 @@ public Completable send(String destination, String data) { data)); } - public Completable send(StompMessage stompMessage) { + public Completable send(@NonNull StompMessage stompMessage) { Completable completable = mConnectionProvider.send(stompMessage.compile()); mConnectionComplete.subscribe(); return completable.startWith(mConnectionComplete); @@ -141,7 +143,7 @@ public Observable topic(String destinationPath) { return topic(destinationPath, null); } - public Observable topic(String destPath, List headerList) { + public Observable topic(@Nullable String destPath, List headerList) { if (destPath == null) return Observable.error(new IllegalArgumentException("Topic path cannot be null")); else if (!mStreamMap.containsKey(destPath)) @@ -155,7 +157,7 @@ else if (!mStreamMap.containsKey(destPath)) return mStreamMap.get(destPath); } - private Completable subscribePath(String destinationPath, List headerList) { + private Completable subscribePath(String destinationPath, @Nullable List headerList) { String topicId = UUID.randomUUID().toString(); if (mTopics == null) mTopics = new HashMap<>(); diff --git a/lib/src/main/java/ua/naiksoftware/stomp/client/StompMessage.java b/lib/src/main/java/ua/naiksoftware/stomp/client/StompMessage.java index 37810e2..ee3619c 100644 --- a/lib/src/main/java/ua/naiksoftware/stomp/client/StompMessage.java +++ b/lib/src/main/java/ua/naiksoftware/stomp/client/StompMessage.java @@ -1,5 +1,8 @@ package ua.naiksoftware.stomp.client; +import android.support.annotation.NonNull; +import android.support.annotation.Nullable; + import java.io.StringReader; import java.util.ArrayList; import java.util.List; @@ -40,6 +43,7 @@ public String getStompCommand() { return mStompCommand; } + @Nullable public String findHeader(String key) { if (mStompHeaders == null) return null; for (StompHeader header : mStompHeaders) { @@ -48,6 +52,7 @@ public String findHeader(String key) { return null; } + @NonNull public String compile() { StringBuilder builder = new StringBuilder(); builder.append(mStompCommand).append('\n'); @@ -62,7 +67,7 @@ public String compile() { return builder.toString(); } - public static StompMessage from(String data) { + public static StompMessage from(@Nullable String data) { if (data == null || data.trim().isEmpty()) { return new StompMessage(StompCommand.UNKNOWN, null, data); } From d7963576b44904f4c6b965b15aae2ef9488e3e8c Mon Sep 17 00:00:00 2001 From: forresthopkinsa Date: Fri, 11 Aug 2017 10:06:42 -0700 Subject: [PATCH 12/33] Working on reconnection --- build.gradle | 2 +- .../stomp/OkHttpConnectionProvider.java | 13 +++++++++++-- .../ua/naiksoftware/stomp/client/StompClient.java | 15 ++++++++++----- 3 files changed, 22 insertions(+), 8 deletions(-) diff --git a/build.gradle b/build.gradle index 4a30413..55f34ea 100644 --- a/build.gradle +++ b/build.gradle @@ -8,7 +8,7 @@ buildscript { } } dependencies { - classpath 'com.android.tools.build:gradle:3.0.0-alpha9' + classpath 'com.android.tools.build:gradle:3.0.0-beta1' classpath 'com.github.dcendents:android-maven-gradle-plugin:1.5' // NOTE: Do not place your application dependencies here; they belong diff --git a/lib/src/main/java/ua/naiksoftware/stomp/OkHttpConnectionProvider.java b/lib/src/main/java/ua/naiksoftware/stomp/OkHttpConnectionProvider.java index 39c21ed..a52db6b 100644 --- a/lib/src/main/java/ua/naiksoftware/stomp/OkHttpConnectionProvider.java +++ b/lib/src/main/java/ua/naiksoftware/stomp/OkHttpConnectionProvider.java @@ -2,6 +2,7 @@ import android.support.annotation.NonNull; import android.support.annotation.Nullable; +import android.util.Log; import java.util.HashMap; import java.util.Map; @@ -22,6 +23,7 @@ class OkHttpConnectionProvider extends AbstractConnectionProvider { @NonNull private final Map mConnectHttpHeaders; private final OkHttpClient mOkHttpClient; + private final String tag = OkHttpConnectionProvider.class.getSimpleName(); @Nullable private WebSocket openedSocked; @@ -36,7 +38,11 @@ class OkHttpConnectionProvider extends AbstractConnectionProvider { @NonNull @Override public Completable disconnect() { - return Completable.fromAction(() -> openedSocked.close(1000, "")); + if (openedSocked == null) { + return Completable.error(new IllegalStateException("Attempted to disconnect when already disconnected")); + } + return Completable + .fromAction(() -> openedSocked.close(1000, "")); } @Override @@ -65,7 +71,10 @@ public void onOpen(WebSocket webSocket, @NonNull Response response) { @Override public void onMessage(WebSocket webSocket, String text) { - emitMessage(text); + if (text.equals("\n")) + Log.d(tag, "RECEIVED HEARTBEAT"); + else + emitMessage(text); } @Override diff --git a/lib/src/main/java/ua/naiksoftware/stomp/client/StompClient.java b/lib/src/main/java/ua/naiksoftware/stomp/client/StompClient.java index 67c4428..7f3aed5 100644 --- a/lib/src/main/java/ua/naiksoftware/stomp/client/StompClient.java +++ b/lib/src/main/java/ua/naiksoftware/stomp/client/StompClient.java @@ -29,6 +29,7 @@ public class StompClient { public static final String SUPPORTED_VERSIONS = "1.1,1.0"; public static final String DEFAULT_ACK = "auto"; + private final String tag = StompClient.class.getSimpleName(); private final ConnectionProvider mConnectionProvider; private HashMap mTopics; private boolean mConnected; @@ -42,9 +43,13 @@ public class StompClient { public StompClient(ConnectionProvider connectionProvider) { mConnectionProvider = connectionProvider; mMessageStream = PublishSubject.create(); + mStreamMap = new HashMap<>(); + resetStatus(); + } + + private void resetStatus() { mConnectionFuture = new CompletableFuture<>(); mConnectionComplete = Completable.fromFuture(mConnectionFuture).subscribeOn(Schedulers.newThread()); - mStreamMap = new HashMap<>(); } /** @@ -61,7 +66,7 @@ public void connect(boolean reconnect) { /** * Connect without reconnect if connected * - * @param _headers might be null + * @param _headers HTTP headers to send in the INITIAL REQUEST, i.e. during the protocol upgrade */ public void connect(List _headers) { connect(_headers, false); @@ -70,7 +75,7 @@ public void connect(List _headers) { /** * If already connected and reconnect=false - nope * - * @param _headers might be null + * @param _headers HTTP headers to send in the INITIAL REQUEST, i.e. during the protocol upgrade */ public void connect(@Nullable List _headers, boolean reconnect) { if (reconnect) disconnect(); @@ -123,7 +128,6 @@ public Completable send(String destination, String data) { public Completable send(@NonNull StompMessage stompMessage) { Completable completable = mConnectionProvider.send(stompMessage.compile()); - mConnectionComplete.subscribe(); return completable.startWith(mConnectionComplete); } @@ -136,7 +140,8 @@ public Observable lifecycle() { } public void disconnect() { - mConnectionProvider.disconnect().subscribe(); + resetStatus(); + mConnectionProvider.disconnect().subscribe(() -> mConnected = false); } public Observable topic(String destinationPath) { From d719ba65cad00f36b60be1cc8e79946694cf5811 Mon Sep 17 00:00:00 2001 From: forresthopkinsa Date: Fri, 11 Aug 2017 13:45:31 -0700 Subject: [PATCH 13/33] Support for RabbitMQ topic wildcards --- README.md | 12 +++ .../stomp/client/StompClient.java | 77 ++++++++++++++++++- 2 files changed, 87 insertions(+), 2 deletions(-) diff --git a/README.md b/README.md index b7647f4..65f033e 100644 --- a/README.md +++ b/README.md @@ -225,6 +225,18 @@ These are the possible changes you need to make to your code for this branch, if - Connection Provider is selected by an enum - It used to be done by passing it a WebSocket class from either JWS or OkHttp - New way of using it can be seen in the examples above +- Topic subscription now supports wildcard parsing + - Example: RabbitMQ allows subscribing to topics with wildcards ([more info](https://www.rabbitmq.com/tutorials/tutorial-five-java.html)) + - Usage: + ``` java + StompClient client = Stomp.over(...); + client.setParser(StompClient.Parser.RABBITMQ); + client.connect(); + + client.topic("/topic/*").subscribe(message -> { + Log.i(TAG, "Received message: " + message.getPayload()); + }); + ``` ## Additional Reading diff --git a/lib/src/main/java/ua/naiksoftware/stomp/client/StompClient.java b/lib/src/main/java/ua/naiksoftware/stomp/client/StompClient.java index 7f3aed5..5c96a87 100644 --- a/lib/src/main/java/ua/naiksoftware/stomp/client/StompClient.java +++ b/lib/src/main/java/ua/naiksoftware/stomp/client/StompClient.java @@ -10,6 +10,7 @@ import java.util.List; import java.util.UUID; +import java8.util.StringJoiner; import java8.util.concurrent.CompletableFuture; import rx.Completable; import rx.Observable; @@ -39,12 +40,14 @@ public class StompClient { private CompletableFuture mConnectionFuture; private Completable mConnectionComplete; private HashMap> mStreamMap; + private Parser parser; public StompClient(ConnectionProvider connectionProvider) { mConnectionProvider = connectionProvider; mMessageStream = PublishSubject.create(); mStreamMap = new HashMap<>(); resetStatus(); + parser = Parser.NONE; } private void resetStatus() { @@ -52,6 +55,26 @@ private void resetStatus() { mConnectionComplete = Completable.fromFuture(mConnectionFuture).subscribeOn(Schedulers.newThread()); } + public enum Parser { + NONE, + RABBITMQ + } + + /** + * Set the wildcard parser for Topic subscription. + *

    + * Right now, the only options are NONE and RABBITMQ. + *

    + * When set to RABBITMQ, topic subscription allows for RMQ-style wildcards. + *

    + * See more info here. + * + * @param parser Set to NONE by default + */ + public void setParser(Parser parser) { + this.parser = parser; + } + /** * Connect without reconnect if connected */ @@ -148,13 +171,13 @@ public Observable topic(String destinationPath) { return topic(destinationPath, null); } - public Observable topic(@Nullable String destPath, List headerList) { + public Observable topic(@NonNull String destPath, List headerList) { if (destPath == null) return Observable.error(new IllegalArgumentException("Topic path cannot be null")); else if (!mStreamMap.containsKey(destPath)) mStreamMap.put(destPath, mMessageStream - .filter(msg -> destPath.equals(msg.findHeader(StompHeader.DESTINATION))) + .filter(msg -> matches(destPath, msg)) .doOnSubscribe(() -> subscribePath(destPath, headerList).subscribe()) .doOnUnsubscribe(() -> unsubscribePath(destPath).subscribe()) .share() @@ -162,6 +185,56 @@ else if (!mStreamMap.containsKey(destPath)) return mStreamMap.get(destPath); } + private boolean matches(String path, StompMessage msg) { + String dest = msg.findHeader(StompHeader.DESTINATION); + if (dest == null) return false; + boolean ret; + + switch (parser) { + case NONE: + ret = path.equals(dest); + break; + + case RABBITMQ: + // for example string "lorem.ipsum.*.sit": + + // split it up into ["lorem", "ipsum", "*", "sit"] + String[] split = path.split("\\."); + ArrayList transformed = new ArrayList<>(); + // check for wildcards and replace with corresponding regex + for (String s : split) { + switch (s) { + case "*": + transformed.add("[^.]+"); + break; + case "#": + // TODO: make this work with zero-word + // e.g. "lorem.#.dolor" should ideally match "lorem.dolor" + transformed.add(".*"); + break; + default: + transformed.add(s); + break; + } + } + // at this point, 'transformed' looks like ["lorem", "ipsum", "[^.]+", "sit"] + StringJoiner sj = new StringJoiner("\\."); + for (String s : transformed) + sj.add(s); + String join = sj.toString(); + // join = "lorem\.ipsum\.[^.]+\.sit" + + ret = dest.matches(join); + break; + + default: + ret = false; + break; + } + + return ret; + } + private Completable subscribePath(String destinationPath, @Nullable List headerList) { String topicId = UUID.randomUUID().toString(); From 9c7a46bcf0a5b79d697a797e86e7e8d11ddab560 Mon Sep 17 00:00:00 2001 From: forresthopkinsa Date: Tue, 15 Aug 2017 15:05:37 -0700 Subject: [PATCH 14/33] Finished fixing reconnect and its race conditions Managed to do it without any `synchronized` blocking --- README.md | 17 +++++++++ build.gradle | 2 +- .../stomp/OkHttpConnectionProvider.java | 36 +++++++++++++++++-- .../stomp/client/StompClient.java | 32 ++++++++--------- 4 files changed, 68 insertions(+), 19 deletions(-) diff --git a/README.md b/README.md index 65f033e..c4e8748 100644 --- a/README.md +++ b/README.md @@ -30,6 +30,8 @@ You can use this library two ways: - Using the new Native Java 8 support - As of this writing, you must be using Android Studio Canary to use this feature. - Has been tested in the following environments: + - Beta 2, Gradle plugin v3.0.0-beta2 + - Beta 1, Gradle plugin v3.0.0-beta1 - Canary 9, Gradle plugin v3.0.0-alpha9 - Canary 8, Gradle plugin v3.0.0-alpha8 - It *should* work in all 3.0.0+ versions @@ -219,6 +221,21 @@ These are the possible changes you need to make to your code for this branch, if }); ``` - Be sure to implement this change, because the IDE might not catch the error. +- Removed `reconnect` parameter from `connect(...)` methods + - Old, deprecated overloads of the method: + ``` java + void connect(boolean reconnect) {...} + void connect(List _headers, boolean reconnect) {...} + ``` + - Now, these are the *only* two ways to call `connect` (these existed before, too): + ``` java + void connect() {...} + void connect(List _headers) {...} + ``` + - Additionally, reconnection is now handled by just calling `reconnect()` + - It automatically attaches the last-used connect headers + - It is meant to be used when already connected; it executes `disconnect()` + - Note that if you're already disconnected, this will throw a `TimeoutException` - Passing null as the topic path now throws an exception - Previously, it was supposed to silently fail, although it would probably hit a NPE first (untested) - Now it throws an IllegalArgumentException diff --git a/build.gradle b/build.gradle index 55f34ea..333cf60 100644 --- a/build.gradle +++ b/build.gradle @@ -8,7 +8,7 @@ buildscript { } } dependencies { - classpath 'com.android.tools.build:gradle:3.0.0-beta1' + classpath 'com.android.tools.build:gradle:3.0.0-beta2' classpath 'com.github.dcendents:android-maven-gradle-plugin:1.5' // NOTE: Do not place your application dependencies here; they belong diff --git a/lib/src/main/java/ua/naiksoftware/stomp/OkHttpConnectionProvider.java b/lib/src/main/java/ua/naiksoftware/stomp/OkHttpConnectionProvider.java index a52db6b..259031d 100644 --- a/lib/src/main/java/ua/naiksoftware/stomp/OkHttpConnectionProvider.java +++ b/lib/src/main/java/ua/naiksoftware/stomp/OkHttpConnectionProvider.java @@ -7,6 +7,8 @@ import java.util.HashMap; import java.util.Map; import java.util.TreeMap; +import java.util.concurrent.TimeUnit; +import java.util.concurrent.TimeoutException; import okhttp3.Headers; import okhttp3.OkHttpClient; @@ -16,6 +18,7 @@ import okhttp3.WebSocketListener; import okio.ByteString; import rx.Completable; +import rx.subjects.BehaviorSubject; class OkHttpConnectionProvider extends AbstractConnectionProvider { @@ -24,6 +27,7 @@ class OkHttpConnectionProvider extends AbstractConnectionProvider { private final Map mConnectHttpHeaders; private final OkHttpClient mOkHttpClient; private final String tag = OkHttpConnectionProvider.class.getSimpleName(); + private final BehaviorSubject mConnectionStream; @Nullable private WebSocket openedSocked; @@ -33,24 +37,50 @@ class OkHttpConnectionProvider extends AbstractConnectionProvider { mUri = uri; mConnectHttpHeaders = connectHttpHeaders != null ? connectHttpHeaders : new HashMap<>(); mOkHttpClient = okHttpClient; + mConnectionStream = BehaviorSubject.create(false); + mConnectionStream.subscribe(value -> Log.d(tag, "Connection stream emitted: " + value)); } @NonNull @Override public Completable disconnect() { + Completable block = mConnectionStream + .first(isConnected -> isConnected) + .timeout(1, TimeUnit.SECONDS) + .doOnError(error -> { + if (error.getClass().equals(TimeoutException.class)) + Log.e(tag, "Attempted to disconnect when already disconnected"); + }) + .toCompletable(); + + /* if (openedSocked == null) { return Completable.error(new IllegalStateException("Attempted to disconnect when already disconnected")); } + */ + return Completable - .fromAction(() -> openedSocked.close(1000, "")); + .fromAction(() -> openedSocked.close(1000, "")) + .startWith(block); } @Override void createWebSocketConnection() { - + Completable block = mConnectionStream + .first(isConnected -> !isConnected) + .timeout(1, TimeUnit.SECONDS) + .doOnError(error -> { + if (error.getClass().equals(TimeoutException.class)) + Log.e(tag, "Attempted to connect when already connected"); + }) + .toCompletable(); + block.get(); // todo: do this the right way + + /* if (openedSocked != null) { throw new IllegalStateException("Already have connection to web socket"); } + */ Request.Builder requestBuilder = new Request.Builder() .url(mUri); @@ -86,6 +116,7 @@ public void onMessage(WebSocket webSocket, @NonNull ByteString bytes) { public void onClosed(WebSocket webSocket, int code, String reason) { emitLifecycleEvent(new LifecycleEvent(LifecycleEvent.Type.CLOSED)); openedSocked = null; + mConnectionStream.onNext(false); } @Override @@ -95,6 +126,7 @@ public void onFailure(WebSocket webSocket, Throwable t, Response response) { } ); + mConnectionStream.onNext(true); } @Override diff --git a/lib/src/main/java/ua/naiksoftware/stomp/client/StompClient.java b/lib/src/main/java/ua/naiksoftware/stomp/client/StompClient.java index 5c96a87..6b7697f 100644 --- a/lib/src/main/java/ua/naiksoftware/stomp/client/StompClient.java +++ b/lib/src/main/java/ua/naiksoftware/stomp/client/StompClient.java @@ -14,6 +14,7 @@ import java8.util.concurrent.CompletableFuture; import rx.Completable; import rx.Observable; +import rx.Subscription; import rx.schedulers.Schedulers; import rx.subjects.PublishSubject; import ua.naiksoftware.stomp.ConnectionProvider; @@ -41,6 +42,8 @@ public class StompClient { private Completable mConnectionComplete; private HashMap> mStreamMap; private Parser parser; + private Subscription lifecycleSub; + private List mHeaders; public StompClient(ConnectionProvider connectionProvider) { mConnectionProvider = connectionProvider; @@ -82,28 +85,16 @@ public void connect() { connect(null); } - public void connect(boolean reconnect) { - connect(null, reconnect); - } - - /** - * Connect without reconnect if connected - * - * @param _headers HTTP headers to send in the INITIAL REQUEST, i.e. during the protocol upgrade - */ - public void connect(List _headers) { - connect(_headers, false); - } - /** * If already connected and reconnect=false - nope * * @param _headers HTTP headers to send in the INITIAL REQUEST, i.e. during the protocol upgrade */ - public void connect(@Nullable List _headers, boolean reconnect) { - if (reconnect) disconnect(); + public void connect(@Nullable List _headers) { + mHeaders = _headers; + if (mConnected) return; - lifecycle() + lifecycleSub = lifecycle() .subscribe(lifecycleEvent -> { switch (lifecycleEvent.getType()) { case OPENED: @@ -138,6 +129,14 @@ public void connect(@Nullable List _headers, boolean reconnect) { }); } + /** + * Disconnect from server, and then reconnect with the last-used headers + */ + public void reconnect() { + disconnect(); + connect(mHeaders); + } + public Completable send(String destination) { return send(destination, null); } @@ -164,6 +163,7 @@ public Observable lifecycle() { public void disconnect() { resetStatus(); + lifecycleSub.unsubscribe(); mConnectionProvider.disconnect().subscribe(() -> mConnected = false); } From 2067dfcc61b2ed54e1f6ddc1d3fdeffa1c81d722 Mon Sep 17 00:00:00 2001 From: forresthopkinsa Date: Tue, 15 Aug 2017 16:47:24 -0700 Subject: [PATCH 15/33] Abstracted reconnect logic and implemented it for JWS (JWS is still totally untested) --- .../stomp/AbstractConnectionProvider.java | 46 ++++++++++++++--- .../stomp/OkHttpConnectionProvider.java | 49 ++----------------- .../stomp/WebSocketsConnectionProvider.java | 9 ++-- 3 files changed, 47 insertions(+), 57 deletions(-) diff --git a/lib/src/main/java/ua/naiksoftware/stomp/AbstractConnectionProvider.java b/lib/src/main/java/ua/naiksoftware/stomp/AbstractConnectionProvider.java index be5eb51..d22bc91 100644 --- a/lib/src/main/java/ua/naiksoftware/stomp/AbstractConnectionProvider.java +++ b/lib/src/main/java/ua/naiksoftware/stomp/AbstractConnectionProvider.java @@ -4,8 +4,11 @@ import android.support.annotation.Nullable; import android.util.Log; +import java.util.concurrent.TimeUnit; + import rx.Completable; import rx.Observable; +import rx.subjects.BehaviorSubject; import rx.subjects.PublishSubject; /** @@ -22,29 +25,56 @@ abstract class AbstractConnectionProvider implements ConnectionProvider { private final PublishSubject mLifecycleStream; @NonNull private final PublishSubject mMessagesStream; + final BehaviorSubject mConnectionStream; AbstractConnectionProvider() { mLifecycleStream = PublishSubject.create(); mMessagesStream = PublishSubject.create(); + mConnectionStream = BehaviorSubject.create(false); } @NonNull @Override public Observable messages() { - createWebSocketConnection(); - return mMessagesStream; + return mMessagesStream.startWith(initSocket().toObservable()); } /** - * Completable to close socket. + * Simply close socket. *

    * For example: *

    -     * return Completable.fromAction(() -> webSocket.close());
    +     * webSocket.close();
          * 
    */ + abstract void rawDisconnect(); + @Override - public abstract Completable disconnect(); + public Completable disconnect() { + Observable ex = Observable.error(new IllegalStateException("Attempted to disconnect when already disconnected")); + + Completable block = mConnectionStream + .first(isConnected -> isConnected) + .timeout(1, TimeUnit.SECONDS, ex) + .toCompletable(); + + return Completable + .fromAction(this::rawDisconnect) + .startWith(block); + } + + private Completable initSocket() { + Observable ex = Observable.error(new IllegalStateException("Attempted to connect when already connected")); + + Completable block = mConnectionStream + .first(isConnected -> !isConnected) + .timeout(1, TimeUnit.SECONDS, ex) + .toCompletable(); + + return Completable + .fromAction(this::createWebSocketConnection) + .startWith(block); + } /** * Most important method: connects to websocket and notifies program of messages. @@ -61,7 +91,7 @@ public Completable send(String stompMessage) { throw new IllegalStateException("Not connected yet"); } else { Log.d(TAG, "Send STOMP message: " + stompMessage); - bareSend(stompMessage); + rawSend(stompMessage); return null; } }); @@ -77,7 +107,7 @@ public Completable send(String stompMessage) { * * @param stompMessage message to send */ - abstract void bareSend(String stompMessage); + abstract void rawSend(String stompMessage); /** * Get socket object. @@ -94,6 +124,8 @@ public Completable send(String stompMessage) { void emitLifecycleEvent(@NonNull LifecycleEvent lifecycleEvent) { Log.d(TAG, "Emit lifecycle event: " + lifecycleEvent.getType().name()); mLifecycleStream.onNext(lifecycleEvent); + if (lifecycleEvent.getType().equals(LifecycleEvent.Type.CLOSED)) + mConnectionStream.onNext(false); } void emitMessage(String stompMessage) { diff --git a/lib/src/main/java/ua/naiksoftware/stomp/OkHttpConnectionProvider.java b/lib/src/main/java/ua/naiksoftware/stomp/OkHttpConnectionProvider.java index 259031d..945b11d 100644 --- a/lib/src/main/java/ua/naiksoftware/stomp/OkHttpConnectionProvider.java +++ b/lib/src/main/java/ua/naiksoftware/stomp/OkHttpConnectionProvider.java @@ -7,8 +7,6 @@ import java.util.HashMap; import java.util.Map; import java.util.TreeMap; -import java.util.concurrent.TimeUnit; -import java.util.concurrent.TimeoutException; import okhttp3.Headers; import okhttp3.OkHttpClient; @@ -17,8 +15,6 @@ import okhttp3.WebSocket; import okhttp3.WebSocketListener; import okio.ByteString; -import rx.Completable; -import rx.subjects.BehaviorSubject; class OkHttpConnectionProvider extends AbstractConnectionProvider { @@ -27,7 +23,6 @@ class OkHttpConnectionProvider extends AbstractConnectionProvider { private final Map mConnectHttpHeaders; private final OkHttpClient mOkHttpClient; private final String tag = OkHttpConnectionProvider.class.getSimpleName(); - private final BehaviorSubject mConnectionStream; @Nullable private WebSocket openedSocked; @@ -37,51 +32,16 @@ class OkHttpConnectionProvider extends AbstractConnectionProvider { mUri = uri; mConnectHttpHeaders = connectHttpHeaders != null ? connectHttpHeaders : new HashMap<>(); mOkHttpClient = okHttpClient; - mConnectionStream = BehaviorSubject.create(false); - mConnectionStream.subscribe(value -> Log.d(tag, "Connection stream emitted: " + value)); } @NonNull @Override - public Completable disconnect() { - Completable block = mConnectionStream - .first(isConnected -> isConnected) - .timeout(1, TimeUnit.SECONDS) - .doOnError(error -> { - if (error.getClass().equals(TimeoutException.class)) - Log.e(tag, "Attempted to disconnect when already disconnected"); - }) - .toCompletable(); - - /* - if (openedSocked == null) { - return Completable.error(new IllegalStateException("Attempted to disconnect when already disconnected")); - } - */ - - return Completable - .fromAction(() -> openedSocked.close(1000, "")) - .startWith(block); + public void rawDisconnect() { + openedSocked.close(1000, ""); } @Override void createWebSocketConnection() { - Completable block = mConnectionStream - .first(isConnected -> !isConnected) - .timeout(1, TimeUnit.SECONDS) - .doOnError(error -> { - if (error.getClass().equals(TimeoutException.class)) - Log.e(tag, "Attempted to connect when already connected"); - }) - .toCompletable(); - block.get(); // todo: do this the right way - - /* - if (openedSocked != null) { - throw new IllegalStateException("Already have connection to web socket"); - } - */ - Request.Builder requestBuilder = new Request.Builder() .url(mUri); @@ -114,9 +74,8 @@ public void onMessage(WebSocket webSocket, @NonNull ByteString bytes) { @Override public void onClosed(WebSocket webSocket, int code, String reason) { - emitLifecycleEvent(new LifecycleEvent(LifecycleEvent.Type.CLOSED)); openedSocked = null; - mConnectionStream.onNext(false); + emitLifecycleEvent(new LifecycleEvent(LifecycleEvent.Type.CLOSED)); } @Override @@ -130,7 +89,7 @@ public void onFailure(WebSocket webSocket, Throwable t, Response response) { } @Override - void bareSend(String stompMessage) { + void rawSend(String stompMessage) { openedSocked.send(stompMessage); } diff --git a/lib/src/main/java/ua/naiksoftware/stomp/WebSocketsConnectionProvider.java b/lib/src/main/java/ua/naiksoftware/stomp/WebSocketsConnectionProvider.java index c672df4..4f93085 100644 --- a/lib/src/main/java/ua/naiksoftware/stomp/WebSocketsConnectionProvider.java +++ b/lib/src/main/java/ua/naiksoftware/stomp/WebSocketsConnectionProvider.java @@ -20,8 +20,6 @@ import javax.net.ssl.SSLContext; import javax.net.ssl.SSLSocketFactory; -import rx.Completable; - /** * Created by naik on 05.05.16. */ @@ -49,8 +47,8 @@ class WebSocketsConnectionProvider extends AbstractConnectionProvider { @NonNull @Override - public Completable disconnect() { - return Completable.fromAction(() -> mWebSocketClient.close()); + public void rawDisconnect() { + mWebSocketClient.close(); } @Override @@ -112,10 +110,11 @@ public void onError(Exception ex) { mWebSocketClient.connect(); haveConnection = true; + mConnectionStream.onNext(true); } @Override - void bareSend(String stompMessage) { + void rawSend(String stompMessage) { mWebSocketClient.send(stompMessage); } From 3880e3cd25f7cc6f06d3cd12f854262fc27c8512 Mon Sep 17 00:00:00 2001 From: forresthopkinsa Date: Tue, 19 Sep 2017 10:05:58 -0700 Subject: [PATCH 16/33] Started on heartbeats, fixed example --- README.md | 37 +++++++++++++------ build.gradle | 2 +- .../stompclientexample/MainActivity.java | 30 +++++---------- gradle/wrapper/gradle-wrapper.properties | 4 +- .../stomp/AbstractConnectionProvider.java | 5 +++ .../stomp/ConnectionProvider.java | 2 + .../java/ua/naiksoftware/stomp/Stomp.java | 2 +- .../stomp/client/StompClient.java | 16 +++++++- 8 files changed, 61 insertions(+), 37 deletions(-) diff --git a/README.md b/README.md index c4e8748..2516c19 100644 --- a/README.md +++ b/README.md @@ -1,15 +1,17 @@ -# STOMP protocol via WebSocket for Android +# Websockets on Android [![Release](https://jitpack.io/v/forresthopkinsa/StompProtocolAndroid.svg)](https://jitpack.io/#forresthopkinsa/StompProtocolAndroid) ## Overview -**Note that this is a FORK of a project by NaikSoftware! This version is made to avoid using RetroLambda! (Scroll down to see other changes)** +**Note that this is a FORK of a project by NaikSoftware! This version was originally made to avoid using RetroLambda.** + +*(It now has [many other differences](#changes-in-this-fork).)* This library provides support for [STOMP protocol](https://stomp.github.io/) over Websockets. -At now library works only as client for any backend that supports STOMP, such as -NodeJS (e.g. using StompJS) or Spring Boot ([with WebSocket support](https://spring.io/guides/gs/messaging-stomp-websocket/)). +Right now, the library works as a client for any backend that supports STOMP, such as +Node.js (e.g. using StompJS) or Spring Boot ([with WebSocket support](https://spring.io/guides/gs/messaging-stomp-websocket/)). Add library as gradle dependency (Versioning info [here](https://jitpack.io/#forresthopkinsa/StompProtocolAndroid)): @@ -28,12 +30,10 @@ You can use this library two ways: - Using the old JACK toolchain - If you have Java 8 compatiblity and Jack enabled, this library will work for you - Using the new Native Java 8 support - - As of this writing, you must be using Android Studio Canary to use this feature. + - As of this writing, you must be using Android Studio Beta to use this feature. - Has been tested in the following environments: - - Beta 2, Gradle plugin v3.0.0-beta2 - - Beta 1, Gradle plugin v3.0.0-beta1 - - Canary 9, Gradle plugin v3.0.0-alpha9 - - Canary 8, Gradle plugin v3.0.0-alpha8 + - Beta 1-9, Gradle plugin v3.0.0-beta(1-9) + - Canary 8-9, Gradle plugin v3.0.0-alpha(8-9) - It *should* work in all 3.0.0+ versions - You can find more info on the [Releases Page](https://github.com/forresthopkinsa/StompProtocolAndroid/releases) @@ -101,7 +101,7 @@ Check out the [upstream example server](https://github.com/NaikSoftware/stomp-pr }); client.send("/app/hello", "world").subscribe( - aVoid -> Log.d(TAG, "Sent data!"), + () -> Log.d(TAG, "Sent data!"), error -> Log.e(TAG, "Encountered error while sending data!", error) ); @@ -131,8 +131,8 @@ client.lifecycle().subscribe(lifecycleEvent -> { Log.d(TAG, "Stomp connection opened"); break; case CLOSED: - Log.d(TAG, "Stomp connection closed"); - break; + Log.d(TAG, "Stomp connection closed"); + break; case ERROR: Log.e(TAG, "Stomp connection error", lifecycleEvent.getException()); break; @@ -152,6 +152,16 @@ Yes, it's safe to pass `null` for either (or both) of the last two arguments. Th Note: This method is only supported using OkHttp, not JWS. +**Heartbeating** + +STOMP Heartbeat implementation is in progress. Right now, you can send a heartbeat request header upon initial websocket connect: + +``` java +// ask server to send us heartbeat every ten seconds +client.setHeartbeat(10000); +client.connect(); +``` + **Support** Right now, the library only supports sending and receiving messages. ACK messages and transactions are not implemented yet. @@ -254,6 +264,9 @@ These are the possible changes you need to make to your code for this branch, if Log.i(TAG, "Received message: " + message.getPayload()); }); ``` +- Rudimentary heartbeat mechanism + - You can use `StompClient.setHeartbeat(ms interval)` to send a heartbeat header to the server + - WIP; currently we don't deal with those heartbeats in any way other than printing them to console ## Additional Reading diff --git a/build.gradle b/build.gradle index 333cf60..5625c67 100644 --- a/build.gradle +++ b/build.gradle @@ -8,7 +8,7 @@ buildscript { } } dependencies { - classpath 'com.android.tools.build:gradle:3.0.0-beta2' + classpath 'com.android.tools.build:gradle:3.0.0-beta6' classpath 'com.github.dcendents:android-maven-gradle-plugin:1.5' // NOTE: Do not place your application dependencies here; they belong diff --git a/example-client/src/main/java/ua/naiksoftware/stompclientexample/MainActivity.java b/example-client/src/main/java/ua/naiksoftware/stompclientexample/MainActivity.java index 91bc44a..28a7ac9 100644 --- a/example-client/src/main/java/ua/naiksoftware/stompclientexample/MainActivity.java +++ b/example-client/src/main/java/ua/naiksoftware/stompclientexample/MainActivity.java @@ -1,7 +1,7 @@ package ua.naiksoftware.stompclientexample; -import android.support.v7.app.AppCompatActivity; import android.os.Bundle; +import android.support.v7.app.AppCompatActivity; import android.support.v7.widget.LinearLayoutManager; import android.support.v7.widget.RecyclerView; import android.util.Log; @@ -11,15 +11,12 @@ import com.google.gson.Gson; import com.google.gson.GsonBuilder; -import org.java_websocket.WebSocket; - import java.text.SimpleDateFormat; import java.util.ArrayList; import java.util.Date; import java.util.List; import java.util.Locale; -import rx.Observable; import rx.Subscription; import rx.android.schedulers.AndroidSchedulers; import rx.schedulers.Schedulers; @@ -56,7 +53,7 @@ public void disconnectStomp(View view) { } public void connectStomp(View view) { - mStompClient = Stomp.over(WebSocket.class, "ws://" + ANDROID_EMULATOR_LOCALHOST + mStompClient = Stomp.over(Stomp.ConnectionProvider.JWS, "ws://" + ANDROID_EMULATOR_LOCALHOST + ":" + RestClient.SERVER_PORT + "/example-endpoint/websocket"); mStompClient.lifecycle() @@ -90,10 +87,10 @@ public void connectStomp(View view) { public void sendEchoViaStomp(View v) { mStompClient.send("/topic/hello-msg-mapping", "Echo STOMP " + mTimeFormat.format(new Date())) - .compose(applySchedulers()) - .subscribe(aVoid -> { - Log.d(TAG, "STOMP echo send successfully"); - }, throwable -> { + .unsubscribeOn(Schedulers.newThread()) + .subscribeOn(Schedulers.io()) + .observeOn(AndroidSchedulers.mainThread()) + .subscribe(() -> Log.d(TAG, "STOMP echo send successfully"), throwable -> { Log.e(TAG, "Error send STOMP echo", throwable); toast(throwable.getMessage()); }); @@ -102,10 +99,10 @@ public void sendEchoViaStomp(View v) { public void sendEchoViaRest(View v) { mRestPingSubscription = RestClient.getInstance().getExampleRepository() .sendRestEcho("Echo REST " + mTimeFormat.format(new Date())) - .compose(applySchedulers()) - .subscribe(aVoid -> { - Log.d(TAG, "REST echo send successfully"); - }, throwable -> { + .unsubscribeOn(Schedulers.newThread()) + .subscribeOn(Schedulers.io()) + .observeOn(AndroidSchedulers.mainThread()) + .subscribe(aVoid -> Log.d(TAG, "REST echo send successfully"), throwable -> { Log.e(TAG, "Error send REST echo", throwable); toast(throwable.getMessage()); }); @@ -122,13 +119,6 @@ private void toast(String text) { Toast.makeText(this, text, Toast.LENGTH_SHORT).show(); } - protected Observable.Transformer applySchedulers() { - return rObservable -> rObservable - .unsubscribeOn(Schedulers.newThread()) - .subscribeOn(Schedulers.io()) - .observeOn(AndroidSchedulers.mainThread()); - } - @Override protected void onDestroy() { mStompClient.disconnect(); diff --git a/gradle/wrapper/gradle-wrapper.properties b/gradle/wrapper/gradle-wrapper.properties index 168b761..d72e173 100644 --- a/gradle/wrapper/gradle-wrapper.properties +++ b/gradle/wrapper/gradle-wrapper.properties @@ -1,6 +1,6 @@ -#Tue Aug 01 08:03:03 MST 2017 +#Tue Sep 05 08:26:08 MST 2017 distributionBase=GRADLE_USER_HOME distributionPath=wrapper/dists zipStoreBase=GRADLE_USER_HOME zipStorePath=wrapper/dists -distributionUrl=https\://services.gradle.org/distributions/gradle-4.1-rc-1-all.zip +distributionUrl=https\://services.gradle.org/distributions/gradle-4.1-all.zip diff --git a/lib/src/main/java/ua/naiksoftware/stomp/AbstractConnectionProvider.java b/lib/src/main/java/ua/naiksoftware/stomp/AbstractConnectionProvider.java index d22bc91..9e2982b 100644 --- a/lib/src/main/java/ua/naiksoftware/stomp/AbstractConnectionProvider.java +++ b/lib/src/main/java/ua/naiksoftware/stomp/AbstractConnectionProvider.java @@ -76,6 +76,11 @@ private Completable initSocket() { .startWith(block); } + // Doesn't do anything at all, only here as a stub + public Completable setHeartbeat(int ms) { + return Completable.complete(); + } + /** * Most important method: connects to websocket and notifies program of messages. *

    diff --git a/lib/src/main/java/ua/naiksoftware/stomp/ConnectionProvider.java b/lib/src/main/java/ua/naiksoftware/stomp/ConnectionProvider.java index 7866527..0d18ac0 100644 --- a/lib/src/main/java/ua/naiksoftware/stomp/ConnectionProvider.java +++ b/lib/src/main/java/ua/naiksoftware/stomp/ConnectionProvider.java @@ -30,4 +30,6 @@ public interface ConnectionProvider { * Automatically emits Lifecycle.CLOSE */ Completable disconnect(); + + Completable setHeartbeat(int ms); } diff --git a/lib/src/main/java/ua/naiksoftware/stomp/Stomp.java b/lib/src/main/java/ua/naiksoftware/stomp/Stomp.java index 31fba3e..1eb7658 100644 --- a/lib/src/main/java/ua/naiksoftware/stomp/Stomp.java +++ b/lib/src/main/java/ua/naiksoftware/stomp/Stomp.java @@ -50,7 +50,7 @@ public static StompClient over(@NonNull ConnectionProvider connectionProvider, S public static StompClient over(@NonNull ConnectionProvider connectionProvider, String uri, @Nullable Map connectHttpHeaders, @Nullable OkHttpClient okHttpClient) { if (connectionProvider == ConnectionProvider.JWS) { if (okHttpClient != null) { - throw new IllegalArgumentException("You cannot pass a webSocketClient with 'org.java_websocket.WebSocket'. use null instead."); + throw new IllegalArgumentException("You cannot pass an OkHttpClient when using JWS. Use null instead."); } return createStompClient(new WebSocketsConnectionProvider(uri, connectHttpHeaders)); } diff --git a/lib/src/main/java/ua/naiksoftware/stomp/client/StompClient.java b/lib/src/main/java/ua/naiksoftware/stomp/client/StompClient.java index 6b7697f..a100dfc 100644 --- a/lib/src/main/java/ua/naiksoftware/stomp/client/StompClient.java +++ b/lib/src/main/java/ua/naiksoftware/stomp/client/StompClient.java @@ -44,6 +44,7 @@ public class StompClient { private Parser parser; private Subscription lifecycleSub; private List mHeaders; + private int heartbeat; public StompClient(ConnectionProvider connectionProvider) { mConnectionProvider = connectionProvider; @@ -78,6 +79,18 @@ public void setParser(Parser parser) { this.parser = parser; } + /** + * Sets the heartbeat interval to request from the server. + *

    + * Not very useful yet, because we don't have any heartbeat logic on our side. + * + * @param ms heartbeat time in milliseconds + */ + public void setHeartbeat(int ms) { + heartbeat = ms; + mConnectionProvider.setHeartbeat(ms).subscribe(); + } + /** * Connect without reconnect if connected */ @@ -86,7 +99,7 @@ public void connect() { } /** - * If already connected and reconnect=false - nope + * Connect to websocket. If already connected, this will silently fail. * * @param _headers HTTP headers to send in the INITIAL REQUEST, i.e. during the protocol upgrade */ @@ -100,6 +113,7 @@ public void connect(@Nullable List _headers) { case OPENED: List headers = new ArrayList<>(); headers.add(new StompHeader(StompHeader.VERSION, SUPPORTED_VERSIONS)); + headers.add(new StompHeader(StompHeader.HEART_BEAT, "0," + heartbeat)); if (_headers != null) headers.addAll(_headers); mConnectionProvider.send(new StompMessage(StompCommand.CONNECT, headers, null).compile()) .subscribe(); From 24da370ff803daae39501faf8dfbb86b2acdf30e Mon Sep 17 00:00:00 2001 From: forresthopkinsa Date: Tue, 19 Sep 2017 13:20:16 -0700 Subject: [PATCH 17/33] Huge cleanup, ready for release --- .gitignore | 8 ++----- example-client/src/main/AndroidManifest.xml | 18 +++++++------- .../stompclientexample/ExampleUnitTest.java | 2 +- lib/src/main/AndroidManifest.xml | 5 ++-- .../java/ua/naiksoftware/stomp/Stomp.java | 24 +++++++++---------- .../stomp/WebSocketsConnectionProvider.java | 3 ++- .../naiksoftware/stomp/ExampleUnitTest.java | 2 +- 7 files changed, 29 insertions(+), 33 deletions(-) diff --git a/.gitignore b/.gitignore index a814809..9a4a2d1 100644 --- a/.gitignore +++ b/.gitignore @@ -1,11 +1,7 @@ *.iml .gradle /local.properties -/.idea/workspace.xml -/.idea/libraries -/.idea/misc.xml -/.idea/modules.xml -/.idea/vcs.xml +.idea .DS_Store /build -/captures +/captures \ No newline at end of file diff --git a/example-client/src/main/AndroidManifest.xml b/example-client/src/main/AndroidManifest.xml index 8acdccd..2b1f999 100644 --- a/example-client/src/main/AndroidManifest.xml +++ b/example-client/src/main/AndroidManifest.xml @@ -1,20 +1,20 @@ + package="ua.naiksoftware.stompclientexample"> - + + android:allowBackup="true" + android:icon="@mipmap/ic_launcher" + android:label="@string/app_name" + android:supportsRtl="true" + android:theme="@style/AppTheme"> - + - + diff --git a/example-client/src/test/java/ua/naiksoftware/stompclientexample/ExampleUnitTest.java b/example-client/src/test/java/ua/naiksoftware/stompclientexample/ExampleUnitTest.java index 4795c29..ac03654 100644 --- a/example-client/src/test/java/ua/naiksoftware/stompclientexample/ExampleUnitTest.java +++ b/example-client/src/test/java/ua/naiksoftware/stompclientexample/ExampleUnitTest.java @@ -2,7 +2,7 @@ import org.junit.Test; -import static org.junit.Assert.*; +import static org.junit.Assert.assertEquals; /** * To work on unit tests, switch the Test Artifact in the Build Variants view. diff --git a/lib/src/main/AndroidManifest.xml b/lib/src/main/AndroidManifest.xml index 8fdbc9a..58c8122 100644 --- a/lib/src/main/AndroidManifest.xml +++ b/lib/src/main/AndroidManifest.xml @@ -1,6 +1,5 @@ - + - + diff --git a/lib/src/main/java/ua/naiksoftware/stomp/Stomp.java b/lib/src/main/java/ua/naiksoftware/stomp/Stomp.java index 1eb7658..f64d60d 100644 --- a/lib/src/main/java/ua/naiksoftware/stomp/Stomp.java +++ b/lib/src/main/java/ua/naiksoftware/stomp/Stomp.java @@ -10,12 +10,12 @@ /** * Supported overlays: - * - org.java_websocket.WebSocket ('org.java-websocket:Java-WebSocket:1.3.2') - * - okhttp3.WebSocket ('com.squareup.okhttp3:okhttp:3.8.1') - * - * You can add own relay, just implement ConnectionProvider for you stomp transport, - * such as web socket. - * + * - org.java_websocket.WebSocket ('org.java-websocket:Java-WebSocket:1.3.2') + * - okhttp3.WebSocket ('com.squareup.okhttp3:okhttp:3.8.1') + *

    + * You can add own relay, just implement ConnectionProvider for you stomp transport, + * such as web socket. + *

    * Created by naik on 05.05.16. */ public class Stomp { @@ -25,9 +25,8 @@ public static StompClient over(@NonNull ConnectionProvider connectionProvider, S } /** - * * @param connectionProvider connectionProvider method - * @param uri URI to connect + * @param uri URI to connect * @param connectHttpHeaders HTTP headers, will be passed with handshake query, may be null * @return StompClient for receiving and sending messages. Call #StompClient.connect */ @@ -38,13 +37,14 @@ public static StompClient over(@NonNull ConnectionProvider connectionProvider, S /** * {@code webSocketClient} can accept the following type of clients: *

      - *
    • {@code org.java_websocket.WebSocket}: cannot accept an existing client
    • - *
    • {@code okhttp3.WebSocket}: can accept a non-null instance of {@code okhttp3.OkHttpClient}
    • + *
    • {@code org.java_websocket.WebSocket}: cannot accept an existing client
    • + *
    • {@code okhttp3.WebSocket}: can accept a non-null instance of {@code okhttp3.OkHttpClient}
    • *
    + * * @param connectionProvider connectionProvider method - * @param uri URI to connect + * @param uri URI to connect * @param connectHttpHeaders HTTP headers, will be passed with handshake query, may be null - * @param okHttpClient Existing client that will be used to open the WebSocket connection, may be null to use default client + * @param okHttpClient Existing client that will be used to open the WebSocket connection, may be null to use default client * @return StompClient for receiving and sending messages. Call #StompClient.connect */ public static StompClient over(@NonNull ConnectionProvider connectionProvider, String uri, @Nullable Map connectHttpHeaders, @Nullable OkHttpClient okHttpClient) { diff --git a/lib/src/main/java/ua/naiksoftware/stomp/WebSocketsConnectionProvider.java b/lib/src/main/java/ua/naiksoftware/stomp/WebSocketsConnectionProvider.java index 4f93085..372afab 100644 --- a/lib/src/main/java/ua/naiksoftware/stomp/WebSocketsConnectionProvider.java +++ b/lib/src/main/java/ua/naiksoftware/stomp/WebSocketsConnectionProvider.java @@ -38,6 +38,7 @@ class WebSocketsConnectionProvider extends AbstractConnectionProvider { /** * Support UIR scheme ws://host:port/path + * * @param connectHttpHeaders may be null */ WebSocketsConnectionProvider(String uri, @Nullable Map connectHttpHeaders) { @@ -97,7 +98,7 @@ public void onError(Exception ex) { } }; - if(mUri.startsWith("wss")) { + if (mUri.startsWith("wss")) { try { SSLContext sc = SSLContext.getInstance("TLS"); sc.init(null, null, null); diff --git a/lib/src/test/java/ua/naiksoftware/stomp/ExampleUnitTest.java b/lib/src/test/java/ua/naiksoftware/stomp/ExampleUnitTest.java index 082e58a..3b89b15 100644 --- a/lib/src/test/java/ua/naiksoftware/stomp/ExampleUnitTest.java +++ b/lib/src/test/java/ua/naiksoftware/stomp/ExampleUnitTest.java @@ -2,7 +2,7 @@ import org.junit.Test; -import static org.junit.Assert.*; +import static org.junit.Assert.assertEquals; /** * To work on unit tests, switch the Test Artifact in the Build Variants view. From cb10e8b3c11ec68967e3d13ee3062f368d1e7e1f Mon Sep 17 00:00:00 2001 From: forresthopkinsa Date: Fri, 29 Sep 2017 14:46:13 -0700 Subject: [PATCH 18/33] Fixed protocol breach with message format; removed Clojars dependency. Also cleaned up some remnants of the merge in the example client. --- README.md | 6 +++++- build.gradle | 1 - example-client/build.gradle | 2 +- .../stompclientexample/ExampleRepository.java | 4 ++-- .../stompclientexample/RestClient.java | 4 ++-- lib/build.gradle | 2 +- .../stomp/client/StompClient.java | 20 +++++++++++++++++-- .../stomp/client/StompMessage.java | 8 +++++++- 8 files changed, 36 insertions(+), 11 deletions(-) diff --git a/README.md b/README.md index 2516c19..4324a4f 100644 --- a/README.md +++ b/README.md @@ -32,7 +32,7 @@ You can use this library two ways: - Using the new Native Java 8 support - As of this writing, you must be using Android Studio Beta to use this feature. - Has been tested in the following environments: - - Beta 1-9, Gradle plugin v3.0.0-beta(1-9) + - Beta 1-6, Gradle plugin v3.0.0-beta(1-6) - Canary 8-9, Gradle plugin v3.0.0-alpha(8-9) - It *should* work in all 3.0.0+ versions - You can find more info on the [Releases Page](https://github.com/forresthopkinsa/StompProtocolAndroid/releases) @@ -267,6 +267,10 @@ These are the possible changes you need to make to your code for this branch, if - Rudimentary heartbeat mechanism - You can use `StompClient.setHeartbeat(ms interval)` to send a heartbeat header to the server - WIP; currently we don't deal with those heartbeats in any way other than printing them to console +- Better adherence to STOMP spec + - According to the [spec](http://stomp.github.io/stomp-specification-1.2.html#STOMP_Frames), the end of the message body should be immediately followed by a NULL octet, marking the end of the frame. + - Before, we were adding an extra two newlines between the body and NULL octet, which was breaking compatibility with picky servers. + - This shouldn't make any difference, but if it does, you can revert to legacy formatting with `client.setLegacyWhitespace(true)`. ## Additional Reading diff --git a/build.gradle b/build.gradle index 5625c67..ae5ccda 100644 --- a/build.gradle +++ b/build.gradle @@ -20,7 +20,6 @@ allprojects { repositories { jcenter() maven { url "https://jitpack.io" } - maven { url "http://clojars.org/repo" } } } diff --git a/example-client/build.gradle b/example-client/build.gradle index b12b778..73a2602 100644 --- a/example-client/build.gradle +++ b/example-client/build.gradle @@ -28,7 +28,7 @@ dependencies { compile fileTree(dir: 'libs', include: ['*.jar']) testCompile 'junit:junit:4.12' compile 'com.android.support:appcompat-v7:25.2.0' - compile 'org.java-websocket:java-websocket:1.3.2' + compile 'org.java-websocket:Java-WebSocket:1.3.4' compile 'com.android.support:recyclerview-v7:25.2.0' compile 'io.reactivex:rxandroid:1.2.1' compile 'com.squareup.retrofit2:converter-gson:2.3.0' diff --git a/example-client/src/main/java/ua/naiksoftware/stompclientexample/ExampleRepository.java b/example-client/src/main/java/ua/naiksoftware/stompclientexample/ExampleRepository.java index b3beb47..b81f19a 100644 --- a/example-client/src/main/java/ua/naiksoftware/stompclientexample/ExampleRepository.java +++ b/example-client/src/main/java/ua/naiksoftware/stompclientexample/ExampleRepository.java @@ -1,8 +1,8 @@ package ua.naiksoftware.stompclientexample; -import io.reactivex.Flowable; import retrofit2.http.POST; import retrofit2.http.Query; +import rx.Observable; /** * Created by Naik on 24.02.17. @@ -10,5 +10,5 @@ public interface ExampleRepository { @POST("hello-convert-and-send") - Flowable sendRestEcho(@Query("msg") String message); + Observable sendRestEcho(@Query("msg") String message); } diff --git a/example-client/src/main/java/ua/naiksoftware/stompclientexample/RestClient.java b/example-client/src/main/java/ua/naiksoftware/stompclientexample/RestClient.java index 209a497..6c71f7d 100644 --- a/example-client/src/main/java/ua/naiksoftware/stompclientexample/RestClient.java +++ b/example-client/src/main/java/ua/naiksoftware/stompclientexample/RestClient.java @@ -1,7 +1,7 @@ package ua.naiksoftware.stompclientexample; import retrofit2.Retrofit; -import retrofit2.adapter.rxjava2.RxJava2CallAdapterFactory; +import retrofit2.adapter.rxjava.RxJavaCallAdapterFactory; import retrofit2.converter.gson.GsonConverterFactory; /** @@ -33,7 +33,7 @@ public static RestClient getInstance() { private RestClient() { Retrofit retrofit = new Retrofit.Builder().baseUrl("http://" + ANDROID_EMULATOR_LOCALHOST + ":" + SERVER_PORT + "/") .addConverterFactory(GsonConverterFactory.create()) - .addCallAdapterFactory(RxJava2CallAdapterFactory.create()) + .addCallAdapterFactory(RxJavaCallAdapterFactory.create()) .build(); mExampleRepository = retrofit.create(ExampleRepository.class); } diff --git a/lib/build.gradle b/lib/build.gradle index 1c4e625..8675db1 100644 --- a/lib/build.gradle +++ b/lib/build.gradle @@ -35,7 +35,7 @@ dependencies { compile 'net.sourceforge.streamsupport:streamsupport:1.5.5' compile 'net.sourceforge.streamsupport:streamsupport-cfuture:1.5.5' // Supported transports - compile 'org.java-websocket:java-websocket:1.3.2' + compile 'org.java-websocket:Java-WebSocket:1.3.4' compile 'com.squareup.okhttp3:okhttp:3.8.1' implementation 'com.android.support:support-annotations:24.2.0' } diff --git a/lib/src/main/java/ua/naiksoftware/stomp/client/StompClient.java b/lib/src/main/java/ua/naiksoftware/stomp/client/StompClient.java index 02562b3..ff90f01 100644 --- a/lib/src/main/java/ua/naiksoftware/stomp/client/StompClient.java +++ b/lib/src/main/java/ua/naiksoftware/stomp/client/StompClient.java @@ -36,6 +36,7 @@ public class StompClient { private ConcurrentHashMap mTopics; private boolean mConnected; private boolean isConnecting; + private boolean legacyWhitespace; private PublishSubject mMessageStream; private CompletableFuture mConnectionFuture; @@ -115,7 +116,7 @@ public void connect(@Nullable List _headers) { headers.add(new StompHeader(StompHeader.VERSION, SUPPORTED_VERSIONS)); headers.add(new StompHeader(StompHeader.HEART_BEAT, "0," + heartbeat)); if (_headers != null) headers.addAll(_headers); - mConnectionProvider.send(new StompMessage(StompCommand.CONNECT, headers, null).compile()) + mConnectionProvider.send(new StompMessage(StompCommand.CONNECT, headers, null).compile(legacyWhitespace)) .subscribe(); break; @@ -163,7 +164,7 @@ public Completable send(String destination, String data) { } public Completable send(@NonNull StompMessage stompMessage) { - Completable completable = mConnectionProvider.send(stompMessage.compile()); + Completable completable = mConnectionProvider.send(stompMessage.compile(legacyWhitespace)); return completable.startWith(mConnectionComplete); } @@ -199,6 +200,21 @@ else if (!mStreamMap.containsKey(destPath)) return mStreamMap.get(destPath); } + /** + * Reverts to the old frame formatting, which included two newlines between the message body + * and the end-of-frame marker. + *

    + * Before: Body\n\n^@ + *

    + * After: Body^@ + * + * @param legacyWhitespace whether to append an extra two newlines + * @see The STOMP spec + */ + public void setLegacyWhitespace(boolean legacyWhitespace) { + this.legacyWhitespace = legacyWhitespace; + } + private boolean matches(String path, StompMessage msg) { String dest = msg.findHeader(StompHeader.DESTINATION); if (dest == null) return false; diff --git a/lib/src/main/java/ua/naiksoftware/stomp/client/StompMessage.java b/lib/src/main/java/ua/naiksoftware/stomp/client/StompMessage.java index ee3619c..c5c2dda 100644 --- a/lib/src/main/java/ua/naiksoftware/stomp/client/StompMessage.java +++ b/lib/src/main/java/ua/naiksoftware/stomp/client/StompMessage.java @@ -54,6 +54,11 @@ public String findHeader(String key) { @NonNull public String compile() { + return compile(false); + } + + @NonNull + public String compile(boolean legacyWhitespace) { StringBuilder builder = new StringBuilder(); builder.append(mStompCommand).append('\n'); for (StompHeader header : mStompHeaders) { @@ -61,7 +66,8 @@ public String compile() { } builder.append('\n'); if (mPayload != null) { - builder.append(mPayload).append("\n\n"); + builder.append(mPayload); + if (legacyWhitespace) builder.append("\n\n"); } builder.append(TERMINATE_MESSAGE_SYMBOL); return builder.toString(); From a429a3ba40e3254361fa00d56ba32fac412061dd Mon Sep 17 00:00:00 2001 From: forresthopkinsa Date: Wed, 22 Nov 2017 09:03:55 -0700 Subject: [PATCH 19/33] Updating documentation and Gradle files --- README.md | 22 ++++++++++++++-------- build.gradle | 2 +- example-client/build.gradle | 1 - lib/build.gradle | 1 - 4 files changed, 15 insertions(+), 11 deletions(-) diff --git a/README.md b/README.md index 4324a4f..4d33ba9 100644 --- a/README.md +++ b/README.md @@ -6,7 +6,7 @@ **Note that this is a FORK of a project by NaikSoftware! This version was originally made to avoid using RetroLambda.** -*(It now has [many other differences](#changes-in-this-fork).)* +*(It now has [many other important differences](#changes-in-this-fork).)* This library provides support for [STOMP protocol](https://stomp.github.io/) over Websockets. @@ -21,7 +21,7 @@ repositories { maven { url "https://jitpack.io" } } dependencies { - compile 'com.github.forresthopkinsa:StompProtocolAndroid:1.4.0' + compile 'com.github.forresthopkinsa:StompProtocolAndroid:17.10.0' } ``` @@ -30,12 +30,8 @@ You can use this library two ways: - Using the old JACK toolchain - If you have Java 8 compatiblity and Jack enabled, this library will work for you - Using the new Native Java 8 support - - As of this writing, you must be using Android Studio Beta to use this feature. - - Has been tested in the following environments: - - Beta 1-6, Gradle plugin v3.0.0-beta(1-6) - - Canary 8-9, Gradle plugin v3.0.0-alpha(8-9) - - It *should* work in all 3.0.0+ versions - - You can find more info on the [Releases Page](https://github.com/forresthopkinsa/StompProtocolAndroid/releases) + - It should work in all Android Studio (and Gradle plugin) 3.0.0+ versions + - You can find compatibility info on the [Releases Page](https://github.com/forresthopkinsa/StompProtocolAndroid/releases) However, *this fork is NOT compatible with Retrolambda.* If you have RL as a dependency, then you should be using the [upstream version](https://github.com/NaikSoftware/StompProtocolAndroid) of this project! @@ -168,6 +164,15 @@ Right now, the library only supports sending and receiving messages. ACK message ## Changes in this fork +**Summary** + +Improvements: Most of the Rx logic has been rewritten, and a good portion of the other code has also been modified +to allow for more stability. Additionally, a lot of blocking code has been replaced with reactive code, +resulting in better performance. + +Drawbacks: In order to allow for major changes, this branch sacrifices backward compatibility. Code written +for the upstream will likely have to be modified to work with this version. You can find more details below. + **Build changes** The upstream master is based on Retrolambda. This version is based on Native Java 8 compilation, @@ -270,6 +275,7 @@ These are the possible changes you need to make to your code for this branch, if - Better adherence to STOMP spec - According to the [spec](http://stomp.github.io/stomp-specification-1.2.html#STOMP_Frames), the end of the message body should be immediately followed by a NULL octet, marking the end of the frame. - Before, we were adding an extra two newlines between the body and NULL octet, which was breaking compatibility with picky servers. + - Now, we format it correctly; there is no whitespace between the end of the body and the `\u0000`. - This shouldn't make any difference, but if it does, you can revert to legacy formatting with `client.setLegacyWhitespace(true)`. ## Additional Reading diff --git a/build.gradle b/build.gradle index ae5ccda..be064ef 100644 --- a/build.gradle +++ b/build.gradle @@ -8,7 +8,7 @@ buildscript { } } dependencies { - classpath 'com.android.tools.build:gradle:3.0.0-beta6' + classpath 'com.android.tools.build:gradle:3.0.1' classpath 'com.github.dcendents:android-maven-gradle-plugin:1.5' // NOTE: Do not place your application dependencies here; they belong diff --git a/example-client/build.gradle b/example-client/build.gradle index 73a2602..b3bd364 100644 --- a/example-client/build.gradle +++ b/example-client/build.gradle @@ -2,7 +2,6 @@ apply plugin: 'com.android.application' android { compileSdkVersion 25 - buildToolsVersion "25.0.2" defaultConfig { applicationId "ua.naiksoftware.stompclientexample" diff --git a/lib/build.gradle b/lib/build.gradle index 8675db1..2fa2922 100644 --- a/lib/build.gradle +++ b/lib/build.gradle @@ -5,7 +5,6 @@ group='com.github.forresthopkinsa' android { compileSdkVersion 25 - buildToolsVersion "25.0.2" defaultConfig { minSdkVersion 16 From 4695ad00a373647091e0f6dc0d5bc4889878aa7f Mon Sep 17 00:00:00 2001 From: forresthopkinsa Date: Mon, 27 Nov 2017 13:58:43 -0700 Subject: [PATCH 20/33] Updating JWS to new implementation (Draft 17 to 6455, RFC compliant) Fixes https://github.com/NaikSoftware/StompProtocolAndroid/issues/79 --- lib/build.gradle | 2 +- .../ua/naiksoftware/stomp/WebSocketsConnectionProvider.java | 5 ++--- 2 files changed, 3 insertions(+), 4 deletions(-) diff --git a/lib/build.gradle b/lib/build.gradle index 2fa2922..57a627a 100644 --- a/lib/build.gradle +++ b/lib/build.gradle @@ -34,7 +34,7 @@ dependencies { compile 'net.sourceforge.streamsupport:streamsupport:1.5.5' compile 'net.sourceforge.streamsupport:streamsupport-cfuture:1.5.5' // Supported transports - compile 'org.java-websocket:Java-WebSocket:1.3.4' + compile 'org.java-websocket:Java-WebSocket:1.3.6' compile 'com.squareup.okhttp3:okhttp:3.8.1' implementation 'com.android.support:support-annotations:24.2.0' } diff --git a/lib/src/main/java/ua/naiksoftware/stomp/WebSocketsConnectionProvider.java b/lib/src/main/java/ua/naiksoftware/stomp/WebSocketsConnectionProvider.java index 372afab..9ee4b16 100644 --- a/lib/src/main/java/ua/naiksoftware/stomp/WebSocketsConnectionProvider.java +++ b/lib/src/main/java/ua/naiksoftware/stomp/WebSocketsConnectionProvider.java @@ -6,7 +6,7 @@ import org.java_websocket.WebSocket; import org.java_websocket.client.WebSocketClient; -import org.java_websocket.drafts.Draft_17; +import org.java_websocket.drafts.Draft_6455; import org.java_websocket.exceptions.InvalidDataException; import org.java_websocket.handshake.ClientHandshake; import org.java_websocket.handshake.ServerHandshake; @@ -46,7 +46,6 @@ class WebSocketsConnectionProvider extends AbstractConnectionProvider { mConnectHttpHeaders = connectHttpHeaders != null ? connectHttpHeaders : new HashMap<>(); } - @NonNull @Override public void rawDisconnect() { mWebSocketClient.close(); @@ -57,7 +56,7 @@ void createWebSocketConnection() { if (haveConnection) throw new IllegalStateException("Already have connection to web socket"); - mWebSocketClient = new WebSocketClient(URI.create(mUri), new Draft_17(), mConnectHttpHeaders, 0) { + mWebSocketClient = new WebSocketClient(URI.create(mUri), new Draft_6455(), mConnectHttpHeaders, 0) { @Override public void onWebsocketHandshakeReceivedAsClient(WebSocket conn, ClientHandshake request, @NonNull ServerHandshake response) throws InvalidDataException { From 17b9d6c65bca90b509a539e460a1932e4dd5b802 Mon Sep 17 00:00:00 2001 From: forresthopkinsa Date: Mon, 27 Nov 2017 14:11:35 -0700 Subject: [PATCH 21/33] Deleting the old, non-functional example client and unit tests I'll replace these with a real example and real tests sometime soon --- .idea/gradle.xml | 1 - example-client/.gitignore | 1 - example-client/README.md | 3 - example-client/build.gradle | 37 ----- example-client/proguard-rules.pro | 17 --- .../stompclientexample/ApplicationTest.java | 13 -- example-client/src/main/AndroidManifest.xml | 22 --- .../stompclientexample/EchoModel.java | 20 --- .../stompclientexample/ExampleRepository.java | 14 -- .../stompclientexample/MainActivity.java | 128 ------------------ .../stompclientexample/RestClient.java | 44 ------ .../stompclientexample/SimpleAdapter.java | 51 ------- .../src/main/res/layout/activity_main.xml | 75 ---------- .../src/main/res/layout/item_layout.xml | 19 --- .../src/main/res/mipmap-hdpi/ic_launcher.png | Bin 3418 -> 0 bytes .../src/main/res/mipmap-mdpi/ic_launcher.png | Bin 2206 -> 0 bytes .../src/main/res/mipmap-xhdpi/ic_launcher.png | Bin 4842 -> 0 bytes .../main/res/mipmap-xxhdpi/ic_launcher.png | Bin 7718 -> 0 bytes .../main/res/mipmap-xxxhdpi/ic_launcher.png | Bin 10486 -> 0 bytes .../src/main/res/values-w820dp/dimens.xml | 6 - example-client/src/main/res/values/colors.xml | 6 - example-client/src/main/res/values/dimens.xml | 5 - .../src/main/res/values/strings.xml | 7 - example-client/src/main/res/values/styles.xml | 11 -- .../stompclientexample/ExampleUnitTest.java | 15 -- .../naiksoftware/stomp/ApplicationTest.java | 13 -- settings.gradle | 2 +- 27 files changed, 1 insertion(+), 509 deletions(-) delete mode 100644 example-client/.gitignore delete mode 100644 example-client/README.md delete mode 100644 example-client/build.gradle delete mode 100644 example-client/proguard-rules.pro delete mode 100644 example-client/src/androidTest/java/ua/naiksoftware/stompclientexample/ApplicationTest.java delete mode 100644 example-client/src/main/AndroidManifest.xml delete mode 100644 example-client/src/main/java/ua/naiksoftware/stompclientexample/EchoModel.java delete mode 100644 example-client/src/main/java/ua/naiksoftware/stompclientexample/ExampleRepository.java delete mode 100644 example-client/src/main/java/ua/naiksoftware/stompclientexample/MainActivity.java delete mode 100644 example-client/src/main/java/ua/naiksoftware/stompclientexample/RestClient.java delete mode 100644 example-client/src/main/java/ua/naiksoftware/stompclientexample/SimpleAdapter.java delete mode 100644 example-client/src/main/res/layout/activity_main.xml delete mode 100644 example-client/src/main/res/layout/item_layout.xml delete mode 100644 example-client/src/main/res/mipmap-hdpi/ic_launcher.png delete mode 100644 example-client/src/main/res/mipmap-mdpi/ic_launcher.png delete mode 100644 example-client/src/main/res/mipmap-xhdpi/ic_launcher.png delete mode 100644 example-client/src/main/res/mipmap-xxhdpi/ic_launcher.png delete mode 100644 example-client/src/main/res/mipmap-xxxhdpi/ic_launcher.png delete mode 100644 example-client/src/main/res/values-w820dp/dimens.xml delete mode 100644 example-client/src/main/res/values/colors.xml delete mode 100644 example-client/src/main/res/values/dimens.xml delete mode 100644 example-client/src/main/res/values/strings.xml delete mode 100644 example-client/src/main/res/values/styles.xml delete mode 100644 example-client/src/test/java/ua/naiksoftware/stompclientexample/ExampleUnitTest.java delete mode 100644 lib/src/androidTest/java/ua/naiksoftware/stomp/ApplicationTest.java diff --git a/.idea/gradle.xml b/.idea/gradle.xml index e9273c1..edcc4de 100644 --- a/.idea/gradle.xml +++ b/.idea/gradle.xml @@ -8,7 +8,6 @@ diff --git a/example-client/.gitignore b/example-client/.gitignore deleted file mode 100644 index 796b96d..0000000 --- a/example-client/.gitignore +++ /dev/null @@ -1 +0,0 @@ -/build diff --git a/example-client/README.md b/example-client/README.md deleted file mode 100644 index deabae9..0000000 --- a/example-client/README.md +++ /dev/null @@ -1,3 +0,0 @@ -## Example client for library - -Use server from here https://github.com/NaikSoftware/stomp-protocol-example-server \ No newline at end of file diff --git a/example-client/build.gradle b/example-client/build.gradle deleted file mode 100644 index b3bd364..0000000 --- a/example-client/build.gradle +++ /dev/null @@ -1,37 +0,0 @@ -apply plugin: 'com.android.application' - -android { - compileSdkVersion 25 - - defaultConfig { - applicationId "ua.naiksoftware.stompclientexample" - minSdkVersion 16 - targetSdkVersion 25 - versionCode 1 - versionName "1.0" - } - buildTypes { - release { - minifyEnabled false - proguardFiles getDefaultProguardFile('proguard-android.txt'), 'proguard-rules.pro' - } - } - - compileOptions { - sourceCompatibility JavaVersion.VERSION_1_8 - targetCompatibility JavaVersion.VERSION_1_8 - } -} - -dependencies { - compile fileTree(dir: 'libs', include: ['*.jar']) - testCompile 'junit:junit:4.12' - compile 'com.android.support:appcompat-v7:25.2.0' - compile 'org.java-websocket:Java-WebSocket:1.3.4' - compile 'com.android.support:recyclerview-v7:25.2.0' - compile 'io.reactivex:rxandroid:1.2.1' - compile 'com.squareup.retrofit2:converter-gson:2.3.0' - compile 'com.squareup.retrofit2:adapter-rxjava:2.3.0' - compile 'com.squareup.retrofit2:retrofit:2.3.0' - compile project(':lib') -} diff --git a/example-client/proguard-rules.pro b/example-client/proguard-rules.pro deleted file mode 100644 index c96428f..0000000 --- a/example-client/proguard-rules.pro +++ /dev/null @@ -1,17 +0,0 @@ -# Add project specific ProGuard rules here. -# By default, the flags in this file are appended to flags specified -# in /home/naik/Programs/android-sdk-linux/tools/proguard/proguard-android.txt -# You can edit the include path and order by changing the proguardFiles -# directive in build.gradle. -# -# For more details, see -# http://developer.android.com/guide/developing/tools/proguard.html - -# Add any project specific keep options here: - -# If your project uses WebView with JS, uncomment the following -# and specify the fully qualified class name to the JavaScript interface -# class: -#-keepclassmembers class fqcn.of.javascript.interface.for.webview { -# public *; -#} diff --git a/example-client/src/androidTest/java/ua/naiksoftware/stompclientexample/ApplicationTest.java b/example-client/src/androidTest/java/ua/naiksoftware/stompclientexample/ApplicationTest.java deleted file mode 100644 index 3a0f4c2..0000000 --- a/example-client/src/androidTest/java/ua/naiksoftware/stompclientexample/ApplicationTest.java +++ /dev/null @@ -1,13 +0,0 @@ -package ua.naiksoftware.stompclientexample; - -import android.app.Application; -import android.test.ApplicationTestCase; - -/** - * Testing Fundamentals - */ -public class ApplicationTest extends ApplicationTestCase { - public ApplicationTest() { - super(Application.class); - } -} \ No newline at end of file diff --git a/example-client/src/main/AndroidManifest.xml b/example-client/src/main/AndroidManifest.xml deleted file mode 100644 index 2b1f999..0000000 --- a/example-client/src/main/AndroidManifest.xml +++ /dev/null @@ -1,22 +0,0 @@ - - - - - - - - - - - - - - - - \ No newline at end of file diff --git a/example-client/src/main/java/ua/naiksoftware/stompclientexample/EchoModel.java b/example-client/src/main/java/ua/naiksoftware/stompclientexample/EchoModel.java deleted file mode 100644 index 1c694ac..0000000 --- a/example-client/src/main/java/ua/naiksoftware/stompclientexample/EchoModel.java +++ /dev/null @@ -1,20 +0,0 @@ -package ua.naiksoftware.stompclientexample; - -/** - * Created by Naik on 24.02.17. - */ -public class EchoModel { - - private String echo; - - public EchoModel() { - } - - public String getEcho() { - return echo; - } - - public void setEcho(String echo) { - this.echo = echo; - } -} diff --git a/example-client/src/main/java/ua/naiksoftware/stompclientexample/ExampleRepository.java b/example-client/src/main/java/ua/naiksoftware/stompclientexample/ExampleRepository.java deleted file mode 100644 index b81f19a..0000000 --- a/example-client/src/main/java/ua/naiksoftware/stompclientexample/ExampleRepository.java +++ /dev/null @@ -1,14 +0,0 @@ -package ua.naiksoftware.stompclientexample; - -import retrofit2.http.POST; -import retrofit2.http.Query; -import rx.Observable; - -/** - * Created by Naik on 24.02.17. - */ -public interface ExampleRepository { - - @POST("hello-convert-and-send") - Observable sendRestEcho(@Query("msg") String message); -} diff --git a/example-client/src/main/java/ua/naiksoftware/stompclientexample/MainActivity.java b/example-client/src/main/java/ua/naiksoftware/stompclientexample/MainActivity.java deleted file mode 100644 index 28a7ac9..0000000 --- a/example-client/src/main/java/ua/naiksoftware/stompclientexample/MainActivity.java +++ /dev/null @@ -1,128 +0,0 @@ -package ua.naiksoftware.stompclientexample; - -import android.os.Bundle; -import android.support.v7.app.AppCompatActivity; -import android.support.v7.widget.LinearLayoutManager; -import android.support.v7.widget.RecyclerView; -import android.util.Log; -import android.view.View; -import android.widget.Toast; - -import com.google.gson.Gson; -import com.google.gson.GsonBuilder; - -import java.text.SimpleDateFormat; -import java.util.ArrayList; -import java.util.Date; -import java.util.List; -import java.util.Locale; - -import rx.Subscription; -import rx.android.schedulers.AndroidSchedulers; -import rx.schedulers.Schedulers; -import ua.naiksoftware.stomp.Stomp; -import ua.naiksoftware.stomp.client.StompClient; - -import static ua.naiksoftware.stompclientexample.RestClient.ANDROID_EMULATOR_LOCALHOST; - -public class MainActivity extends AppCompatActivity { - - private static final String TAG = "MainActivity"; - - private SimpleAdapter mAdapter; - private List mDataSet = new ArrayList<>(); - private StompClient mStompClient; - private Subscription mRestPingSubscription; - private final SimpleDateFormat mTimeFormat = new SimpleDateFormat("HH:mm:ss", Locale.getDefault()); - private RecyclerView mRecyclerView; - private Gson mGson = new GsonBuilder().create(); - - @Override - protected void onCreate(Bundle savedInstanceState) { - super.onCreate(savedInstanceState); - setContentView(R.layout.activity_main); - mRecyclerView = (RecyclerView) findViewById(R.id.recycler_view); - mAdapter = new SimpleAdapter(mDataSet); - mAdapter.setHasStableIds(true); - mRecyclerView.setAdapter(mAdapter); - mRecyclerView.setLayoutManager(new LinearLayoutManager(this, LinearLayoutManager.VERTICAL, true)); - } - - public void disconnectStomp(View view) { - mStompClient.disconnect(); - } - - public void connectStomp(View view) { - mStompClient = Stomp.over(Stomp.ConnectionProvider.JWS, "ws://" + ANDROID_EMULATOR_LOCALHOST - + ":" + RestClient.SERVER_PORT + "/example-endpoint/websocket"); - - mStompClient.lifecycle() - .subscribeOn(Schedulers.io()) - .observeOn(AndroidSchedulers.mainThread()) - .subscribe(lifecycleEvent -> { - switch (lifecycleEvent.getType()) { - case OPENED: - toast("Stomp connection opened"); - break; - case ERROR: - Log.e(TAG, "Stomp connection error", lifecycleEvent.getException()); - toast("Stomp connection error"); - break; - case CLOSED: - toast("Stomp connection closed"); - } - }); - - // Receive greetings - mStompClient.topic("/topic/greetings") - .subscribeOn(Schedulers.io()) - .observeOn(AndroidSchedulers.mainThread()) - .subscribe(topicMessage -> { - Log.d(TAG, "Received " + topicMessage.getPayload()); - addItem(mGson.fromJson(topicMessage.getPayload(), EchoModel.class)); - }); - - mStompClient.connect(); - } - - public void sendEchoViaStomp(View v) { - mStompClient.send("/topic/hello-msg-mapping", "Echo STOMP " + mTimeFormat.format(new Date())) - .unsubscribeOn(Schedulers.newThread()) - .subscribeOn(Schedulers.io()) - .observeOn(AndroidSchedulers.mainThread()) - .subscribe(() -> Log.d(TAG, "STOMP echo send successfully"), throwable -> { - Log.e(TAG, "Error send STOMP echo", throwable); - toast(throwable.getMessage()); - }); - } - - public void sendEchoViaRest(View v) { - mRestPingSubscription = RestClient.getInstance().getExampleRepository() - .sendRestEcho("Echo REST " + mTimeFormat.format(new Date())) - .unsubscribeOn(Schedulers.newThread()) - .subscribeOn(Schedulers.io()) - .observeOn(AndroidSchedulers.mainThread()) - .subscribe(aVoid -> Log.d(TAG, "REST echo send successfully"), throwable -> { - Log.e(TAG, "Error send REST echo", throwable); - toast(throwable.getMessage()); - }); - } - - private void addItem(EchoModel echoModel) { - mDataSet.add(echoModel.getEcho() + " - " + mTimeFormat.format(new Date())); - mAdapter.notifyDataSetChanged(); - mRecyclerView.smoothScrollToPosition(mDataSet.size() - 1); - } - - private void toast(String text) { - Log.i(TAG, text); - Toast.makeText(this, text, Toast.LENGTH_SHORT).show(); - } - - @Override - protected void onDestroy() { - mStompClient.disconnect(); - if (mRestPingSubscription != null) mRestPingSubscription.unsubscribe(); - super.onDestroy(); - } -} diff --git a/example-client/src/main/java/ua/naiksoftware/stompclientexample/RestClient.java b/example-client/src/main/java/ua/naiksoftware/stompclientexample/RestClient.java deleted file mode 100644 index 6c71f7d..0000000 --- a/example-client/src/main/java/ua/naiksoftware/stompclientexample/RestClient.java +++ /dev/null @@ -1,44 +0,0 @@ -package ua.naiksoftware.stompclientexample; - -import retrofit2.Retrofit; -import retrofit2.adapter.rxjava.RxJavaCallAdapterFactory; -import retrofit2.converter.gson.GsonConverterFactory; - -/** - * Created by Naik on 24.02.17. - */ -public class RestClient { - - public static final String ANDROID_EMULATOR_LOCALHOST = "10.0.2.2"; - public static final String SERVER_PORT = "8080"; - - private static RestClient instance; - private static final Object lock = new Object(); - - public static RestClient getInstance() { - RestClient instance = RestClient.instance; - if (instance == null) { - synchronized (lock) { - instance = RestClient.instance; - if (instance == null) { - RestClient.instance = instance = new RestClient(); - } - } - } - return instance; - } - - private final ExampleRepository mExampleRepository; - - private RestClient() { - Retrofit retrofit = new Retrofit.Builder().baseUrl("http://" + ANDROID_EMULATOR_LOCALHOST + ":" + SERVER_PORT + "/") - .addConverterFactory(GsonConverterFactory.create()) - .addCallAdapterFactory(RxJavaCallAdapterFactory.create()) - .build(); - mExampleRepository = retrofit.create(ExampleRepository.class); - } - - public ExampleRepository getExampleRepository() { - return mExampleRepository; - } -} diff --git a/example-client/src/main/java/ua/naiksoftware/stompclientexample/SimpleAdapter.java b/example-client/src/main/java/ua/naiksoftware/stompclientexample/SimpleAdapter.java deleted file mode 100644 index f546409..0000000 --- a/example-client/src/main/java/ua/naiksoftware/stompclientexample/SimpleAdapter.java +++ /dev/null @@ -1,51 +0,0 @@ -package ua.naiksoftware.stompclientexample; - -import android.support.v7.widget.RecyclerView; -import android.view.LayoutInflater; -import android.view.View; -import android.view.ViewGroup; -import android.widget.TextView; - -import java.util.List; - -/** - * Created by Naik on 24.02.17. - */ -public class SimpleAdapter extends RecyclerView.Adapter { - - private final List mDataSet; - - public SimpleAdapter(List dataSet) { - mDataSet = dataSet; - } - - @Override - public SimpleViewHolder onCreateViewHolder(ViewGroup parent, int viewType) { - return new SimpleViewHolder(LayoutInflater.from(parent.getContext()).inflate(R.layout.item_layout, parent, false)); - } - - @Override - public void onBindViewHolder(SimpleViewHolder holder, int position) { - holder.mTextView.setText(mDataSet.get(position)); - } - - @Override - public int getItemCount() { - return mDataSet.size(); - } - - @Override - public long getItemId(int position) { - return mDataSet.get(position).hashCode(); - } - - static class SimpleViewHolder extends RecyclerView.ViewHolder { - - final TextView mTextView; - - public SimpleViewHolder(View itemView) { - super(itemView); - mTextView = (TextView) itemView.findViewById(R.id.text); - } - } -} diff --git a/example-client/src/main/res/layout/activity_main.xml b/example-client/src/main/res/layout/activity_main.xml deleted file mode 100644 index 0596fa9..0000000 --- a/example-client/src/main/res/layout/activity_main.xml +++ /dev/null @@ -1,75 +0,0 @@ - - - - - -