Skip to content

Commit a5c7586

Browse files
Working on StompClient. This version is not stable
1 parent 24f989f commit a5c7586

File tree

2 files changed

+158
-36
lines changed

2 files changed

+158
-36
lines changed

README.md

Lines changed: 73 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -134,18 +134,89 @@ client.lifecycle().subscribe(lifecycleEvent -> {
134134

135135
**Custom client**
136136

137-
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:
137+
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:
138138

139139
``` java
140-
client = Stomp.over(WebSocket.class, address, null, unsafeClient);
140+
client = Stomp.over(WebSocket.class, address, null, getUnsafeOkHttpClient());
141141
```
142142

143143
Yes, it's safe to pass `null` for either (or both) of the last two arguments. That's exactly what the shorter overloads do.
144144

145+
Note: This method is only supported using OkHttp, not JWS.
146+
145147
**Support**
146148

147149
Right now, the library only supports sending and receiving messages. ACK messages and transactions are not implemented yet.
148150

151+
## Changes in this fork
152+
153+
**Build changes**
154+
155+
The upstream master is based on Retrolambda. This version is based on Native Java 8 compilation,
156+
although it should also work with Jack. It will *not* work with Retrolambda.
157+
158+
**Code changes**
159+
160+
These are the possible changes you need to make to your code for this branch, if you were using the upstream before:
161+
162+
- Disconnecting is now mandatory
163+
- Previously, the socket would automatically disconnect if nothing was listening to it
164+
- Now, it will not disconnect unless you explicitly run client.disconnect()
165+
- send() now returns a Completable
166+
- Previously, it returned an Observable\<Void\>
167+
- Now, it and all its overloads return a Completable, which is functionally about the same
168+
- **However**, Completable does not inherit from Observable, so there are a couple differences:
169+
- Of course, if you were storing send() in an Observable, you'll have to change that
170+
- Additionally, if you were inheriting onNext before, you're going to have to adjust it:
171+
- Old way, deprecated:
172+
``` java
173+
client.send("/app/hello", "world").subscribe(
174+
aVoid -> Log.d(TAG, "Sent data!"),
175+
error -> Log.e(TAG, "Encountered error while sending data!", error)
176+
);
177+
```
178+
!! -v:
179+
``` java
180+
client.send("/app/hello", "world").subscribe(new Subscriber<Void>() {
181+
@Override
182+
public void onNext(Void aVoid) {
183+
Log.d(TAG, "Sent data!");
184+
}
185+
@Override
186+
public void onError(Throwable error) {
187+
Log.e(TAG, "Encountered error while sending data!", error);
188+
}
189+
@Override
190+
public void onCompleted() {} // useless
191+
});
192+
```
193+
- New way of handling it:
194+
``` java
195+
client.send("/app/hello", "world").subscribe(
196+
() -> Log.d(TAG, "Sent data!"),
197+
error -> Log.e(TAG, "Encountered error while sending data!", error)
198+
);
199+
```
200+
Or if you just can't get enough of anonymous classes:
201+
``` java
202+
client.send("/app/hello", "world").subscribe(new Subscriber<Object /*This can be anything*/ >() {
203+
@Override
204+
public void onCompleted() {
205+
Log.d(TAG, "Sent data!");
206+
}
207+
@Override
208+
public void onError(Throwable error) {
209+
Log.e(TAG, "Encountered error while sending data!", error);
210+
}
211+
@Override
212+
public void onNext(Object o) {} // useless
213+
});
214+
```
215+
- Be sure to implement this change, because the IDE might not catch the error.
216+
- Passing null as the topic path now throws an exception
217+
- Previously, it was supposed to silently fail, although it would probably hit a NPE first (untested)
218+
- Now it throws an IllegalArgumentException
219+
149220
## Additional Reading
150221
151222
- [Spring + Websockets + STOMP](https://spring.io/guides/gs/messaging-stomp-websocket/)

lib/src/main/java/ua/naiksoftware/stomp/client/StompClient.java

Lines changed: 85 additions & 34 deletions
Original file line numberDiff line numberDiff line change
@@ -13,10 +13,12 @@
1313
import java.util.UUID;
1414
import java.util.concurrent.CopyOnWriteArrayList;
1515

16+
import rx.Completable;
1617
import rx.Observable;
1718
import rx.Subscriber;
1819
import rx.Subscription;
1920
import rx.observables.ConnectableObservable;
21+
import rx.subjects.PublishSubject;
2022
import ua.naiksoftware.stomp.ConnectionProvider;
2123
import ua.naiksoftware.stomp.LifecycleEvent;
2224
import ua.naiksoftware.stomp.StompHeader;
@@ -31,17 +33,22 @@ public class StompClient {
3133
public static final String SUPPORTED_VERSIONS = "1.1,1.0";
3234
public static final String DEFAULT_ACK = "auto";
3335

36+
/*
3437
private Subscription mMessagesSubscription;
3538
private Map<String, Set<Subscriber<? super StompMessage>>> mSubscribers = new HashMap<>();
36-
private List<ConnectableObservable<Void>> mWaitConnectionObservables;
39+
*/
40+
private List<Completable> mWaitConnectionCompletables;
3741
private final ConnectionProvider mConnectionProvider;
3842
private HashMap<String, String> mTopics;
3943
private boolean mConnected;
4044
private boolean isConnecting;
4145

46+
private PublishSubject<StompMessage> mMessageStream;
47+
4248
public StompClient(ConnectionProvider connectionProvider) {
4349
mConnectionProvider = connectionProvider;
44-
mWaitConnectionObservables = new CopyOnWriteArrayList<>();
50+
mWaitConnectionCompletables = new CopyOnWriteArrayList<>();
51+
mMessageStream = PublishSubject.create();
4552
}
4653

4754
/**
@@ -96,46 +103,43 @@ public void connect(List<StompHeader> _headers, boolean reconnect) {
96103
});
97104

98105
isConnecting = true;
99-
mMessagesSubscription = mConnectionProvider.messages()
106+
mConnectionProvider.messages()
100107
.map(StompMessage::from)
108+
.doOnNext(this::callSubscribers)
109+
.filter(msg -> msg.getStompCommand().equals(StompCommand.CONNECTED))
101110
.subscribe(stompMessage -> {
102-
if (stompMessage.getStompCommand().equals(StompCommand.CONNECTED)) {
103-
mConnected = true;
104-
isConnecting = false;
105-
for (ConnectableObservable<Void> observable : mWaitConnectionObservables) {
106-
observable.connect();
107-
}
108-
mWaitConnectionObservables.clear();
111+
mConnected = true;
112+
isConnecting = false;
113+
for (Completable completable : mWaitConnectionCompletables) {
114+
completable.subscribe();
109115
}
110-
callSubscribers(stompMessage);
116+
mWaitConnectionCompletables.clear();
111117
});
112118
}
113119

114-
public Observable<Void> send(String destination) {
120+
public Completable send(String destination) {
115121
return send(new StompMessage(
116122
StompCommand.SEND,
117123
Collections.singletonList(new StompHeader(StompHeader.DESTINATION, destination)),
118124
null));
119125
}
120126

121-
public Observable<Void> send(String destination, String data) {
127+
public Completable send(String destination, String data) {
122128
return send(new StompMessage(
123129
StompCommand.SEND,
124130
Collections.singletonList(new StompHeader(StompHeader.DESTINATION, destination)),
125131
data));
126132
}
127133

128-
public Observable<Void> send(StompMessage stompMessage) {
129-
Observable<Void> observable = mConnectionProvider.send(stompMessage.compile()).toObservable();
134+
public Completable send(StompMessage stompMessage) {
135+
Completable completable = mConnectionProvider.send(stompMessage.compile());
130136
if (!mConnected) {
131-
ConnectableObservable<Void> deferred = observable.publish();
132-
mWaitConnectionObservables.add(deferred);
133-
return deferred;
134-
} else {
135-
return observable;
137+
mWaitConnectionCompletables.add(completable);
136138
}
139+
return completable;
137140
}
138141

142+
/*
139143
private void callSubscribers(StompMessage stompMessage) {
140144
String messageDestination = stompMessage.findHeader(StompHeader.DESTINATION);
141145
for (String dest : mSubscribers.keySet()) {
@@ -147,6 +151,11 @@ private void callSubscribers(StompMessage stompMessage) {
147151
}
148152
}
149153
}
154+
*/
155+
156+
private void callSubscribers(StompMessage stompMessage) {
157+
mMessageStream.onNext(stompMessage);
158+
}
150159

151160
public Observable<LifecycleEvent> lifecycle() {
152161
return mConnectionProvider.getLifecycleReceiver();
@@ -160,53 +169,95 @@ public Observable<StompMessage> topic(String destinationPath) {
160169
return topic(destinationPath, null);
161170
}
162171

172+
public Observable<StompMessage> topic(String destPath, List<StompHeader> headerList) {
173+
Observable<StompMessage> ret;
174+
175+
if (destPath == null)
176+
ret = Observable.error(new IllegalArgumentException("Topic path cannot be null"));
177+
else
178+
ret = mMessageStream
179+
.filter(msg -> destPath.equals(msg.findHeader(StompHeader.DESTINATION)))
180+
.doOnSubscribe(() -> subscribePath(destPath, headerList));
181+
// still need to figure out how to do the unsubscribes reactively... more difficult than it sounds
182+
return ret;
183+
}
184+
185+
/*
163186
public Observable<StompMessage> topic(String destinationPath, List<StompHeader> headerList) {
187+
// basically:
188+
// on SUBSCRIBE, add the observer to the Set in the mSubscribers map that's associated with the specified topic,
189+
// and send a subscribe message IF WE HAVEN'T ALREADY SUBSCRIBED TO THE TOPIC
190+
//
191+
// on UNSUBSCRIBE, remove unsubscribed observers, and remove unobserved topics
192+
193+
// on observer subscribe...
164194
return Observable.<StompMessage>create(subscriber -> {
195+
// get list of other subscribers to topic
165196
Set<Subscriber<? super StompMessage>> subscribersSet = mSubscribers.get(destinationPath);
197+
// if there are no other subscribers on topic...
166198
if (subscribersSet == null) {
199+
// create new subscriber list,
167200
subscribersSet = new HashSet<>();
201+
// and add the list to the map
168202
mSubscribers.put(destinationPath, subscribersSet);
203+
// send SUBSCRIBE message and add topic to mTopics
169204
subscribePath(destinationPath, headerList).subscribe();
170205
}
206+
// finally, now that we know that there is a list for this topic, add observer to it
171207
subscribersSet.add(subscriber);
172208
173209
}).doOnUnsubscribe(() -> {
210+
// on unsubscribe...
174211
Iterator<String> mapIterator = mSubscribers.keySet().iterator();
212+
// for each topic in the map,
175213
while (mapIterator.hasNext()) {
214+
// get topic path
176215
String destinationUrl = mapIterator.next();
216+
// get observers subscribed to this topic
177217
Set<Subscriber<? super StompMessage>> set = mSubscribers.get(destinationUrl);
178218
Iterator<Subscriber<? super StompMessage>> setIterator = set.iterator();
219+
// for each observer subscribed to this topic,
179220
while (setIterator.hasNext()) {
180221
Subscriber<? super StompMessage> subscriber = setIterator.next();
222+
// if observer is no longer subscribed,
181223
if (subscriber.isUnsubscribed()) {
224+
// remove it from the set
182225
setIterator.remove();
226+
// if there are no observers subscribed to this topic anymore...
183227
if (set.size() < 1) {
228+
// remote the set from the map
184229
mapIterator.remove();
230+
// send UNSUBSCRIBE message
185231
unsubscribePath(destinationUrl).subscribe();
186232
}
187233
}
188234
}
189235
}
190236
});
191237
}
238+
*/
239+
240+
private Completable subscribePath(String destinationPath, List<StompHeader> headerList) {
241+
String topicId = UUID.randomUUID().toString();
192242

193-
private Observable<Void> subscribePath(String destinationPath, List<StompHeader> headerList) {
194-
if (destinationPath == null) return Observable.empty();
195-
String topicId = UUID.randomUUID().toString();
243+
if (mTopics == null) mTopics = new HashMap<>();
196244

197-
if (mTopics == null) mTopics = new HashMap<>();
198-
mTopics.put(destinationPath, topicId);
199-
List<StompHeader> headers = new ArrayList<>();
200-
headers.add(new StompHeader(StompHeader.ID, topicId));
201-
headers.add(new StompHeader(StompHeader.DESTINATION, destinationPath));
202-
headers.add(new StompHeader(StompHeader.ACK, DEFAULT_ACK));
203-
if (headerList != null) headers.addAll(headerList);
204-
return send(new StompMessage(StompCommand.SUBSCRIBE,
205-
headers, null));
206-
}
245+
// Only continue if we don't already have a subscription to the topic
246+
if (mTopics.containsKey(destinationPath))
247+
return Completable.complete();
248+
249+
mTopics.put(destinationPath, topicId);
250+
List<StompHeader> headers = new ArrayList<>();
251+
headers.add(new StompHeader(StompHeader.ID, topicId));
252+
headers.add(new StompHeader(StompHeader.DESTINATION, destinationPath));
253+
headers.add(new StompHeader(StompHeader.ACK, DEFAULT_ACK));
254+
if (headerList != null) headers.addAll(headerList);
255+
return send(new StompMessage(StompCommand.SUBSCRIBE,
256+
headers, null));
257+
}
207258

208259

209-
private Observable<Void> unsubscribePath(String dest) {
260+
private Completable unsubscribePath(String dest) {
210261
String topicId = mTopics.get(dest);
211262
Log.d(TAG, "Unsubscribe path: " + dest + " id: " + topicId);
212263

0 commit comments

Comments
 (0)