Add MessagePack support for Java SignalR Client (#23532)

* Implement ParseMessages for java messagePack client

* Fix some spacing & syntax

* Implement write

* Tab -> Spaces

* MessagePacker -> MessageBufferPacker

* Tabs -> Spaces

* Tabs -> Spaces

* InvocationMessage may not include streamIDs

* Only 1 ctor per message type

* Fixup HubConnection.java

* Change return type of parseMessages to List

* Fix HubConnection

* Check for primitive value before returning

* Implement length header prefix

* Minor fixes

* Use ByteBuffer to read length header

* Add case for Char

* Close unpacker

* Typo

* Override onMessage w/ ByteString

* Change OKHttpWebSocketWrapper

* Account for nil InvocationId

* Change interface & MessagePack impl

* Update JsonHubProtocol

* Use ByteBuffer

* Fixup HubConnection

* Fixup more stuff

* Convert more stuff to ByteBuffer

* Account for ReadOnly

* Spacing

* No need to reset ByteBuffer when setting position

* Add Protocol to HubConnection ctor

* Set default, make stuff public

* Fixup tests

* More test cleanup

* Spacing

* only grab remaining buffer bytes in json

* Last test fixes

* Get rid of some unused imports

* First round of msgpack tests

* Flip condition

* Respond to feedback

* Spacing

* More tests

* Add test for primitives

* Add more tests, start using msgpack-jackson

* Fix build.gradle

* Remove debug prints

* Start using Type instead of Class

* Add overloads for Type, make messagePack readValue() more efficient

* Apply feedback, add some tests

* Add some tests, fix some tests

* Fix tests for real

* Add a whole buncha tests

* Add TestUtils change that I didn't commit yesterday

* Respond to some feedback

* Add a couple Json tests

* Apply more feedback

* Move readonly fix to msgpack

* Minor optimization

* Fixup some javadocs

* Respond to feedback

* Remove TypeReference, make Protocols private again

* Feedback
This commit is contained in:
William Godbe 2020-08-20 12:12:41 -07:00 committed by GitHub
parent 901ae06bb8
commit 8522ba8e55
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
40 changed files with 3813 additions and 474 deletions

View File

@ -39,6 +39,8 @@ dependencies {
implementation 'com.squareup.okhttp3:okhttp:3.11.0'
api 'io.reactivex.rxjava2:rxjava:2.2.3'
implementation 'org.slf4j:slf4j-api:1.7.25'
compile 'org.msgpack:msgpack-core:0.8.20'
compile 'org.msgpack:jackson-dataformat-msgpack:0.8.20'
}
spotless {

View File

@ -3,6 +3,7 @@
package com.microsoft.signalr;
import java.lang.reflect.Type;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.List;
@ -13,10 +14,10 @@ class CallbackMap {
private final Map<String, List<InvocationHandler>> handlers = new HashMap<>();
private final ReentrantLock lock = new ReentrantLock();
public InvocationHandler put(String target, ActionBase action, Class<?>... classes) {
public InvocationHandler put(String target, ActionBase action, Type... types) {
try {
lock.lock();
InvocationHandler handler = new InvocationHandler(action, classes);
InvocationHandler handler = new InvocationHandler(action, types);
if (!handlers.containsKey(target)) {
handlers.put(target, new ArrayList<>());
}

View File

@ -3,14 +3,28 @@
package com.microsoft.signalr;
import java.util.Map;
final class CancelInvocationMessage extends HubMessage {
private final int type = HubMessageType.CANCEL_INVOCATION.value;
private Map<String, String> headers;
private final String invocationId;
public CancelInvocationMessage(String invocationId) {
public CancelInvocationMessage(Map<String, String> headers, String invocationId) {
if (headers != null && !headers.isEmpty()) {
this.headers = headers;
}
this.invocationId = invocationId;
}
public Map<String, String> getHeaders() {
return headers;
}
public String getInvocationId() {
return invocationId;
}
@Override
public HubMessageType getMessageType() {
return HubMessageType.CANCEL_INVOCATION;

View File

@ -5,6 +5,7 @@ package com.microsoft.signalr;
final class CloseMessage extends HubMessage {
private final String error;
private final boolean allowReconnect;
@Override
public HubMessageType getMessageType() {
@ -12,14 +13,27 @@ final class CloseMessage extends HubMessage {
}
public CloseMessage() {
this(null);
this(null, false);
}
public CloseMessage(String error) {
this(error, false);
}
public CloseMessage(boolean allowReconnect) {
this(null, allowReconnect);
}
public CloseMessage(String error, boolean allowReconnect) {
this.error = error;
this.allowReconnect = allowReconnect;
}
public String getError() {
return this.error;
}
public boolean getAllowReconnect() {
return this.allowReconnect;
}
}

View File

@ -3,13 +3,19 @@
package com.microsoft.signalr;
import java.util.Map;
final class CompletionMessage extends HubMessage {
private final int type = HubMessageType.COMPLETION.value;
private Map<String, String> headers;
private final String invocationId;
private final Object result;
private final String error;
public CompletionMessage(String invocationId, Object result, String error) {
public CompletionMessage(Map<String, String> headers, String invocationId, Object result, String error) {
if (headers != null && !headers.isEmpty()) {
this.headers = headers;
}
if (error != null && result != null) {
throw new IllegalArgumentException("Expected either 'error' or 'result' to be provided, but not both.");
}
@ -17,6 +23,10 @@ final class CompletionMessage extends HubMessage {
this.result = result;
this.error = error;
}
public Map<String, String> getHeaders() {
return headers;
}
public Object getResult() {
return result;

View File

@ -4,6 +4,7 @@
package com.microsoft.signalr;
import java.io.IOException;
import java.nio.ByteBuffer;
import java.util.ArrayList;
import java.util.Collection;
import java.util.List;
@ -15,6 +16,7 @@ import java.util.concurrent.locks.ReentrantLock;
import io.reactivex.Single;
import io.reactivex.subjects.SingleSubject;
import okhttp3.*;
import okio.ByteString;
final class DefaultHttpClient extends HttpClient {
private OkHttpClient client = null;
@ -104,7 +106,7 @@ final class DefaultHttpClient extends HttpClient {
}
@Override
public Single<HttpResponse> send(HttpRequest httpRequest, String bodyContent) {
public Single<HttpResponse> send(HttpRequest httpRequest, ByteBuffer bodyContent) {
Request.Builder requestBuilder = new Request.Builder().url(httpRequest.getUrl());
switch (httpRequest.getMethod()) {
@ -114,7 +116,7 @@ final class DefaultHttpClient extends HttpClient {
case "POST":
RequestBody body;
if (bodyContent != null) {
body = RequestBody.create(MediaType.parse("text/plain"), bodyContent);
body = RequestBody.create(MediaType.parse("text/plain"), ByteString.of(bodyContent));
} else {
body = RequestBody.create(null, new byte[]{});
}
@ -150,7 +152,7 @@ final class DefaultHttpClient extends HttpClient {
@Override
public void onResponse(Call call, Response response) throws IOException {
try (ResponseBody body = response.body()) {
HttpResponse httpResponse = new HttpResponse(response.code(), response.message(), body.string());
HttpResponse httpResponse = new HttpResponse(response.code(), response.message(), ByteBuffer.wrap(body.bytes()));
responseSubject.onSuccess(httpResponse);
}
}

View File

@ -3,15 +3,18 @@
package com.microsoft.signalr;
import java.nio.charset.StandardCharsets;
import java.nio.ByteBuffer;
import com.google.gson.Gson;
final class HandshakeProtocol {
private static final Gson gson = new Gson();
private static final String RECORD_SEPARATOR = "\u001e";
public static String createHandshakeRequestMessage(HandshakeRequestMessage message) {
public static ByteBuffer createHandshakeRequestMessage(HandshakeRequestMessage message) {
// The handshake request is always in the JSON format
return gson.toJson(message) + RECORD_SEPARATOR;
return ByteBuffer.wrap((gson.toJson(message) + RECORD_SEPARATOR).getBytes(StandardCharsets.UTF_8));
}
public static HandshakeResponseMessage parseHandshakeResponse(String message) {

View File

@ -3,6 +3,7 @@
package com.microsoft.signalr;
import java.nio.ByteBuffer;
import java.util.HashMap;
import java.util.Map;
@ -45,23 +46,23 @@ class HttpRequest {
class HttpResponse {
private final int statusCode;
private final String statusText;
private final String content;
private final ByteBuffer content;
public HttpResponse(int statusCode) {
this(statusCode, "");
}
public HttpResponse(int statusCode, String statusText) {
this(statusCode, statusText, "");
this(statusCode, statusText, ByteBuffer.wrap(new byte[] {}));
}
public HttpResponse(int statusCode, String statusText, String content) {
public HttpResponse(int statusCode, String statusText, ByteBuffer content) {
this.statusCode = statusCode;
this.statusText = statusText;
this.content = content;
}
public String getContent() {
public ByteBuffer getContent() {
return content;
}
@ -95,7 +96,7 @@ abstract class HttpClient implements AutoCloseable {
return this.send(request);
}
public Single<HttpResponse> post(String url, String body, HttpRequest options) {
public Single<HttpResponse> post(String url, ByteBuffer body, HttpRequest options) {
options.setUrl(url);
options.setMethod("POST");
return this.send(options, body);
@ -122,7 +123,7 @@ abstract class HttpClient implements AutoCloseable {
public abstract Single<HttpResponse> send(HttpRequest request);
public abstract Single<HttpResponse> send(HttpRequest request, String body);
public abstract Single<HttpResponse> send(HttpRequest request, ByteBuffer body);
public abstract WebSocketWrapper createWebSocket(String url, Map<String, String> headers);

View File

@ -16,6 +16,7 @@ public class HttpHubConnectionBuilder {
private final String url;
private Transport transport;
private HttpClient httpClient;
private HubProtocol protocol = new JsonHubProtocol();
private boolean skipNegotiate;
private Single<String> accessTokenProvider;
private long handshakeResponseTimeout = 0;
@ -54,6 +55,16 @@ public class HttpHubConnectionBuilder {
this.httpClient = httpClient;
return this;
}
/**
* Sets MessagePack as the {@link HubProtocol} to be used by the {@link HubConnection}.
*
* @return This instance of the HttpHubConnectionBuilder.
*/
public HttpHubConnectionBuilder withMessagePackHubProtocol() {
this.protocol = new MessagePackHubProtocol();
return this;
}
/**
* Indicates to the {@link HubConnection} that it should skip the negotiate process.
@ -133,7 +144,7 @@ public class HttpHubConnectionBuilder {
* @return A new instance of {@link HubConnection}.
*/
public HubConnection build() {
return new HubConnection(url, transport, skipNegotiate, httpClient, accessTokenProvider,
return new HubConnection(url, transport, skipNegotiate, httpClient, protocol, accessTokenProvider,
handshakeResponseTimeout, headers, transportEnum, configureBuilder);
}
}

View File

@ -4,6 +4,10 @@
package com.microsoft.signalr;
import java.io.StringReader;
import java.lang.reflect.Type;
import java.lang.reflect.TypeVariable;
import java.nio.ByteBuffer;
import java.nio.charset.StandardCharsets;
import java.util.*;
import java.util.concurrent.*;
import java.util.concurrent.atomic.AtomicInteger;
@ -26,8 +30,8 @@ import okhttp3.OkHttpClient;
* A connection used to invoke hub methods on a SignalR Server.
*/
public class HubConnection implements AutoCloseable {
private static final String RECORD_SEPARATOR = "\u001e";
private static final List<Class<?>> emptyArray = new ArrayList<>();
private static final byte RECORD_SEPARATOR = 0x1e;
private static final List<Type> emptyArray = new ArrayList<>();
private static final int MAX_NEGOTIATE_ATTEMPTS = 100;
private String baseUrl;
@ -126,7 +130,7 @@ public class HubConnection implements AutoCloseable {
return transport;
}
HubConnection(String url, Transport transport, boolean skipNegotiate, HttpClient httpClient,
HubConnection(String url, Transport transport, boolean skipNegotiate, HttpClient httpClient, HubProtocol protocol,
Single<String> accessTokenProvider, long handshakeResponseTimeout, Map<String, String> headers, TransportEnum transportEnum,
Action1<OkHttpClient.Builder> configureBuilder) {
if (url == null || url.isEmpty()) {
@ -134,7 +138,7 @@ public class HubConnection implements AutoCloseable {
}
this.baseUrl = url;
this.protocol = new JsonHubProtocol();
this.protocol = protocol;
if (accessTokenProvider != null) {
this.accessTokenProvider = accessTokenProvider;
@ -165,8 +169,20 @@ public class HubConnection implements AutoCloseable {
this.callback = (payload) -> {
resetServerTimeout();
if (!handshakeReceived) {
int handshakeLength = payload.indexOf(RECORD_SEPARATOR) + 1;
String handshakeResponseString = payload.substring(0, handshakeLength - 1);
List<Byte> handshakeByteList = new ArrayList<Byte>();
byte curr = payload.get();
// Add the handshake to handshakeBytes, but not the record separator
while (curr != RECORD_SEPARATOR) {
handshakeByteList.add(curr);
curr = payload.get();
}
int handshakeLength = handshakeByteList.size() + 1;
byte[] handshakeBytes = new byte[handshakeLength - 1];
for (int i = 0; i < handshakeLength - 1; i++) {
handshakeBytes[i] = handshakeByteList.get(i);
}
// The handshake will always be a UTF8 Json string
String handshakeResponseString = new String(handshakeBytes, StandardCharsets.UTF_8);
HandshakeResponseMessage handshakeResponse;
try {
handshakeResponse = HandshakeProtocol.parseHandshakeResponse(handshakeResponseString);
@ -185,14 +201,13 @@ public class HubConnection implements AutoCloseable {
handshakeReceived = true;
handshakeResponseSubject.onComplete();
payload = payload.substring(handshakeLength);
// The payload only contained the handshake response so we can return.
if (payload.length() == 0) {
if (!payload.hasRemaining()) {
return;
}
}
HubMessage[] messages = protocol.parseMessages(payload, connectionState);
List<HubMessage> messages = protocol.parseMessages(payload, connectionState);
for (HubMessage message : messages) {
logger.debug("Received message of type {}.", message.getMessageType());
@ -271,7 +286,7 @@ public class HubConnection implements AutoCloseable {
throw new RuntimeException(String.format("Unexpected status code returned from negotiate: %d %s.",
response.getStatusCode(), response.getStatusText()));
}
JsonReader reader = new JsonReader(new StringReader(response.getContent()));
JsonReader reader = new JsonReader(new StringReader(new String(response.getContent().array(), StandardCharsets.UTF_8)));
NegotiateResponse negotiateResponse = new NegotiateResponse(reader);
if (negotiateResponse.getError() != null) {
@ -372,7 +387,7 @@ public class HubConnection implements AutoCloseable {
transport.setOnClose((message) -> stopConnection(message));
return transport.start(negotiateResponse.getFinalUrl()).andThen(Completable.defer(() -> {
String handshake = HandshakeProtocol.createHandshakeRequestMessage(
ByteBuffer handshake = HandshakeProtocol.createHandshakeRequestMessage(
new HandshakeRequestMessage(protocol.getName(), protocol.getVersion()));
connectionState = new ConnectionState(this);
@ -585,9 +600,9 @@ public class HubConnection implements AutoCloseable {
args = checkUploadStream(args, streamIds);
InvocationMessage invocationMessage;
if (isStreamInvocation) {
invocationMessage = new StreamInvocationMessage(id, method, args, streamIds);
invocationMessage = new StreamInvocationMessage(null, id, method, args, streamIds);
} else {
invocationMessage = new InvocationMessage(id, method, args, streamIds);
invocationMessage = new InvocationMessage(null, id, method, args, streamIds);
}
sendHubMessage(invocationMessage);
@ -602,13 +617,13 @@ public class HubConnection implements AutoCloseable {
for (String streamId: streamIds) {
Observable observable = this.streamMap.get(streamId);
observable.subscribe(
(item) -> sendHubMessage(new StreamItem(streamId, item)),
(item) -> sendHubMessage(new StreamItem(null, streamId, item)),
(error) -> {
sendHubMessage(new CompletionMessage(streamId, null, error.toString()));
sendHubMessage(new CompletionMessage(null, streamId, null, error.toString()));
this.streamMap.remove(streamId);
},
() -> {
sendHubMessage(new CompletionMessage(streamId, null, null));
sendHubMessage(new CompletionMessage(null, streamId, null, null));
this.streamMap.remove(streamId);
});
}
@ -678,8 +693,26 @@ public class HubConnection implements AutoCloseable {
* @param <T> The expected return type.
* @return A Single that yields the return value when the invocation has completed.
*/
@SuppressWarnings("unchecked")
public <T> Single<T> invoke(Class<T> returnType, String method, Object... args) {
return this.<T>invoke(returnType, returnType, method, args);
}
/**
* Invokes a hub method on the server using the specified method name and arguments.
*
* @param returnType The expected return type.
* @param method The name of the server method to invoke.
* @param args The arguments used to invoke the server method.
* @param <T> The expected return type.
* @return A Single that yields the return value when the invocation has completed.
*/
public <T> Single<T> invoke(Type returnType, String method, Object... args) {
Class<?> returnClass = Utils.typeToClass(returnType);
return this.<T>invoke(returnType, returnClass, method, args);
}
@SuppressWarnings("unchecked")
private <T> Single<T> invoke(Type returnType, Class<?> returnClass, String method, Object... args) {
hubConnectionStateLock.lock();
try {
if (hubConnectionState != HubConnectionState.CONNECTED) {
@ -697,10 +730,10 @@ public class HubConnection implements AutoCloseable {
Subject<Object> pendingCall = irq.getPendingCall();
pendingCall.subscribe(result -> {
// Primitive types can't be cast with the Class cast function
if (returnType.isPrimitive()) {
if (returnClass.isPrimitive()) {
subject.onSuccess((T)result);
} else {
subject.onSuccess(returnType.cast(result));
subject.onSuccess((T)returnClass.cast(result));
}
}, error -> subject.onError(error));
@ -722,8 +755,26 @@ public class HubConnection implements AutoCloseable {
* @param <T> The expected return type.
* @return An observable that yields the streaming results from the server.
*/
@SuppressWarnings("unchecked")
public <T> Observable<T> stream(Class<T> returnType, String method, Object ... args) {
return this.<T>stream(returnType, returnType, method, args);
}
/**
* Invokes a streaming hub method on the server using the specified name and arguments.
*
* @param returnType The expected return type of the stream items.
* @param method The name of the server method to invoke.
* @param args The arguments used to invoke the server method.
* @param <T> The expected return type.
* @return An observable that yields the streaming results from the server.
*/
public <T> Observable<T> stream(Type returnType, String method, Object ... args) {
Class<?> returnClass = Utils.typeToClass(returnType);
return this.<T>stream(returnType, returnClass, method, args);
}
@SuppressWarnings("unchecked")
private <T> Observable<T> stream(Type returnType, Class<?> returnClass, String method, Object ... args) {
String invocationId;
InvocationRequest irq;
hubConnectionStateLock.lock();
@ -741,10 +792,10 @@ public class HubConnection implements AutoCloseable {
Subject<Object> pendingCall = irq.getPendingCall();
pendingCall.subscribe(result -> {
// Primitive types can't be cast with the Class cast function
if (returnType.isPrimitive()) {
if (returnClass.isPrimitive()) {
subject.onNext((T)result);
} else {
subject.onNext(returnType.cast(result));
subject.onNext((T)returnClass.cast(result));
}
}, error -> subject.onError(error),
() -> subject.onComplete());
@ -753,7 +804,7 @@ public class HubConnection implements AutoCloseable {
sendInvocationMessage(method, args, invocationId, true);
return observable.doOnDispose(() -> {
if (subscriptionCount.decrementAndGet() == 0) {
CancelInvocationMessage cancelInvocationMessage = new CancelInvocationMessage(invocationId);
CancelInvocationMessage cancelInvocationMessage = new CancelInvocationMessage(null, invocationId);
sendHubMessage(cancelInvocationMessage);
if (connectionState != null) {
connectionState.tryRemoveInvocation(invocationId);
@ -767,7 +818,7 @@ public class HubConnection implements AutoCloseable {
}
private void sendHubMessage(HubMessage message) {
String serializedMessage = protocol.writeMessage(message);
ByteBuffer serializedMessage = protocol.writeMessage(message);
if (message.getMessageType() == HubMessageType.INVOCATION ) {
logger.debug("Sending {} message '{}'.", message.getMessageType().name(), ((InvocationMessage)message).getInvocationId());
} else if (message.getMessageType() == HubMessageType.STREAM_INVOCATION) {
@ -825,6 +876,7 @@ public class HubConnection implements AutoCloseable {
/**
* Registers a handler that will be invoked when the hub method with the specified method name is invoked.
* Should be used for primitives and non-generic classes.
*
* @param target The name of the hub method to define.
* @param callback The handler that will be raised when the hub method is invoked.
@ -840,6 +892,7 @@ public class HubConnection implements AutoCloseable {
/**
* Registers a handler that will be invoked when the hub method with the specified method name is invoked.
* Should be used for primitives and non-generic classes.
*
* @param target The name of the hub method to define.
* @param callback The handler that will be raised when the hub method is invoked.
@ -858,6 +911,7 @@ public class HubConnection implements AutoCloseable {
/**
* Registers a handler that will be invoked when the hub method with the specified method name is invoked.
* Should be used for primitives and non-generic classes.
*
* @param target The name of the hub method to define.
* @param callback The handler that will be raised when the hub method is invoked.
@ -879,6 +933,7 @@ public class HubConnection implements AutoCloseable {
/**
* Registers a handler that will be invoked when the hub method with the specified method name is invoked.
* Should be used for primitives and non-generic classes.
*
* @param target The name of the hub method to define.
* @param callback The handler that will be raised when the hub method is invoked.
@ -902,6 +957,7 @@ public class HubConnection implements AutoCloseable {
/**
* Registers a handler that will be invoked when the hub method with the specified method name is invoked.
* Should be used for primitives and non-generic classes.
*
* @param target The name of the hub method to define.
* @param callback The handler that will be raised when the hub method is invoked.
@ -928,6 +984,7 @@ public class HubConnection implements AutoCloseable {
/**
* Registers a handler that will be invoked when the hub method with the specified method name is invoked.
* Should be used for primitives and non-generic classes.
*
* @param target The name of the hub method to define.
* @param callback The handler that will be raised when the hub method is invoked.
@ -956,6 +1013,7 @@ public class HubConnection implements AutoCloseable {
/**
* Registers a handler that will be invoked when the hub method with the specified method name is invoked.
* Should be used for primitives and non-generic classes.
*
* @param target The name of the hub method to define.
* @param callback The handler that will be raised when the hub method is invoked.
@ -986,6 +1044,7 @@ public class HubConnection implements AutoCloseable {
/**
* Registers a handler that will be invoked when the hub method with the specified method name is invoked.
* Should be used for primitives and non-generic classes.
*
* @param target The name of the hub method to define.
* @param callback The handler that will be raised when the hub method is invoked.
@ -1016,7 +1075,224 @@ public class HubConnection implements AutoCloseable {
return registerHandler(target, action, param1, param2, param3, param4, param5, param6, param7, param8);
}
private Subscription registerHandler(String target, ActionBase action, Class<?>... types) {
/**
* Registers a handler that will be invoked when the hub method with the specified method name is invoked.
* Should be used for generic classes and Parameterized Collections, like List or Map.
*
* @param target The name of the hub method to define.
* @param callback The handler that will be raised when the hub method is invoked.
* @param param1 The first parameter.
* @param <T1> The first argument type.
* @return A {@link Subscription} that can be disposed to unsubscribe from the hub method.
*/
@SuppressWarnings("unchecked")
public <T1> Subscription on(String target, Action1<T1> callback, Type param1) {
ActionBase action = params -> callback.invoke((T1)Utils.typeToClass(param1).cast(params[0]));
return registerHandler(target, action, param1);
}
/**
* Registers a handler that will be invoked when the hub method with the specified method name is invoked.
* Should be used for generic classes and Parameterized Collections, like List or Map.
*
* @param target The name of the hub method to define.
* @param callback The handler that will be raised when the hub method is invoked.
* @param param1 The first parameter.
* @param param2 The second parameter.
* @param <T1> The first parameter type.
* @param <T2> The second parameter type.
* @return A {@link Subscription} that can be disposed to unsubscribe from the hub method.
*/
@SuppressWarnings("unchecked")
public <T1, T2> Subscription on(String target, Action2<T1, T2> callback, Type param1, Type param2) {
ActionBase action = params -> {
callback.invoke((T1)Utils.typeToClass(param1).cast(params[0]), (T2)Utils.typeToClass(param2).cast(params[1]));
};
return registerHandler(target, action, param1, param2);
}
/**
* Registers a handler that will be invoked when the hub method with the specified method name is invoked.
* Should be used for generic classes and Parameterized Collections, like List or Map.
*
* @param target The name of the hub method to define.
* @param callback The handler that will be raised when the hub method is invoked.
* @param param1 The first parameter.
* @param param2 The second parameter.
* @param param3 The third parameter.
* @param <T1> The first parameter type.
* @param <T2> The second parameter type.
* @param <T3> The third parameter type.
* @return A {@link Subscription} that can be disposed to unsubscribe from the hub method.
*/
@SuppressWarnings("unchecked")
public <T1, T2, T3> Subscription on(String target, Action3<T1, T2, T3> callback,
Type param1, Type param2, Type param3) {
ActionBase action = params -> {
callback.invoke((T1)Utils.typeToClass(param1).cast(params[0]), (T2)Utils.typeToClass(param2).cast(params[1]),
(T3)Utils.typeToClass(param3).cast(params[2]));
};
return registerHandler(target, action, param1, param2, param3);
}
/**
* Registers a handler that will be invoked when the hub method with the specified method name is invoked.
* Should be used for generic classes and Parameterized Collections, like List or Map.
*
* @param target The name of the hub method to define.
* @param callback The handler that will be raised when the hub method is invoked.
* @param param1 The first parameter.
* @param param2 The second parameter.
* @param param3 The third parameter.
* @param param4 The fourth parameter.
* @param <T1> The first parameter type.
* @param <T2> The second parameter type.
* @param <T3> The third parameter type.
* @param <T4> The fourth parameter type.
* @return A {@link Subscription} that can be disposed to unsubscribe from the hub method.
*/
@SuppressWarnings("unchecked")
public <T1, T2, T3, T4> Subscription on(String target, Action4<T1, T2, T3, T4> callback,
Type param1, Type param2, Type param3, Type param4) {
ActionBase action = params -> {
callback.invoke((T1)Utils.typeToClass(param1).cast(params[0]), (T2)Utils.typeToClass(param2).cast(params[1]),
(T3)Utils.typeToClass(param3).cast(params[2]), (T4)Utils.typeToClass(param4).cast(params[3]));
};
return registerHandler(target, action, param1, param2, param3, param4);
}
/**
* Registers a handler that will be invoked when the hub method with the specified method name is invoked.
* Should be used for generic classes and Parameterized Collections, like List or Map.
*
* @param target The name of the hub method to define.
* @param callback The handler that will be raised when the hub method is invoked.
* @param param1 The first parameter.
* @param param2 The second parameter.
* @param param3 The third parameter.
* @param param4 The fourth parameter.
* @param param5 The fifth parameter.
* @param <T1> The first parameter type.
* @param <T2> The second parameter type.
* @param <T3> The third parameter type.
* @param <T4> The fourth parameter type.
* @param <T5> The fifth parameter type.
* @return A {@link Subscription} that can be disposed to unsubscribe from the hub method.
*/
@SuppressWarnings("unchecked")
public <T1, T2, T3, T4, T5> Subscription on(String target, Action5<T1, T2, T3, T4, T5> callback,
Type param1, Type param2, Type param3, Type param4, Type param5) {
ActionBase action = params -> {
callback.invoke((T1)Utils.typeToClass(param1).cast(params[0]), (T2)Utils.typeToClass(param2).cast(params[1]),
(T3)Utils.typeToClass(param3).cast(params[2]), (T4)Utils.typeToClass(param4).cast(params[3]),
(T5)Utils.typeToClass(param5).cast(params[4]));
};
return registerHandler(target, action, param1, param2, param3, param4, param5);
}
/**
* Registers a handler that will be invoked when the hub method with the specified method name is invoked.
* Should be used for generic classes and Parameterized Collections, like List or Map.
*
* @param target The name of the hub method to define.
* @param callback The handler that will be raised when the hub method is invoked.
* @param param1 The first parameter.
* @param param2 The second parameter.
* @param param3 The third parameter.
* @param param4 The fourth parameter.
* @param param5 The fifth parameter.
* @param param6 The sixth parameter.
* @param <T1> The first parameter type.
* @param <T2> The second parameter type.
* @param <T3> The third parameter type.
* @param <T4> The fourth parameter type.
* @param <T5> The fifth parameter type.
* @param <T6> The sixth parameter type.
* @return A {@link Subscription} that can be disposed to unsubscribe from the hub method.
*/
@SuppressWarnings("unchecked")
public <T1, T2, T3, T4, T5, T6> Subscription on(String target, Action6<T1, T2, T3, T4, T5, T6> callback,
Type param1, Type param2, Type param3, Type param4, Type param5, Type param6) {
ActionBase action = params -> {
callback.invoke((T1)Utils.typeToClass(param1).cast(params[0]), (T2)Utils.typeToClass(param2).cast(params[1]),
(T3)Utils.typeToClass(param3).cast(params[2]), (T4)Utils.typeToClass(param4).cast(params[3]),
(T5)Utils.typeToClass(param5).cast(params[4]), (T6)Utils.typeToClass(param6).cast(params[5]));
};
return registerHandler(target, action, param1, param2, param3, param4, param5, param6);
}
/**
* Registers a handler that will be invoked when the hub method with the specified method name is invoked.
* Should be used for generic classes and Parameterized Collections, like List or Map.
*
* @param target The name of the hub method to define.
* @param callback The handler that will be raised when the hub method is invoked.
* @param param1 The first parameter.
* @param param2 The second parameter.
* @param param3 The third parameter.
* @param param4 The fourth parameter.
* @param param5 The fifth parameter.
* @param param6 The sixth parameter.
* @param param7 The seventh parameter.
* @param <T1> The first parameter type.
* @param <T2> The second parameter type.
* @param <T3> The third parameter type.
* @param <T4> The fourth parameter type.
* @param <T5> The fifth parameter type.
* @param <T6> The sixth parameter type.
* @param <T7> The seventh parameter type.
* @return A {@link Subscription} that can be disposed to unsubscribe from the hub method.
*/
@SuppressWarnings("unchecked")
public <T1, T2, T3, T4, T5, T6, T7> Subscription on(String target, Action7<T1, T2, T3, T4, T5, T6, T7> callback,
Type param1, Type param2, Type param3, Type param4, Type param5, Type param6, Type param7) {
ActionBase action = params -> {
callback.invoke((T1)Utils.typeToClass(param1).cast(params[0]), (T2)Utils.typeToClass(param2).cast(params[1]),
(T3)Utils.typeToClass(param3).cast(params[2]), (T4)Utils.typeToClass(param4).cast(params[3]),
(T5)Utils.typeToClass(param5).cast(params[4]), (T6)Utils.typeToClass(param6).cast(params[5]),
(T7)Utils.typeToClass(param7).cast(params[6]));
};
return registerHandler(target, action, param1, param2, param3, param4, param5, param6, param7);
}
/**
* Registers a handler that will be invoked when the hub method with the specified method name is invoked.
* Should be used for generic classes and Parameterized Collections, like List or Map.
*
* @param target The name of the hub method to define.
* @param callback The handler that will be raised when the hub method is invoked.
* @param param1 The first parameter.
* @param param2 The second parameter.
* @param param3 The third parameter.
* @param param4 The fourth parameter.
* @param param5 The fifth parameter.
* @param param6 The sixth parameter.
* @param param7 The seventh parameter.
* @param param8 The eighth parameter
* @param <T1> The first parameter type.
* @param <T2> The second parameter type.
* @param <T3> The third parameter type.
* @param <T4> The fourth parameter type.
* @param <T5> The fifth parameter type.
* @param <T6> The sixth parameter type.
* @param <T7> The seventh parameter type.
* @param <T8> The eighth parameter type.
* @return A {@link Subscription} that can be disposed to unsubscribe from the hub method.
*/
@SuppressWarnings("unchecked")
public <T1, T2, T3, T4, T5, T6, T7, T8> Subscription on(String target, Action8<T1, T2, T3, T4, T5, T6, T7, T8> callback,
Type param1, Type param2, Type param3, Type param4, Type param5, Type param6, Type param7,
Type param8) {
ActionBase action = params -> {
callback.invoke((T1)Utils.typeToClass(param1).cast(params[0]), (T2)Utils.typeToClass(param2).cast(params[1]),
(T3)Utils.typeToClass(param3).cast(params[2]), (T4)Utils.typeToClass(param4).cast(params[3]),
(T5)Utils.typeToClass(param5).cast(params[4]), (T6)Utils.typeToClass(param6).cast(params[5]),
(T7)Utils.typeToClass(param7).cast(params[6]), (T8)Utils.typeToClass(param8).cast(params[7]));
};
return registerHandler(target, action, param1, param2, param3, param4, param5, param6, param7, param8);
}
private Subscription registerHandler(String target, ActionBase action, Type... types) {
InvocationHandler handler = handlers.put(target, action, types);
logger.debug("Registering handler for client method: '{}'.", target);
return new Subscription(handlers, handler, target);
@ -1087,7 +1363,7 @@ public class HubConnection implements AutoCloseable {
}
@Override
public Class<?> getReturnType(String invocationId) {
public Type getReturnType(String invocationId) {
InvocationRequest irq = getInvocation(invocationId);
if (irq == null) {
return null;
@ -1097,7 +1373,7 @@ public class HubConnection implements AutoCloseable {
}
@Override
public List<Class<?>> getParameterTypes(String methodName) {
public List<Type> getParameterTypes(String methodName) {
List<InvocationHandler> handlers = connection.handlers.get(methodName);
if (handlers == null) {
logger.warn("Failed to find handler for '{}' method.", methodName);
@ -1108,7 +1384,7 @@ public class HubConnection implements AutoCloseable {
throw new RuntimeException(String.format("There are no callbacks registered for the method '%s'.", methodName));
}
return handlers.get(0).getClasses();
return handlers.get(0).getTypes();
}
}

View File

@ -11,7 +11,8 @@ enum HubMessageType {
CANCEL_INVOCATION(5),
PING(6),
CLOSE(7),
INVOCATION_BINDING_FAILURE(-1);
INVOCATION_BINDING_FAILURE(-1),
STREAM_BINDING_FAILURE(-2);
public int value;
HubMessageType(int id) { this.value = id; }

View File

@ -3,6 +3,9 @@
package com.microsoft.signalr;
import java.nio.ByteBuffer;
import java.util.List;
/**
* A protocol abstraction for communicating with SignalR hubs.
*/
@ -13,15 +16,15 @@ interface HubProtocol {
/**
* Creates a new list of {@link HubMessage}s.
* @param message A string representation of one or more {@link HubMessage}s.
* @param message A ByteBuffer representation of one or more {@link HubMessage}s.
* @return A list of {@link HubMessage}s.
*/
HubMessage[] parseMessages(String message, InvocationBinder binder);
List<HubMessage> parseMessages(ByteBuffer message, InvocationBinder binder);
/**
* Writes the specified {@link HubMessage} to a String.
* @param message The message to write.
* @return A string representation of the message.
* @return A ByteBuffer representation of the message.
*/
String writeMessage(HubMessage message);
ByteBuffer writeMessage(HubMessage message);
}

View File

@ -3,9 +3,13 @@
package com.microsoft.signalr;
import java.lang.reflect.Type;
import java.util.List;
/**
* An abstraction for passing around information about method signatures.
*/
interface InvocationBinder {
Class<?> getReturnType(String invocationId);
List<Class<?>> getParameterTypes(String methodName);
Type getReturnType(String invocationId);
List<Type> getParameterTypes(String methodName);
}

View File

@ -3,20 +3,21 @@
package com.microsoft.signalr;
import java.lang.reflect.Type;
import java.util.Arrays;
import java.util.List;
class InvocationHandler {
private final List<Class<?>> classes;
private final List<Type> types;
private final ActionBase action;
InvocationHandler(ActionBase action, Class<?>... classes) {
InvocationHandler(ActionBase action, Type... types) {
this.action = action;
this.classes = Arrays.asList(classes);
this.types = Arrays.asList(types);
}
public List<Class<?>> getClasses() {
return classes;
public List<Type> getTypes() {
return types;
}
public ActionBase getAction() {

View File

@ -4,19 +4,20 @@
package com.microsoft.signalr;
import java.util.Collection;
import java.util.Map;
class InvocationMessage extends HubMessage {
int type = HubMessageType.INVOCATION.value;
private Map<String, String> headers;
private final String invocationId;
private final String target;
private final Object[] arguments;
private Collection<String> streamIds;
public InvocationMessage(String invocationId, String target, Object[] args) {
this(invocationId, target, args, null);
}
public InvocationMessage(String invocationId, String target, Object[] args, Collection<String> streamIds) {
public InvocationMessage(Map<String, String> headers, String invocationId, String target, Object[] args, Collection<String> streamIds) {
if (headers != null && !headers.isEmpty()) {
this.headers = headers;
}
this.invocationId = invocationId;
this.target = target;
this.arguments = args;
@ -24,6 +25,10 @@ class InvocationMessage extends HubMessage {
this.streamIds = streamIds;
}
}
public Map<String, String> getHeaders() {
return headers;
}
public String getInvocationId() {
return invocationId;
@ -36,6 +41,10 @@ class InvocationMessage extends HubMessage {
public Object[] getArguments() {
return arguments;
}
public Collection<String> getStreamIds() {
return streamIds;
}
@Override
public HubMessageType getMessageType() {

View File

@ -3,17 +3,18 @@
package com.microsoft.signalr;
import java.lang.reflect.Type;
import java.util.concurrent.CancellationException;
import io.reactivex.subjects.ReplaySubject;
import io.reactivex.subjects.Subject;
class InvocationRequest {
private final Class<?> returnType;
private final Type returnType;
private final Subject<Object> pendingCall = ReplaySubject.create();
private final String invocationId;
InvocationRequest(Class<?> returnType, String invocationId) {
InvocationRequest(Type returnType, String invocationId) {
this.returnType = returnType;
this.invocationId = invocationId;
}
@ -47,7 +48,7 @@ class InvocationRequest {
return pendingCall;
}
public Class<?> getReturnType() {
public Type getReturnType() {
return returnType;
}

View File

@ -5,7 +5,11 @@ package com.microsoft.signalr;
import java.io.IOException;
import java.io.StringReader;
import java.lang.reflect.Type;
import java.nio.ByteBuffer;
import java.nio.charset.StandardCharsets;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.List;
import com.google.gson.Gson;
@ -36,15 +40,26 @@ class JsonHubProtocol implements HubProtocol {
}
@Override
public HubMessage[] parseMessages(String payload, InvocationBinder binder) {
if (payload.length() == 0) {
return new HubMessage[]{};
public List<HubMessage> parseMessages(ByteBuffer payload, InvocationBinder binder) {
String payloadStr;
// If the payload is readOnly, we have to copy the bytes from its array to make the payload string
if (payload.isReadOnly()) {
byte[] payloadBytes = new byte[payload.remaining()];
payload.get(payloadBytes, 0, payloadBytes.length);
payloadStr = new String(payloadBytes, StandardCharsets.UTF_8);
// Otherwise we can allocate directly from its array
} else {
// The position of the ByteBuffer may have been incremented - make sure we only grab the remaining bytes
payloadStr = new String(payload.array(), payload.position(), payload.remaining(), StandardCharsets.UTF_8);
}
if (!(payload.substring(payload.length() - 1).equals(RECORD_SEPARATOR))) {
if (payloadStr.length() == 0) {
return null;
}
if (!(payloadStr.substring(payloadStr.length() - 1).equals(RECORD_SEPARATOR))) {
throw new RuntimeException("Message is incomplete.");
}
String[] messages = payload.split(RECORD_SEPARATOR);
String[] messages = payloadStr.split(RECORD_SEPARATOR);
List<HubMessage> hubMessages = new ArrayList<>();
try {
for (String str : messages) {
@ -87,7 +102,7 @@ class JsonHubProtocol implements HubProtocol {
if (target != null) {
boolean startedArray = false;
try {
List<Class<?>> types = binder.getParameterTypes(target);
List<Type> types = binder.getParameterTypes(target);
startedArray = true;
arguments = bindArguments(reader, types);
} catch (Exception ex) {
@ -125,7 +140,7 @@ class JsonHubProtocol implements HubProtocol {
case INVOCATION:
if (argumentsToken != null) {
try {
List<Class<?>> types = binder.getParameterTypes(target);
List<Type> types = binder.getParameterTypes(target);
arguments = bindArguments(argumentsToken, types);
} catch (Exception ex) {
argumentBindingException = ex;
@ -135,25 +150,25 @@ class JsonHubProtocol implements HubProtocol {
hubMessages.add(new InvocationBindingFailureMessage(invocationId, target, argumentBindingException));
} else {
if (arguments == null) {
hubMessages.add(new InvocationMessage(invocationId, target, new Object[0]));
hubMessages.add(new InvocationMessage(null, invocationId, target, new Object[0], null));
} else {
hubMessages.add(new InvocationMessage(invocationId, target, arguments.toArray()));
hubMessages.add(new InvocationMessage(null, invocationId, target, arguments.toArray(), null));
}
}
break;
case COMPLETION:
if (resultToken != null) {
Class<?> returnType = binder.getReturnType(invocationId);
Type returnType = binder.getReturnType(invocationId);
result = gson.fromJson(resultToken, returnType != null ? returnType : Object.class);
}
hubMessages.add(new CompletionMessage(invocationId, result, error));
hubMessages.add(new CompletionMessage(null, invocationId, result, error));
break;
case STREAM_ITEM:
if (resultToken != null) {
Class<?> returnType = binder.getReturnType(invocationId);
Type returnType = binder.getReturnType(invocationId);
result = gson.fromJson(resultToken, returnType != null ? returnType : Object.class);
}
hubMessages.add(new StreamItem(invocationId, result));
hubMessages.add(new StreamItem(null, invocationId, result));
break;
case STREAM_INVOCATION:
case CANCEL_INVOCATION:
@ -176,15 +191,15 @@ class JsonHubProtocol implements HubProtocol {
throw new RuntimeException("Error reading JSON.", ex);
}
return hubMessages.toArray(new HubMessage[hubMessages.size()]);
return hubMessages;
}
@Override
public String writeMessage(HubMessage hubMessage) {
return gson.toJson(hubMessage) + RECORD_SEPARATOR;
public ByteBuffer writeMessage(HubMessage hubMessage) {
return ByteBuffer.wrap((gson.toJson(hubMessage) + RECORD_SEPARATOR).getBytes(StandardCharsets.UTF_8));
}
private ArrayList<Object> bindArguments(JsonArray argumentsToken, List<Class<?>> paramTypes) {
private ArrayList<Object> bindArguments(JsonArray argumentsToken, List<Type> paramTypes) {
if (argumentsToken.size() != paramTypes.size()) {
throw new RuntimeException(String.format("Invocation provides %d argument(s) but target expects %d.", argumentsToken.size(), paramTypes.size()));
}
@ -200,7 +215,7 @@ class JsonHubProtocol implements HubProtocol {
return arguments;
}
private ArrayList<Object> bindArguments(JsonReader reader, List<Class<?>> paramTypes) throws IOException {
private ArrayList<Object> bindArguments(JsonReader reader, List<Type> paramTypes) throws IOException {
reader.beginArray();
int paramCount = paramTypes.size();
int argCount = 0;

View File

@ -3,6 +3,8 @@
package com.microsoft.signalr;
import java.nio.ByteBuffer;
import java.nio.charset.StandardCharsets;
import java.util.Map;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
@ -100,7 +102,7 @@ class LongPollingTransport implements Transport {
} else {
if (response.getContent() != null) {
logger.debug("Message received.");
onReceiveThread.submit(() ->this.onReceive(response.getContent()));
onReceiveThread.submit(() -> this.onReceive(response.getContent()));
} else {
logger.debug("Poll timed out, reissuing.");
}
@ -121,7 +123,7 @@ class LongPollingTransport implements Transport {
}
@Override
public Completable send(String message) {
public Completable send(ByteBuffer message) {
if (!this.active) {
return Completable.error(new Exception("Cannot send unless the transport is active."));
}
@ -138,7 +140,7 @@ class LongPollingTransport implements Transport {
}
@Override
public void onReceive(String message) {
public void onReceive(ByteBuffer message) {
this.onReceiveCallBack.invoke(message);
logger.debug("OnReceived callback has been invoked.");
}

View File

@ -0,0 +1,637 @@
// Copyright (c) .NET Foundation. All rights reserved.
// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information.
package com.microsoft.signalr;
import java.io.IOException;
import java.lang.reflect.Type;
import java.nio.ByteBuffer;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collection;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import org.msgpack.core.MessageBufferPacker;
import org.msgpack.core.MessageFormat;
import org.msgpack.core.MessagePack;
import org.msgpack.core.MessagePackException;
import org.msgpack.core.MessagePacker;
import org.msgpack.core.MessageUnpacker;
import org.msgpack.jackson.dataformat.MessagePackFactory;
import org.msgpack.value.ValueType;
import com.fasterxml.jackson.databind.ObjectMapper;
import com.fasterxml.jackson.databind.type.TypeFactory;
class MessagePackHubProtocol implements HubProtocol {
private static final int ERROR_RESULT = 1;
private static final int VOID_RESULT = 2;
private static final int NON_VOID_RESULT = 3;
private ObjectMapper objectMapper = new ObjectMapper(new MessagePackFactory());
private TypeFactory typeFactory = objectMapper.getTypeFactory();
@Override
public String getName() {
return "messagepack";
}
@Override
public int getVersion() {
return 1;
}
@Override
public TransferFormat getTransferFormat() {
return TransferFormat.BINARY;
}
@Override
public List<HubMessage> parseMessages(ByteBuffer payload, InvocationBinder binder) {
if (payload.remaining() == 0) {
return null;
}
// MessagePack library can't handle read-only ByteBuffer - copy into an array-backed ByteBuffer if this is the case
if (payload.isReadOnly()) {
byte[] payloadBytes = new byte[payload.remaining()];
payload.get(payloadBytes, 0, payloadBytes.length);
payload = ByteBuffer.wrap(payloadBytes);
}
List<HubMessage> hubMessages = new ArrayList<>();
while (payload.hasRemaining()) {
int length;
try {
length = Utils.readLengthHeader(payload);
// Throw if remaining buffer is shorter than length header
if (payload.remaining() < length) {
throw new RuntimeException(String.format("MessagePack message was length %d but claimed to be length %d.", payload.remaining(), length));
}
} catch (IOException ex) {
throw new RuntimeException("Error reading length header.", ex);
}
// Instantiate MessageUnpacker
try(MessageUnpacker unpacker = MessagePack.newDefaultUnpacker(payload)) {
int itemCount = unpacker.unpackArrayHeader();
HubMessageType messageType = HubMessageType.values()[unpacker.unpackInt() - 1];
switch (messageType) {
case INVOCATION:
hubMessages.add(createInvocationMessage(unpacker, binder, itemCount, payload));
break;
case STREAM_ITEM:
hubMessages.add(createStreamItemMessage(unpacker, binder, payload));
break;
case COMPLETION:
hubMessages.add(createCompletionMessage(unpacker, binder, payload));
break;
case STREAM_INVOCATION:
hubMessages.add(createStreamInvocationMessage(unpacker, binder, itemCount, payload));
break;
case CANCEL_INVOCATION:
hubMessages.add(createCancelInvocationMessage(unpacker));
break;
case PING:
hubMessages.add(PingMessage.getInstance());
break;
case CLOSE:
hubMessages.add(createCloseMessage(unpacker, itemCount));
break;
default:
break;
}
// Make sure that we actually read the right number of bytes
int readBytes = (int) unpacker.getTotalReadBytes();
if (readBytes != length) {
// Check what the last message was
// If it was an invocation binding failure, we have to correct the position of the buffer
if (hubMessages.get(hubMessages.size() - 1).getMessageType() == HubMessageType.INVOCATION_BINDING_FAILURE) {
payload.position(payload.position() + (length - readBytes));
} else {
throw new RuntimeException(String.format("MessagePack message was length %d but claimed to be length %d.", readBytes, length));
}
}
unpacker.close();
payload.position(payload.position() + readBytes);
} catch (MessagePackException | IOException ex) {
throw new RuntimeException("Error reading MessagePack data.", ex);
}
}
return hubMessages;
}
@Override
public ByteBuffer writeMessage(HubMessage hubMessage) {
HubMessageType messageType = hubMessage.getMessageType();
try {
byte[] message;
switch (messageType) {
case INVOCATION:
message = writeInvocationMessage((InvocationMessage) hubMessage);
break;
case STREAM_ITEM:
message = writeStreamItemMessage((StreamItem) hubMessage);
break;
case COMPLETION:
message = writeCompletionMessage((CompletionMessage) hubMessage);
break;
case STREAM_INVOCATION:
message = writeStreamInvocationMessage((StreamInvocationMessage) hubMessage);
break;
case CANCEL_INVOCATION:
message = writeCancelInvocationMessage((CancelInvocationMessage) hubMessage);
break;
case PING:
message = writePingMessage((PingMessage) hubMessage);
break;
case CLOSE:
message = writeCloseMessage((CloseMessage) hubMessage);
break;
default:
throw new RuntimeException(String.format("Unexpected message type: %d", messageType.value));
}
int length = message.length;
List<Byte> header = Utils.getLengthHeader(length);
byte[] messageWithHeader = new byte[header.size() + length];
int headerSize = header.size();
// Write the length header, then all of the bytes of the original message
for (int i = 0; i < headerSize; i++) {
messageWithHeader[i] = header.get(i);
}
for (int i = 0; i < length; i++) {
messageWithHeader[i + headerSize] = message[i];
}
return ByteBuffer.wrap(messageWithHeader);
} catch (MessagePackException | IOException ex) {
throw new RuntimeException("Error writing MessagePack data.", ex);
}
}
private HubMessage createInvocationMessage(MessageUnpacker unpacker, InvocationBinder binder, int itemCount, ByteBuffer payload) throws IOException {
Map<String, String> headers = readHeaders(unpacker);
// invocationId may be nil
String invocationId = null;
if (!unpacker.tryUnpackNil()) {
invocationId = unpacker.unpackString();
}
// For MsgPack, we represent an empty invocation ID as an empty string,
// so we need to normalize that to "null", which is what indicates a non-blocking invocation.
if (invocationId == null || invocationId.isEmpty()) {
invocationId = null;
}
String target = unpacker.unpackString();
Object[] arguments = null;
try {
List<Type> types = binder.getParameterTypes(target);
arguments = bindArguments(unpacker, types, payload);
} catch (Exception ex) {
return new InvocationBindingFailureMessage(invocationId, target, ex);
}
Collection<String> streams = null;
// Older implementations may not send the streamID array
if (itemCount > 5) {
streams = readStreamIds(unpacker);
}
return new InvocationMessage(headers, invocationId, target, arguments, streams);
}
private HubMessage createStreamItemMessage(MessageUnpacker unpacker, InvocationBinder binder, ByteBuffer payload) throws IOException {
Map<String, String> headers = readHeaders(unpacker);
String invocationId = unpacker.unpackString();
Object value;
try {
Type itemType = binder.getReturnType(invocationId);
value = readValue(unpacker, itemType, payload, true);
} catch (Exception ex) {
return new StreamBindingFailureMessage(invocationId, ex);
}
return new StreamItem(headers, invocationId, value);
}
private HubMessage createCompletionMessage(MessageUnpacker unpacker, InvocationBinder binder, ByteBuffer payload) throws IOException {
Map<String, String> headers = readHeaders(unpacker);
String invocationId = unpacker.unpackString();
int resultKind = unpacker.unpackInt();
String error = null;
Object result = null;
switch (resultKind) {
case ERROR_RESULT:
error = unpacker.unpackString();
break;
case VOID_RESULT:
break;
case NON_VOID_RESULT:
Type itemType = binder.getReturnType(invocationId);
result = readValue(unpacker, itemType, payload, true);
break;
default:
throw new RuntimeException("Invalid invocation result kind.");
}
return new CompletionMessage(headers, invocationId, result, error);
}
private HubMessage createStreamInvocationMessage(MessageUnpacker unpacker, InvocationBinder binder, int itemCount, ByteBuffer payload) throws IOException {
Map<String, String> headers = readHeaders(unpacker);
String invocationId = unpacker.unpackString();
String target = unpacker.unpackString();
Object[] arguments = null;
try {
List<Type> types = binder.getParameterTypes(target);
arguments = bindArguments(unpacker, types, payload);
} catch (Exception ex) {
return new InvocationBindingFailureMessage(invocationId, target, ex);
}
Collection<String> streams = readStreamIds(unpacker);
return new StreamInvocationMessage(headers, invocationId, target, arguments, streams);
}
private HubMessage createCancelInvocationMessage(MessageUnpacker unpacker) throws IOException {
Map<String, String> headers = readHeaders(unpacker);
String invocationId = unpacker.unpackString();
return new CancelInvocationMessage(headers, invocationId);
}
private HubMessage createCloseMessage(MessageUnpacker unpacker, int itemCount) throws IOException {
// error may be nil
String error = null;
if (!unpacker.tryUnpackNil()) {
error = unpacker.unpackString();
}
boolean allowReconnect = false;
if (itemCount > 2) {
allowReconnect = unpacker.unpackBoolean();
}
return new CloseMessage(error, allowReconnect);
}
private byte[] writeInvocationMessage(InvocationMessage message) throws IOException {
MessageBufferPacker packer = MessagePack.newDefaultBufferPacker();
packer.packArrayHeader(6);
packer.packInt(message.getMessageType().value);
writeHeaders(message.getHeaders(), packer);
String invocationId = message.getInvocationId();
if (invocationId != null && !invocationId.isEmpty()) {
packer.packString(invocationId);
} else {
packer.packNil();
}
packer.packString(message.getTarget());
Object[] arguments = message.getArguments();
packer.packArrayHeader(arguments.length);
for (Object o: arguments) {
writeValue(o, packer);
}
writeStreamIds(message.getStreamIds(), packer);
packer.flush();
byte[] content = packer.toByteArray();
packer.close();
return content;
}
private byte[] writeStreamItemMessage(StreamItem message) throws IOException {
MessageBufferPacker packer = MessagePack.newDefaultBufferPacker();
packer.packArrayHeader(4);
packer.packInt(message.getMessageType().value);
writeHeaders(message.getHeaders(), packer);
packer.packString(message.getInvocationId());
writeValue(message.getItem(), packer);
packer.flush();
byte[] content = packer.toByteArray();
packer.close();
return content;
}
private byte[] writeCompletionMessage(CompletionMessage message) throws IOException {
MessageBufferPacker packer = MessagePack.newDefaultBufferPacker();
int resultKind =
message.getError() != null ? ERROR_RESULT :
message.getResult() != null ? NON_VOID_RESULT :
VOID_RESULT;
packer.packArrayHeader(4 + (resultKind != VOID_RESULT ? 1: 0));
packer.packInt(message.getMessageType().value);
writeHeaders(message.getHeaders(), packer);
packer.packString(message.getInvocationId());
packer.packInt(resultKind);
switch (resultKind) {
case ERROR_RESULT:
packer.packString(message.getError());
break;
case NON_VOID_RESULT:
writeValue(message.getResult(), packer);
break;
}
packer.flush();
byte[] content = packer.toByteArray();
packer.close();
return content;
}
private byte[] writeStreamInvocationMessage(StreamInvocationMessage message) throws IOException {
MessageBufferPacker packer = MessagePack.newDefaultBufferPacker();
packer.packArrayHeader(6);
packer.packInt(message.getMessageType().value);
writeHeaders(message.getHeaders(), packer);
packer.packString(message.getInvocationId());
packer.packString(message.getTarget());
Object[] arguments = message.getArguments();
packer.packArrayHeader(arguments.length);
for (Object o: arguments) {
writeValue(o, packer);
}
writeStreamIds(message.getStreamIds(), packer);
packer.flush();
byte[] content = packer.toByteArray();
packer.close();
return content;
}
private byte[] writeCancelInvocationMessage(CancelInvocationMessage message) throws IOException {
MessageBufferPacker packer = MessagePack.newDefaultBufferPacker();
packer.packArrayHeader(3);
packer.packInt(message.getMessageType().value);
writeHeaders(message.getHeaders(), packer);
packer.packString(message.getInvocationId());
packer.flush();
byte[] content = packer.toByteArray();
packer.close();
return content;
}
private byte[] writePingMessage(PingMessage message) throws IOException {
MessageBufferPacker packer = MessagePack.newDefaultBufferPacker();
packer.packArrayHeader(1);
packer.packInt(message.getMessageType().value);
packer.flush();
byte[] content = packer.toByteArray();
packer.close();
return content;
}
private byte[] writeCloseMessage(CloseMessage message) throws IOException {
MessageBufferPacker packer = MessagePack.newDefaultBufferPacker();
packer.packArrayHeader(3);
packer.packInt(message.getMessageType().value);
String error = message.getError();
if (error != null && !error.isEmpty()) {
packer.packString(error);
} else {
packer.packNil();
}
packer.packBoolean(message.getAllowReconnect());
packer.flush();
byte[] content = packer.toByteArray();
packer.close();
return content;
}
private Map<String, String> readHeaders(MessageUnpacker unpacker) throws IOException {
int headerCount = unpacker.unpackMapHeader();
if (headerCount > 0) {
Map<String, String> headers = new HashMap<String, String>();
for (int i = 0; i < headerCount; i++) {
headers.put(unpacker.unpackString(), unpacker.unpackString());
}
return headers;
} else {
return null;
}
}
private void writeHeaders(Map<String, String> headers, MessagePacker packer) throws IOException {
if (headers != null) {
packer.packMapHeader(headers.size());
for (String k: headers.keySet()) {
packer.packString(k);
packer.packString(headers.get(k));
}
} else {
packer.packMapHeader(0);
}
}
private Collection<String> readStreamIds(MessageUnpacker unpacker) throws IOException {
int streamCount = unpacker.unpackArrayHeader();
Collection<String> streams = null;
if (streamCount > 0) {
streams = new ArrayList<String>();
for (int i = 0; i < streamCount; i++) {
streams.add(unpacker.unpackString());
}
}
return streams;
}
private void writeStreamIds(Collection<String> streamIds, MessagePacker packer) throws IOException {
if (streamIds != null) {
packer.packArrayHeader(streamIds.size());
for (String s: streamIds) {
packer.packString(s);
}
} else {
packer.packArrayHeader(0);
}
}
private Object[] bindArguments(MessageUnpacker unpacker, List<Type> paramTypes, ByteBuffer payload) throws IOException {
int argumentCount = unpacker.unpackArrayHeader();
if (paramTypes.size() != argumentCount) {
throw new RuntimeException(String.format("Invocation provides %d argument(s) but target expects %d.", argumentCount, paramTypes.size()));
}
Object[] arguments = new Object[argumentCount];
for (int i = 0; i < argumentCount; i++) {
arguments[i] = readValue(unpacker, paramTypes.get(i), payload, true);
}
return arguments;
}
private Object readValue(MessageUnpacker unpacker, Type itemType, ByteBuffer payload, boolean outermostCall) throws IOException {
Class<?> itemClass = Utils.typeToClass(itemType);
MessageFormat messageFormat = unpacker.getNextFormat();
ValueType valueType = messageFormat.getValueType();
int length;
long readBytesStart;
Object item = null;
switch(valueType) {
case NIL:
unpacker.unpackNil();
return null;
case BOOLEAN:
item = unpacker.unpackBoolean();
break;
case INTEGER:
switch (messageFormat) {
case UINT64:
item = unpacker.unpackBigInteger();
break;
case INT64:
case UINT32:
item = unpacker.unpackLong();
break;
default:
item = unpacker.unpackInt();
// unpackInt could correspond to an int, short, char, or byte - cast those literally here
if (itemClass != null) {
if (itemClass.equals(Short.class) || itemClass.equals(short.class)) {
item = ((Integer) item).shortValue();
} else if (itemClass.equals(Character.class) || itemClass.equals(char.class)) {
item = (char) ((Integer) item).shortValue();
} else if (itemClass.equals(Byte.class) || itemClass.equals(byte.class)) {
item = ((Integer) item).byteValue();
}
}
break;
}
break;
case FLOAT:
item = unpacker.unpackDouble();
break;
case STRING:
item = unpacker.unpackString();
// ObjectMapper packs chars as Strings - correct back to char while unpacking if necessary
if (itemClass != null && (itemClass.equals(char.class) || itemClass.equals(Character.class))) {
item = ((String) item).charAt(0);
}
break;
case BINARY:
length = unpacker.unpackBinaryHeader();
byte[] binaryValue = new byte[length];
unpacker.readPayload(binaryValue);
item = binaryValue;
break;
case ARRAY:
readBytesStart = unpacker.getTotalReadBytes();
length = unpacker.unpackArrayHeader();
for (int i = 0; i < length; i++) {
readValue(unpacker, Object.class, payload, false);
}
if (outermostCall) {
// Check how many bytes we've read, grab that from the payload, and deserialize with objectMapper
byte[] payloadBytes = payload.array();
// If itemType was null, we were just in this method to advance the buffer. return null.
if (itemType == null) {
return null;
}
return objectMapper.readValue(payloadBytes, payload.position() + (int) readBytesStart, (int) (unpacker.getTotalReadBytes() - readBytesStart),
typeFactory.constructType(itemType));
} else {
// This is an inner call to readValue - we just need to read the right number of bytes
// We can return null, and the outermost call will know how many bytes to give to objectMapper.
return null;
}
case MAP:
readBytesStart = unpacker.getTotalReadBytes();
length = unpacker.unpackMapHeader();
for (int i = 0; i < length; i++) {
readValue(unpacker, Object.class, payload, false);
readValue(unpacker, Object.class, payload, false);
}
if (outermostCall) {
// Check how many bytes we've read, grab that from the payload, and deserialize with objectMapper
byte[] payloadBytes = payload.array();
byte[] mapBytes = Arrays.copyOfRange(payloadBytes, payload.position() + (int) readBytesStart,
payload.position() + (int) unpacker.getTotalReadBytes());
// If itemType was null, we were just in this method to advance the buffer. return null.
if (itemType == null) {
return null;
}
return objectMapper.readValue(payloadBytes, payload.position() + (int) readBytesStart, (int) (unpacker.getTotalReadBytes() - readBytesStart),
typeFactory.constructType(itemType));
} else {
// This is an inner call to readValue - we just need to read the right number of bytes
// We can return null, and the outermost call will know how many bytes to give to objectMapper.
return null;
}
case EXTENSION:
/*
ExtensionTypeHeader extension = unpacker.unpackExtensionTypeHeader();
byte[] extensionValue = new byte[extension.getLength()];
unpacker.readPayload(extensionValue);
//Convert this to an object?
item = extensionValue;
*/
throw new RuntimeException("Extension types are not supported");
default:
return null;
}
// If itemType was null, we were just in this method to advance the buffer. return null.
if (itemType == null) {
return null;
}
// If we get here, the item isn't a map or a collection/array, so we use the Class to cast it
if (itemClass.isPrimitive()) {
return Utils.toPrimitive(itemClass, item);
}
return itemClass.cast(item);
}
private void writeValue(Object o, MessagePacker packer) throws IOException {
packer.addPayload(objectMapper.writeValueAsBytes(o));
}
}

View File

@ -3,6 +3,8 @@
package com.microsoft.signalr;
import java.nio.ByteBuffer;
import java.nio.charset.StandardCharsets;
import java.util.Map;
import java.util.concurrent.locks.ReentrantLock;
@ -17,6 +19,7 @@ import okhttp3.Request;
import okhttp3.Response;
import okhttp3.WebSocket;
import okhttp3.WebSocketListener;
import okio.ByteString;
class OkHttpWebSocketWrapper extends WebSocketWrapper {
private WebSocket websocketClient;
@ -60,8 +63,9 @@ class OkHttpWebSocketWrapper extends WebSocketWrapper {
}
@Override
public Completable send(String message) {
websocketClient.send(message);
public Completable send(ByteBuffer message) {
ByteString bs = ByteString.of(message);
websocketClient.send(bs);
return Completable.complete();
}
@ -83,7 +87,12 @@ class OkHttpWebSocketWrapper extends WebSocketWrapper {
@Override
public void onMessage(WebSocket webSocket, String message) {
onReceive.invoke(message);
onReceive.invoke(ByteBuffer.wrap(message.getBytes(StandardCharsets.UTF_8)));
}
@Override
public void onMessage(WebSocket webSocket, ByteString bytes) {
onReceive.invoke(bytes.asByteBuffer());
}
@Override

View File

@ -3,6 +3,8 @@
package com.microsoft.signalr;
import java.nio.ByteBuffer;
interface OnReceiveCallBack {
void invoke(String message);
void invoke(ByteBuffer message);
}

View File

@ -0,0 +1,27 @@
// Copyright (c) .NET Foundation. All rights reserved.
// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information.
package com.microsoft.signalr;
class StreamBindingFailureMessage extends HubMessage {
private final String invocationId;
private final Exception exception;
public StreamBindingFailureMessage(String invocationId, Exception exception) {
this.invocationId = invocationId;
this.exception = exception;
}
public String getInvocationId() {
return invocationId;
}
public Exception getException() {
return exception;
}
@Override
public HubMessageType getMessageType() {
return HubMessageType.STREAM_BINDING_FAILURE;
}
}

View File

@ -4,16 +4,12 @@
package com.microsoft.signalr;
import java.util.Collection;
import java.util.Map;
final class StreamInvocationMessage extends InvocationMessage {
public StreamInvocationMessage(String invocationId, String target, Object[] args) {
super(invocationId, target, args);
super.type = HubMessageType.STREAM_INVOCATION.value;
}
public StreamInvocationMessage(String invocationId, String target, Object[] args, Collection<String> streamIds) {
super(invocationId, target, args, streamIds);
public StreamInvocationMessage(Map<String, String> headers, String invocationId, String target, Object[] args, Collection<String> streamIds) {
super(headers, invocationId, target, args, streamIds);
super.type = HubMessageType.STREAM_INVOCATION.value;
}

View File

@ -3,15 +3,25 @@
package com.microsoft.signalr;
import java.util.Map;
final class StreamItem extends HubMessage {
private final int type = HubMessageType.STREAM_ITEM.value;
private Map<String, String> headers;
private final String invocationId;
private final Object item;
public StreamItem(String invocationId, Object item) {
public StreamItem(Map<String, String> headers, String invocationId, Object item) {
if (headers != null && !headers.isEmpty()) {
this.headers = headers;
}
this.invocationId = invocationId;
this.item = item;
}
public Map<String, String> getHeaders() {
return headers;
}
public String getInvocationId() {
return invocationId;

View File

@ -3,13 +3,15 @@
package com.microsoft.signalr;
import java.nio.ByteBuffer;
import io.reactivex.Completable;
interface Transport {
Completable start(String url);
Completable send(String message);
Completable send(ByteBuffer message);
void setOnReceive(OnReceiveCallBack callback);
void onReceive(String message);
void onReceive(ByteBuffer message);
void setOnClose(TransportOnClosedCallback onCloseCallback);
Completable stop();
}

View File

@ -3,6 +3,16 @@
package com.microsoft.signalr;
import java.io.IOException;
import java.lang.reflect.Array;
import java.lang.reflect.GenericArrayType;
import java.lang.reflect.ParameterizedType;
import java.lang.reflect.Type;
import java.lang.reflect.TypeVariable;
import java.lang.reflect.WildcardType;
import java.nio.ByteBuffer;
import java.util.ArrayList;
class Utils {
public static String appendQueryString(String original, String queryStringValue) {
if (original.contains("?")) {
@ -11,4 +21,92 @@ class Utils {
return original + "?" + queryStringValue;
}
}
public static int readLengthHeader(ByteBuffer buffer) throws IOException {
// The payload starts with a length prefix encoded as a VarInt. VarInts use the most significant bit
// as a marker whether the byte is the last byte of the VarInt or if it spans to the next byte. Bytes
// appear in the reverse order - i.e. the first byte contains the least significant bits of the value
// Examples:
// VarInt: 0x35 - %00110101 - the most significant bit is 0 so the value is %x0110101 i.e. 0x35 (53)
// VarInt: 0x80 0x25 - %10000000 %00101001 - the most significant bit of the first byte is 1 so the
// remaining bits (%x0000000) are the lowest bits of the value. The most significant bit of the second
// byte is 0 meaning this is last byte of the VarInt. The actual value bits (%x0101001) need to be
// prepended to the bits we already read so the values is %01010010000000 i.e. 0x1480 (5248)
// We support payloads up to 2GB so the biggest number we support is 7fffffff which when encoded as
// VarInt is 0xFF 0xFF 0xFF 0xFF 0x07 - hence the maximum length prefix is 5 bytes.
int length = 0;
int numBytes = 0;
int maxLength = 5;
byte curr;
do {
// If we run out of bytes before we finish reading the length header, the message is malformed
if (buffer.hasRemaining()) {
curr = buffer.get();
} else {
throw new RuntimeException("The length header was incomplete");
}
length = length | (curr & (byte) 0x7f) << (numBytes * 7);
numBytes++;
} while (numBytes < maxLength && (curr & (byte) 0x80) != 0);
// Max header length is 5, and the maximum value of the 5th byte is 0x07
if ((curr & (byte) 0x80) != 0 || (numBytes == maxLength && curr > (byte) 0x07)) {
throw new RuntimeException("Messages over 2GB in size are not supported");
}
return length;
}
public static ArrayList<Byte> getLengthHeader(int length) {
// This code writes length prefix of the message as a VarInt. Read the comment in
// the readLengthHeader for details.
ArrayList<Byte> header = new ArrayList<Byte>();
do {
byte curr = (byte) (length & 0x7f);
length >>= 7;
if (length > 0) {
curr |= 0x80;
}
header.add(curr);
} while (length > 0);
return header;
}
public static Object toPrimitive(Class<?> c, Object value) {
if (boolean.class == c) return ((Boolean) value).booleanValue();
if (byte.class == c) return ((Byte) value).byteValue();
if (short.class == c) return ((Short) value).shortValue();
if (int.class == c) return ((Integer) value).intValue();
if (long.class == c) return ((Long) value).longValue();
if (float.class == c) return ((Float) value).floatValue();
if (double.class == c) return ((Double) value).doubleValue();
if (char.class == c) return ((Character) value).charValue();
return value;
}
public static Class<?> typeToClass(Type type) {
if (type == null) {
return null;
}
if (type instanceof Class) {
return (Class<?>) type;
} else if (type instanceof GenericArrayType) {
// Instantiate an array of the same type as this type, then return its class
return Array.newInstance(typeToClass(((GenericArrayType)type).getGenericComponentType()), 0).getClass();
} else if (type instanceof ParameterizedType) {
return typeToClass(((ParameterizedType) type).getRawType());
} else if (type instanceof TypeVariable) {
Type[] bounds = ((TypeVariable<?>) type).getBounds();
return bounds.length == 0 ? Object.class : typeToClass(bounds[0]);
} else if (type instanceof WildcardType) {
Type[] bounds = ((WildcardType) type).getUpperBounds();
return bounds.length == 0 ? Object.class : typeToClass(bounds[0]);
} else {
throw new UnsupportedOperationException("Cannot handle type class: " + type.getClass());
}
}
}

View File

@ -3,6 +3,7 @@
package com.microsoft.signalr;
import java.nio.ByteBuffer;
import java.util.Map;
import org.slf4j.Logger;
@ -59,7 +60,7 @@ class WebSocketTransport implements Transport {
}
@Override
public Completable send(String message) {
public Completable send(ByteBuffer message) {
return webSocketClient.send(message);
}
@ -70,7 +71,7 @@ class WebSocketTransport implements Transport {
}
@Override
public void onReceive(String message) {
public void onReceive(ByteBuffer message) {
this.onReceiveCallBack.invoke(message);
}

View File

@ -3,6 +3,8 @@
package com.microsoft.signalr;
import java.nio.ByteBuffer;
import io.reactivex.Completable;
abstract class WebSocketWrapper {
@ -10,7 +12,7 @@ abstract class WebSocketWrapper {
public abstract Completable stop();
public abstract Completable send(String message);
public abstract Completable send(ByteBuffer message);
public abstract void setOnReceive(OnReceiveCallBack onReceive);

View File

@ -0,0 +1,53 @@
// Copyright (c) .NET Foundation. All rights reserved.
// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information.
package com.microsoft.signalr;
import java.nio.ByteBuffer;
class ByteString{
private byte[] src;
private ByteString(byte[] src) {
this.src = src;
}
public static ByteString of(byte[] src) {
return new ByteString(src);
}
public static ByteString of(ByteBuffer src) {
return new ByteString(src.array());
}
public byte[] array() {
return src;
}
@Override
public boolean equals(Object obj) {
if (!(obj instanceof ByteString)) {
return false;
}
byte[] otherSrc = ((ByteString) obj).array();
if (otherSrc.length != src.length) {
return false;
}
for (int i = 0; i < src.length; i++) {
if (src[i] != otherSrc[i]) {
return false;
}
}
return true;
}
@Override
public String toString() {
String str = "";
for (byte b: src) {
str += String.format("%02X", b);
}
return str;
}
}

View File

@ -5,15 +5,16 @@ package com.microsoft.signalr;
import static org.junit.jupiter.api.Assertions.*;
import java.nio.ByteBuffer;
import org.junit.jupiter.api.Test;
class HandshakeProtocolTest {
@Test
public void VerifyCreateHandshakerequestMessage() {
HandshakeRequestMessage handshakeRequest = new HandshakeRequestMessage("json", 1);
String result = HandshakeProtocol.createHandshakeRequestMessage(handshakeRequest);
ByteBuffer result = HandshakeProtocol.createHandshakeRequestMessage(handshakeRequest);
String expectedResult = "{\"protocol\":\"json\",\"version\":1}\u001E";
assertEquals(expectedResult, result);
assertEquals(expectedResult, TestUtils.byteBufferToString(result));
}
@Test

View File

@ -5,12 +5,15 @@ package com.microsoft.signalr;
import static org.junit.jupiter.api.Assertions.*;
import java.lang.reflect.Type;
import java.nio.ByteBuffer;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.HashMap;
import java.util.List;
import org.junit.jupiter.api.Test;
import com.fasterxml.jackson.core.type.TypeReference;
class JsonHubProtocolTest {
private JsonHubProtocol jsonHubProtocol = new JsonHubProtocol();
@ -32,8 +35,8 @@ class JsonHubProtocolTest {
@Test
public void verifyWriteMessage() {
InvocationMessage invocationMessage = new InvocationMessage(null, "test", new Object[] {"42"});
String result = jsonHubProtocol.writeMessage(invocationMessage);
InvocationMessage invocationMessage = new InvocationMessage(null, null, "test", new Object[] {"42"}, null);
String result = TestUtils.byteBufferToString(jsonHubProtocol.writeMessage(invocationMessage));
String expectedResult = "{\"type\":1,\"target\":\"test\",\"arguments\":[\"42\"]}\u001E";
assertEquals(expectedResult, result);
}
@ -41,29 +44,33 @@ class JsonHubProtocolTest {
@Test
public void parsePingMessage() {
String stringifiedMessage = "{\"type\":6}\u001E";
TestBinder binder = new TestBinder(PingMessage.getInstance());
ByteBuffer message = TestUtils.stringToByteBuffer(stringifiedMessage);
TestBinder binder = new TestBinder(null, null);
HubMessage[] messages = jsonHubProtocol.parseMessages(stringifiedMessage, binder);
List<HubMessage> messages = jsonHubProtocol.parseMessages(message, binder);
//We know it's only one message
assertEquals(1, messages.length);
assertEquals(HubMessageType.PING, messages[0].getMessageType());
assertNotNull(messages);
assertEquals(1, messages.size());
assertEquals(HubMessageType.PING, messages.get(0).getMessageType());
}
@Test
public void parseCloseMessage() {
String stringifiedMessage = "{\"type\":7}\u001E";
TestBinder binder = new TestBinder(new CloseMessage());
ByteBuffer message = TestUtils.stringToByteBuffer(stringifiedMessage);
TestBinder binder = new TestBinder(null, null);
HubMessage[] messages = jsonHubProtocol.parseMessages(stringifiedMessage, binder);
List<HubMessage> messages = jsonHubProtocol.parseMessages(message, binder);
//We know it's only one message
assertEquals(1, messages.length);
assertNotNull(messages);
assertEquals(1, messages.size());
assertEquals(HubMessageType.CLOSE, messages[0].getMessageType());
assertEquals(HubMessageType.CLOSE, messages.get(0).getMessageType());
//We can safely cast here because we know that it's a close message.
CloseMessage closeMessage = (CloseMessage) messages[0];
CloseMessage closeMessage = (CloseMessage) messages.get(0);
assertEquals(null, closeMessage.getError());
}
@ -71,17 +78,19 @@ class JsonHubProtocolTest {
@Test
public void parseCloseMessageWithError() {
String stringifiedMessage = "{\"type\":7,\"error\": \"There was an error\"}\u001E";
TestBinder binder = new TestBinder(new CloseMessage("There was an error"));
ByteBuffer message = TestUtils.stringToByteBuffer(stringifiedMessage);
TestBinder binder = new TestBinder(new Type[] { int.class }, null);
HubMessage[] messages = jsonHubProtocol.parseMessages(stringifiedMessage, binder);
List<HubMessage> messages = jsonHubProtocol.parseMessages(message, binder);
//We know it's only one message
assertEquals(1, messages.length);
assertNotNull(messages);
assertEquals(1, messages.size());
assertEquals(HubMessageType.CLOSE, messages[0].getMessageType());
assertEquals(HubMessageType.CLOSE, messages.get(0).getMessageType());
//We can safely cast here because we know that it's a close message.
CloseMessage closeMessage = (CloseMessage) messages[0];
CloseMessage closeMessage = (CloseMessage) messages.get(0);
assertEquals("There was an error", closeMessage.getError());
}
@ -89,17 +98,19 @@ class JsonHubProtocolTest {
@Test
public void parseSingleMessage() {
String stringifiedMessage = "{\"type\":1,\"target\":\"test\",\"arguments\":[42]}\u001E";
TestBinder binder = new TestBinder(new InvocationMessage("1", "test", new Object[] { 42 }));
ByteBuffer message = TestUtils.stringToByteBuffer(stringifiedMessage);
TestBinder binder = new TestBinder(new Type[] { int.class }, null);
HubMessage[] messages = jsonHubProtocol.parseMessages(stringifiedMessage, binder);
List<HubMessage> messages = jsonHubProtocol.parseMessages(message, binder);
//We know it's only one message
assertEquals(1, messages.length);
assertNotNull(messages);
assertEquals(1, messages.size());
assertEquals(HubMessageType.INVOCATION, messages[0].getMessageType());
assertEquals(HubMessageType.INVOCATION, messages.get(0).getMessageType());
//We can safely cast here because we know that it's an invocation message.
InvocationMessage invocationMessage = (InvocationMessage) messages[0];
InvocationMessage invocationMessage = (InvocationMessage) messages.get(0);
assertEquals("test", invocationMessage.getTarget());
assertEquals(null, invocationMessage.getInvocationId());
@ -111,34 +122,39 @@ class JsonHubProtocolTest {
@Test
public void parseSingleUnsupportedStreamInvocationMessage() {
String stringifiedMessage = "{\"type\":4,\"Id\":1,\"target\":\"test\",\"arguments\":[42]}\u001E";
TestBinder binder = new TestBinder(new StreamInvocationMessage("1", "test", new Object[] { 42 }));
ByteBuffer message = TestUtils.stringToByteBuffer(stringifiedMessage);
TestBinder binder = new TestBinder(new Type[] { int.class }, null);
Throwable exception = assertThrows(UnsupportedOperationException.class, () -> jsonHubProtocol.parseMessages(stringifiedMessage, binder));
Throwable exception = assertThrows(UnsupportedOperationException.class, () -> jsonHubProtocol.parseMessages(message, binder));
assertEquals("The message type STREAM_INVOCATION is not supported yet.", exception.getMessage());
}
@Test
public void parseSingleUnsupportedCancelInvocationMessage() {
String stringifiedMessage = "{\"type\":5,\"invocationId\":123}\u001E";
TestBinder binder = new TestBinder(null);
ByteBuffer message = TestUtils.stringToByteBuffer(stringifiedMessage);
TestBinder binder = new TestBinder(null, null);
Throwable exception = assertThrows(UnsupportedOperationException.class, () -> jsonHubProtocol.parseMessages(stringifiedMessage, binder));
Throwable exception = assertThrows(UnsupportedOperationException.class, () -> jsonHubProtocol.parseMessages(message, binder));
assertEquals("The message type CANCEL_INVOCATION is not supported yet.", exception.getMessage());
}
@Test
public void parseTwoMessages() {
String twoMessages = "{\"type\":1,\"target\":\"one\",\"arguments\":[42]}\u001E{\"type\":1,\"target\":\"two\",\"arguments\":[43]}\u001E";
TestBinder binder = new TestBinder(new InvocationMessage("1", "one", new Object[] { 42 }));
String stringifiedMessage = "{\"type\":1,\"target\":\"one\",\"arguments\":[42]}\u001E{\"type\":1,\"target\":\"two\",\"arguments\":[43]}\u001E";
ByteBuffer message = TestUtils.stringToByteBuffer(stringifiedMessage);
TestBinder binder = new TestBinder(new Type[] { int.class }, null);
HubMessage[] messages = jsonHubProtocol.parseMessages(twoMessages, binder);
assertEquals(2, messages.length);
List<HubMessage> messages = jsonHubProtocol.parseMessages(message, binder);
assertNotNull(messages);
assertEquals(2, messages.size());
// Check the first message
assertEquals(HubMessageType.INVOCATION, messages[0].getMessageType());
assertEquals(HubMessageType.INVOCATION, messages.get(0).getMessageType());
//Now that we know we have an invocation message we can cast the hubMessage.
InvocationMessage invocationMessage = (InvocationMessage) messages[0];
InvocationMessage invocationMessage = (InvocationMessage) messages.get(0);
assertEquals("one", invocationMessage.getTarget());
assertEquals(null, invocationMessage.getInvocationId());
@ -146,10 +162,10 @@ class JsonHubProtocolTest {
assertEquals(42, messageResult);
// Check the second message
assertEquals(HubMessageType.INVOCATION, messages[1].getMessageType());
assertEquals(HubMessageType.INVOCATION, messages.get(1).getMessageType());
//Now that we know we have an invocation message we can cast the hubMessage.
InvocationMessage invocationMessage2 = (InvocationMessage) messages[1];
InvocationMessage invocationMessage2 = (InvocationMessage) messages.get(1);
assertEquals("two", invocationMessage2.getTarget());
assertEquals(null, invocationMessage2.getInvocationId());
@ -160,37 +176,135 @@ class JsonHubProtocolTest {
@Test
public void parseSingleMessageMutipleArgs() {
String stringifiedMessage = "{\"type\":1,\"target\":\"test\",\"arguments\":[42, 24]}\u001E";
TestBinder binder = new TestBinder(new InvocationMessage("1", "test", new Object[] { 42, 24 }));
ByteBuffer message = TestUtils.stringToByteBuffer(stringifiedMessage);
TestBinder binder = new TestBinder(new Type[] { int.class, int.class }, null);
HubMessage[] messages = jsonHubProtocol.parseMessages(stringifiedMessage, binder);
List<HubMessage> messages = jsonHubProtocol.parseMessages(message, binder);
assertNotNull(messages);
assertEquals(1, messages.size());
//We know it's only one message
assertEquals(HubMessageType.INVOCATION, messages[0].getMessageType());
assertEquals(HubMessageType.INVOCATION, messages.get(0).getMessageType());
InvocationMessage message = (InvocationMessage)messages[0];
assertEquals("test", message.getTarget());
assertEquals(null, message.getInvocationId());
int messageResult = (int) message.getArguments()[0];
int messageResult2 = (int) message.getArguments()[1];
InvocationMessage invocationMessage = (InvocationMessage)messages.get(0);
assertEquals("test", invocationMessage.getTarget());
assertEquals(null, invocationMessage.getInvocationId());
int messageResult = (int) invocationMessage.getArguments()[0];
int messageResult2 = (int) invocationMessage.getArguments()[1];
assertEquals(42, messageResult);
assertEquals(24, messageResult2);
}
@Test
public void parseSingleMessageNestedCollection() {
String stringifiedMessage = "{\"type\":1,\"target\":\"test\",\"arguments\":[[{\"one\":[\"a\",\"b\"],\"two\":[\"\uBEEF\",\"\uABCD\"]},{\"four\":[\"^\",\"*\"],\"three\":[\"5\",\"9\"]}]]}\u001E";
ByteBuffer message = TestUtils.stringToByteBuffer(stringifiedMessage);
TestBinder binder = new TestBinder(new Type[] { (new TypeReference<ArrayList<HashMap<String, ArrayList<Character>>>>() { }).getType() }, null);
List<HubMessage> messages = jsonHubProtocol.parseMessages(message, binder);
assertNotNull(messages);
assertEquals(1, messages.size());
//We know it's only one message
assertEquals(HubMessageType.INVOCATION, messages.get(0).getMessageType());
InvocationMessage invocationMessage = (InvocationMessage)messages.get(0);
assertEquals("test", invocationMessage.getTarget());
assertEquals(null, invocationMessage.getInvocationId());
assertEquals(null, invocationMessage.getHeaders());
assertEquals(null, invocationMessage.getStreamIds());
@SuppressWarnings("unchecked")
ArrayList<HashMap<String, ArrayList<Character>>> result = (ArrayList<HashMap<String, ArrayList<Character>>>)invocationMessage.getArguments()[0];
assertEquals(2, result.size());
HashMap<String, ArrayList<Character>> firstMap = result.get(0);
HashMap<String, ArrayList<Character>> secondMap = result.get(1);
assertEquals(2, firstMap.keySet().size());
assertEquals(2, secondMap.keySet().size());
ArrayList<Character> firstList = firstMap.get("one");
ArrayList<Character> secondList = firstMap.get("two");
ArrayList<Character> thirdList = secondMap.get("three");
ArrayList<Character> fourthList = secondMap.get("four");
assertEquals(2, firstList.size());
assertEquals(2, secondList.size());
assertEquals(2, thirdList.size());
assertEquals(2, fourthList.size());
assertEquals('a', (char) firstList.get(0));
assertEquals('b', (char) firstList.get(1));
assertEquals('\ubeef', (char) secondList.get(0));
assertEquals('\uabcd', (char) secondList.get(1));
assertEquals('5', (char) thirdList.get(0));
assertEquals('9', (char) thirdList.get(1));
assertEquals('^', (char) fourthList.get(0));
assertEquals('*', (char) fourthList.get(1));
}
@Test
public void parseSingleMessageCustomPojoArg() {
String stringifiedMessage = "{\"type\":1,\"target\":\"test\",\"arguments\":[{\"firstName\":\"John\",\"lastName\":\"Doe\",\"age\":30,\"t\":[5,8]}]}\u001E";
ByteBuffer message = TestUtils.stringToByteBuffer(stringifiedMessage);
TestBinder binder = new TestBinder(new Type[] { (new TypeReference<PersonPojo<ArrayList<Short>>>() { }).getType() }, null);
List<HubMessage> messages = jsonHubProtocol.parseMessages(message, binder);
//We know it's only one message
assertNotNull(messages);
assertEquals(1, messages.size());
assertEquals(HubMessageType.INVOCATION, messages.get(0).getMessageType());
//We can safely cast here because we know that it's an invocation message.
InvocationMessage invocationMessage = (InvocationMessage) messages.get(0);
assertEquals("test", invocationMessage.getTarget());
assertEquals(null, invocationMessage.getInvocationId());
assertEquals(null, invocationMessage.getHeaders());
assertEquals(null, invocationMessage.getStreamIds());
@SuppressWarnings("unchecked")
PersonPojo<ArrayList<Short>> result = (PersonPojo<ArrayList<Short>>)invocationMessage.getArguments()[0];
assertEquals("John", result.getFirstName());
assertEquals("Doe", result.getLastName());
assertEquals(30, result.getAge());
ArrayList<Short> generic = result.getT();
assertEquals(2, generic.size());
assertEquals((short)5, (short)generic.get(0));
assertEquals((short)8, (short)generic.get(1));
}
@Test
public void parseMessageWithOutOfOrderProperties() {
String stringifiedMessage = "{\"arguments\":[42, 24],\"type\":1,\"target\":\"test\"}\u001E";
TestBinder binder = new TestBinder(new InvocationMessage("1", "test", new Object[] { 42, 24 }));
ByteBuffer message = TestUtils.stringToByteBuffer(stringifiedMessage);
TestBinder binder = new TestBinder(new Type[] { int.class, int.class }, null);
HubMessage[] messages = jsonHubProtocol.parseMessages(stringifiedMessage, binder);
List<HubMessage> messages = jsonHubProtocol.parseMessages(message, binder);
assertNotNull(messages);
assertEquals(1, messages.size());
// We know it's only one message
assertEquals(HubMessageType.INVOCATION, messages[0].getMessageType());
assertEquals(HubMessageType.INVOCATION, messages.get(0).getMessageType());
InvocationMessage message = (InvocationMessage) messages[0];
assertEquals("test", message.getTarget());
assertEquals(null, message.getInvocationId());
int messageResult = (int) message.getArguments()[0];
int messageResult2 = (int) message.getArguments()[1];
InvocationMessage invocationMessage = (InvocationMessage) messages.get(0);
assertEquals("test", invocationMessage.getTarget());
assertEquals(null, invocationMessage.getInvocationId());
int messageResult = (int) invocationMessage.getArguments()[0];
int messageResult2 = (int) invocationMessage.getArguments()[1];
assertEquals(42, messageResult);
assertEquals(24, messageResult2);
}
@ -198,134 +312,111 @@ class JsonHubProtocolTest {
@Test
public void parseCompletionMessageWithOutOfOrderProperties() {
String stringifiedMessage = "{\"type\":3,\"result\":42,\"invocationId\":\"1\"}\u001E";
TestBinder binder = new TestBinder(new CompletionMessage("1", 42, null));
ByteBuffer message = TestUtils.stringToByteBuffer(stringifiedMessage);
TestBinder binder = new TestBinder(null, int.class);
HubMessage[] messages = jsonHubProtocol.parseMessages(stringifiedMessage, binder);
List<HubMessage> messages = jsonHubProtocol.parseMessages(message, binder);
assertNotNull(messages);
assertEquals(1, messages.size());
// We know it's only one message
assertEquals(HubMessageType.COMPLETION, messages[0].getMessageType());
assertEquals(HubMessageType.COMPLETION, messages.get(0).getMessageType());
CompletionMessage message = (CompletionMessage) messages[0];
assertEquals(null, message.getError());
assertEquals(42 , message.getResult());
CompletionMessage completionMessage = (CompletionMessage) messages.get(0);
assertEquals(null, completionMessage.getError());
assertEquals(42 , completionMessage.getResult());
}
@Test
public void invocationBindingFailureWhileParsingTooManyArgumentsWithOutOfOrderProperties() {
String stringifiedMessage = "{\"arguments\":[42, 24],\"type\":1,\"target\":\"test\"}\u001E";
TestBinder binder = new TestBinder(new InvocationMessage(null, "test", new Object[] { 42 }));
ByteBuffer message = TestUtils.stringToByteBuffer(stringifiedMessage);
TestBinder binder = new TestBinder(new Type[] { int.class }, null);
HubMessage[] messages = jsonHubProtocol.parseMessages(stringifiedMessage, binder);
assertEquals(1, messages.length);
assertEquals(InvocationBindingFailureMessage.class, messages[0].getClass());
InvocationBindingFailureMessage message = (InvocationBindingFailureMessage)messages[0];
assertEquals("Invocation provides 2 argument(s) but target expects 1.", message.getException().getMessage());
List<HubMessage> messages = jsonHubProtocol.parseMessages(message, binder);
assertNotNull(messages);
assertEquals(1, messages.size());
assertEquals(InvocationBindingFailureMessage.class, messages.get(0).getClass());
InvocationBindingFailureMessage invocationBindingFailureMessage = (InvocationBindingFailureMessage)messages.get(0);
assertEquals("Invocation provides 2 argument(s) but target expects 1.", invocationBindingFailureMessage.getException().getMessage());
}
@Test
public void invocationBindingFailureWhileParsingTooManyArguments() {
String stringifiedMessage = "{\"type\":1,\"target\":\"test\",\"arguments\":[42, 24]}\u001E";
TestBinder binder = new TestBinder(new InvocationMessage(null, "test", new Object[] { 42 }));
ByteBuffer message = TestUtils.stringToByteBuffer(stringifiedMessage);
TestBinder binder = new TestBinder(new Type[] { int.class }, null);
HubMessage[] messages = jsonHubProtocol.parseMessages(stringifiedMessage, binder);
assertEquals(1, messages.length);
assertEquals(InvocationBindingFailureMessage.class, messages[0].getClass());
InvocationBindingFailureMessage message = (InvocationBindingFailureMessage) messages[0];
assertEquals("Invocation provides 2 argument(s) but target expects 1.", message.getException().getMessage());
List<HubMessage> messages = jsonHubProtocol.parseMessages(message, binder);
assertNotNull(messages);
assertEquals(1, messages.size());
assertEquals(InvocationBindingFailureMessage.class, messages.get(0).getClass());
InvocationBindingFailureMessage invocationBindingFailureMessage = (InvocationBindingFailureMessage) messages.get(0);
assertEquals("Invocation provides 2 argument(s) but target expects 1.", invocationBindingFailureMessage.getException().getMessage());
}
@Test
public void invocationBindingFailureWhileParsingTooFewArguments() {
String stringifiedMessage = "{\"type\":1,\"target\":\"test\",\"arguments\":[42]}\u001E";
TestBinder binder = new TestBinder(new InvocationMessage(null, "test", new Object[] { 42, 24 }));
ByteBuffer message = TestUtils.stringToByteBuffer(stringifiedMessage);
TestBinder binder = new TestBinder(new Type[] { int.class, int.class }, null);
HubMessage[] messages = jsonHubProtocol.parseMessages(stringifiedMessage, binder);
assertEquals(1, messages.length);
assertEquals(InvocationBindingFailureMessage.class, messages[0].getClass());
InvocationBindingFailureMessage message = (InvocationBindingFailureMessage) messages[0];
assertEquals("Invocation provides 1 argument(s) but target expects 2.", message.getException().getMessage());
List<HubMessage> messages = jsonHubProtocol.parseMessages(message, binder);
assertNotNull(messages);
assertEquals(1, messages.size());
assertEquals(InvocationBindingFailureMessage.class, messages.get(0).getClass());
InvocationBindingFailureMessage invocationBindingFailureMessage = (InvocationBindingFailureMessage) messages.get(0);
assertEquals("Invocation provides 1 argument(s) but target expects 2.", invocationBindingFailureMessage.getException().getMessage());
}
@Test
public void invocationBindingFailureWhenParsingIncorrectType() {
String stringifiedMessage = "{\"type\":1,\"target\":\"test\",\"arguments\":[\"true\"]}\u001E";
TestBinder binder = new TestBinder(new InvocationMessage(null, "test", new Object[] { 42 }));
ByteBuffer message = TestUtils.stringToByteBuffer(stringifiedMessage);
TestBinder binder = new TestBinder(new Type[] { int.class }, null);
HubMessage[] messages = jsonHubProtocol.parseMessages(stringifiedMessage, binder);
assertEquals(1, messages.length);
assertEquals(InvocationBindingFailureMessage.class, messages[0].getClass());
InvocationBindingFailureMessage message = (InvocationBindingFailureMessage) messages[0];
assertEquals("java.lang.NumberFormatException: For input string: \"true\"", message.getException().getMessage());
List<HubMessage> messages = jsonHubProtocol.parseMessages(message, binder);
assertNotNull(messages);
assertEquals(1, messages.size());
assertEquals(InvocationBindingFailureMessage.class, messages.get(0).getClass());
InvocationBindingFailureMessage invocationBindingFailureMessage = (InvocationBindingFailureMessage) messages.get(0);
assertEquals("java.lang.NumberFormatException: For input string: \"true\"", invocationBindingFailureMessage.getException().getMessage());
}
@Test
public void invocationBindingFailureStillReadsJsonPayloadAfterFailure() {
String stringifiedMessage = "{\"type\":1,\"target\":\"test\",\"arguments\":[\"true\"],\"invocationId\":\"123\"}\u001E";
TestBinder binder = new TestBinder(new InvocationMessage(null, "test", new Object[] { 42 }));
ByteBuffer message = TestUtils.stringToByteBuffer(stringifiedMessage);
TestBinder binder = new TestBinder(new Type[] { int.class }, null);
HubMessage[] messages = jsonHubProtocol.parseMessages(stringifiedMessage, binder);
assertEquals(1, messages.length);
assertEquals(InvocationBindingFailureMessage.class, messages[0].getClass());
InvocationBindingFailureMessage message = (InvocationBindingFailureMessage) messages[0];
assertEquals("java.lang.NumberFormatException: For input string: \"true\"", message.getException().getMessage());
assertEquals("123", message.getInvocationId());
List<HubMessage> messages = jsonHubProtocol.parseMessages(message, binder);
assertNotNull(messages);
assertEquals(1, messages.size());
assertEquals(InvocationBindingFailureMessage.class, messages.get(0).getClass());
InvocationBindingFailureMessage invocationBindingFailureMessage = (InvocationBindingFailureMessage) messages.get(0);
assertEquals("java.lang.NumberFormatException: For input string: \"true\"", invocationBindingFailureMessage.getException().getMessage());
assertEquals("123", invocationBindingFailureMessage.getInvocationId());
}
@Test
public void errorWhileParsingIncompleteMessage() {
String stringifiedMessage = "{\"type\":1,\"target\":\"test\",\"arguments\":";
TestBinder binder = new TestBinder(new InvocationMessage(null, "test", new Object[] { 42, 24 }));
ByteBuffer message = TestUtils.stringToByteBuffer(stringifiedMessage);
TestBinder binder = new TestBinder(new Type[] { int.class, int.class }, null);
RuntimeException exception = assertThrows(RuntimeException.class,
() -> jsonHubProtocol.parseMessages(stringifiedMessage, binder));
() -> jsonHubProtocol.parseMessages(message, binder));
assertEquals("Message is incomplete.", exception.getMessage());
}
private class TestBinder implements InvocationBinder {
private Class<?>[] paramTypes = null;
private Class<?> returnType = null;
public TestBinder(HubMessage expectedMessage) {
if (expectedMessage == null) {
return;
}
switch (expectedMessage.getMessageType()) {
case STREAM_INVOCATION:
ArrayList<Class<?>> streamTypes = new ArrayList<>();
for (Object obj : ((StreamInvocationMessage) expectedMessage).getArguments()) {
streamTypes.add(obj.getClass());
}
paramTypes = streamTypes.toArray(new Class<?>[streamTypes.size()]);
break;
case INVOCATION:
ArrayList<Class<?>> types = new ArrayList<>();
for (Object obj : ((InvocationMessage) expectedMessage).getArguments()) {
types.add(obj.getClass());
}
paramTypes = types.toArray(new Class<?>[types.size()]);
break;
case STREAM_ITEM:
break;
case COMPLETION:
returnType = ((CompletionMessage)expectedMessage).getResult().getClass();
break;
default:
break;
}
}
@Override
public Class<?> getReturnType(String invocationId) {
return returnType;
}
@Override
public List<Class<?>> getParameterTypes(String methodName) {
if (paramTypes == null) {
return new ArrayList<>();
}
return new ArrayList<Class<?>>(Arrays.asList(paramTypes));
}
}
}

View File

@ -5,6 +5,7 @@ package com.microsoft.signalr;
import static org.junit.jupiter.api.Assertions.*;
import java.nio.ByteBuffer;
import java.util.HashMap;
import java.util.Map;
import java.util.concurrent.TimeUnit;
@ -22,7 +23,7 @@ public class LongPollingTransportTest {
@Test
public void LongPollingFailsToConnectWith404Response() {
TestHttpClient client = new TestHttpClient()
.on("GET", (req) -> Single.just(new HttpResponse(404, "", "")));
.on("GET", (req) -> Single.just(new HttpResponse(404, "", TestUtils.emptyByteBuffer)));
Map<String, String> headers = new HashMap<>();
LongPollingTransport transport = new LongPollingTransport(headers, client, Single.just(""));
@ -35,11 +36,12 @@ public class LongPollingTransportTest {
@Test
public void LongPollingTransportCantSendBeforeStart() {
TestHttpClient client = new TestHttpClient()
.on("GET", (req) -> Single.just(new HttpResponse(404, "", "")));
.on("GET", (req) -> Single.just(new HttpResponse(404, "", TestUtils.emptyByteBuffer)));
Map<String, String> headers = new HashMap<>();
LongPollingTransport transport = new LongPollingTransport(headers, client, Single.just(""));
Throwable exception = assertThrows(RuntimeException.class, () -> transport.send("First").timeout(1, TimeUnit.SECONDS).blockingAwait());
ByteBuffer sendBuffer = TestUtils.stringToByteBuffer("First");
Throwable exception = assertThrows(RuntimeException.class, () -> transport.send(sendBuffer).timeout(1, TimeUnit.SECONDS).blockingAwait());
assertEquals(Exception.class, exception.getCause().getClass());
assertEquals("Cannot send unless the transport is active.", exception.getCause().getMessage());
assertFalse(transport.isActive());
@ -53,9 +55,9 @@ public class LongPollingTransportTest {
.on("GET", (req) -> {
if (firstPoll.get()) {
firstPoll.set(false);
return Single.just(new HttpResponse(200, "", ""));
return Single.just(new HttpResponse(200, "", TestUtils.emptyByteBuffer));
}
return Single.just(new HttpResponse(204, "", ""));
return Single.just(new HttpResponse(204, "", TestUtils.emptyByteBuffer));
});
Map<String, String> headers = new HashMap<>();
@ -81,9 +83,9 @@ public class LongPollingTransportTest {
.on("GET", (req) -> {
if (firstPoll.get()) {
firstPoll.set(false);
return Single.just(new HttpResponse(200, "", ""));
return Single.just(new HttpResponse(200, "", TestUtils.emptyByteBuffer));
}
return Single.just(new HttpResponse(999, "", ""));
return Single.just(new HttpResponse(999, "", TestUtils.emptyByteBuffer));
});
Map<String, String> headers = new HashMap<>();
@ -104,7 +106,7 @@ public class LongPollingTransportTest {
@Test
public void CanSetAndTriggerOnReceive() {
TestHttpClient client = new TestHttpClient()
.on("GET", (req) -> Single.just(new HttpResponse(200, "", "")));
.on("GET", (req) -> Single.just(new HttpResponse(200, "", TestUtils.emptyByteBuffer)));
Map<String, String> headers = new HashMap<>();
LongPollingTransport transport = new LongPollingTransport(headers, client, Single.just(""));
@ -112,12 +114,13 @@ public class LongPollingTransportTest {
AtomicBoolean onReceivedRan = new AtomicBoolean(false);
transport.setOnReceive((message) -> {
onReceivedRan.set(true);
assertEquals("TEST", message);
assertEquals("TEST", TestUtils.byteBufferToString(message));
});
// The transport doesn't need to be active to trigger onReceive for the case
// when we are handling the last outstanding poll.
transport.onReceive("TEST");
ByteBuffer onReceiveBuffer = TestUtils.stringToByteBuffer("TEST");
transport.onReceive(onReceiveBuffer);
assertTrue(onReceivedRan.get());
}
@ -129,13 +132,13 @@ public class LongPollingTransportTest {
.on("GET", (req) -> {
if (requestCount.get() == 0) {
requestCount.incrementAndGet();
return Single.just(new HttpResponse(200, "", ""));
return Single.just(new HttpResponse(200, "", TestUtils.emptyByteBuffer));
} else if (requestCount.get() == 1) {
requestCount.incrementAndGet();
return Single.just(new HttpResponse(200, "", "TEST"));
return Single.just(new HttpResponse(200, "", TestUtils.stringToByteBuffer("TEST")));
}
return Single.just(new HttpResponse(204, "", ""));
return Single.just(new HttpResponse(204, "", TestUtils.emptyByteBuffer));
});
Map<String, String> headers = new HashMap<>();
@ -145,7 +148,7 @@ public class LongPollingTransportTest {
AtomicReference<String> message = new AtomicReference<>();
transport.setOnReceive((msg -> {
onReceiveCalled.set(true);
message.set(msg);
message.set(TestUtils.byteBufferToString(msg));
block.onComplete();
}) );
@ -165,19 +168,19 @@ public class LongPollingTransportTest {
.on("GET", (req) -> {
if (requestCount.get() == 0) {
requestCount.incrementAndGet();
return Single.just(new HttpResponse(200, "", ""));
return Single.just(new HttpResponse(200, "", TestUtils.emptyByteBuffer));
} else if (requestCount.get() == 1) {
requestCount.incrementAndGet();
return Single.just(new HttpResponse(200, "", "FIRST"));
return Single.just(new HttpResponse(200, "", TestUtils.stringToByteBuffer("FIRST")));
} else if (requestCount.get() == 2) {
requestCount.incrementAndGet();
return Single.just(new HttpResponse(200, "", "SECOND"));
return Single.just(new HttpResponse(200, "", TestUtils.stringToByteBuffer("SECOND")));
} else if (requestCount.get() == 3) {
requestCount.incrementAndGet();
return Single.just(new HttpResponse(200, "", "THIRD"));
return Single.just(new HttpResponse(200, "", TestUtils.stringToByteBuffer("THIRD")));
}
return Single.just(new HttpResponse(204, "", ""));
return Single.just(new HttpResponse(204, "", TestUtils.emptyByteBuffer));
});
Map<String, String> headers = new HashMap<>();
@ -188,7 +191,7 @@ public class LongPollingTransportTest {
AtomicInteger messageCount = new AtomicInteger();
transport.setOnReceive((msg) -> {
onReceiveCalled.set(true);
message.set(message.get() + msg);
message.set(message.get() + TestUtils.byteBufferToString(msg));
if (messageCount.incrementAndGet() == 3) {
blocker.onComplete();
}
@ -211,14 +214,14 @@ public class LongPollingTransportTest {
.on("GET", (req) -> {
if (requestCount.get() == 0) {
requestCount.incrementAndGet();
return Single.just(new HttpResponse(200, "", ""));
return Single.just(new HttpResponse(200, "", TestUtils.emptyByteBuffer));
}
assertTrue(close.blockingAwait(1, TimeUnit.SECONDS));
return Single.just(new HttpResponse(204, "", ""));
return Single.just(new HttpResponse(204, "", TestUtils.emptyByteBuffer));
}).on("POST", (req) -> {
assertFalse(req.getHeaders().isEmpty());
headerValue.set(req.getHeaders().get("KEY"));
return Single.just(new HttpResponse(200, "", ""));
return Single.just(new HttpResponse(200, "", TestUtils.emptyByteBuffer));
});
Map<String, String> headers = new HashMap<>();
@ -227,7 +230,8 @@ public class LongPollingTransportTest {
transport.setOnClose((error) -> {});
transport.start("http://example.com").timeout(1, TimeUnit.SECONDS).blockingAwait();
assertTrue(transport.send("TEST").blockingAwait(1, TimeUnit.SECONDS));
ByteBuffer sendBuffer = TestUtils.stringToByteBuffer("TEST");
assertTrue(transport.send(sendBuffer).blockingAwait(1, TimeUnit.SECONDS));
close.onComplete();
assertEquals(headerValue.get(), "VALUE");
}
@ -241,15 +245,15 @@ public class LongPollingTransportTest {
.on("GET", (req) -> {
if (requestCount.get() == 0) {
requestCount.incrementAndGet();
return Single.just(new HttpResponse(200, "", ""));
return Single.just(new HttpResponse(200, "", TestUtils.emptyByteBuffer));
}
assertTrue(close.blockingAwait(1, TimeUnit.SECONDS));
return Single.just(new HttpResponse(204, "", ""));
return Single.just(new HttpResponse(204, "", TestUtils.emptyByteBuffer));
})
.on("POST", (req) -> {
assertFalse(req.getHeaders().isEmpty());
headerValue.set(req.getHeaders().get("Authorization"));
return Single.just(new HttpResponse(200, "", ""));
return Single.just(new HttpResponse(200, "", TestUtils.emptyByteBuffer));
});
Map<String, String> headers = new HashMap<>();
@ -258,7 +262,8 @@ public class LongPollingTransportTest {
transport.setOnClose((error) -> {});
transport.start("http://example.com").timeout(1, TimeUnit.SECONDS).blockingAwait();
assertTrue(transport.send("TEST").blockingAwait(1, TimeUnit.SECONDS));
ByteBuffer sendBuffer = TestUtils.stringToByteBuffer("TEST");
assertTrue(transport.send(sendBuffer).blockingAwait(1, TimeUnit.SECONDS));
assertEquals(headerValue.get(), "Bearer TOKEN");
close.onComplete();
}
@ -273,17 +278,17 @@ public class LongPollingTransportTest {
.on("GET", (req) -> {
if (requestCount.get() == 0) {
requestCount.incrementAndGet();
return Single.just(new HttpResponse(200, "", ""));
return Single.just(new HttpResponse(200, "", TestUtils.emptyByteBuffer));
}
assertEquals("Bearer TOKEN1", req.getHeaders().get("Authorization"));
secondGet.onComplete();
assertTrue(close.blockingAwait(1, TimeUnit.SECONDS));
return Single.just(new HttpResponse(204, "", ""));
return Single.just(new HttpResponse(204, "", TestUtils.emptyByteBuffer));
})
.on("POST", (req) -> {
assertFalse(req.getHeaders().isEmpty());
headerValue.set(req.getHeaders().get("Authorization"));
return Single.just(new HttpResponse(200, "", ""));
return Single.just(new HttpResponse(200, "", TestUtils.emptyByteBuffer));
});
AtomicInteger i = new AtomicInteger(0);
@ -294,7 +299,8 @@ public class LongPollingTransportTest {
transport.start("http://example.com").timeout(1, TimeUnit.SECONDS).blockingAwait();
secondGet.blockingAwait(1, TimeUnit.SECONDS);
assertTrue(transport.send("TEST").blockingAwait(1, TimeUnit.SECONDS));
ByteBuffer sendBuffer = TestUtils.stringToByteBuffer("TEST");
assertTrue(transport.send(sendBuffer).blockingAwait(1, TimeUnit.SECONDS));
assertEquals("Bearer TOKEN2", headerValue.get());
close.onComplete();
}
@ -307,9 +313,9 @@ public class LongPollingTransportTest {
.on("GET", (req) -> {
if (firstPoll.get()) {
firstPoll.set(false);
return Single.just(new HttpResponse(200, "", ""));
return Single.just(new HttpResponse(200, "", TestUtils.emptyByteBuffer));
}
return Single.just(new HttpResponse(204, "", ""));
return Single.just(new HttpResponse(204, "", TestUtils.emptyByteBuffer));
});
Map<String, String> headers = new HashMap<>();
@ -341,16 +347,16 @@ public class LongPollingTransportTest {
.on("GET", (req) -> {
if (firstPoll.get()) {
firstPoll.set(false);
return Single.just(new HttpResponse(200, "", ""));
return Single.just(new HttpResponse(200, "", TestUtils.emptyByteBuffer));
} else {
assertTrue(block.blockingAwait(1, TimeUnit.SECONDS));
return Single.just(new HttpResponse(204, "", ""));
return Single.just(new HttpResponse(204, "", TestUtils.emptyByteBuffer));
}
})
.on("DELETE", (req) ->{
//Unblock the last poll when we sent the DELETE request.
block.onComplete();
return Single.just(new HttpResponse(200, "", ""));
return Single.just(new HttpResponse(200, "", TestUtils.emptyByteBuffer));
});
Map<String, String> headers = new HashMap<>();

View File

@ -0,0 +1,919 @@
// Copyright (c) .NET Foundation. All rights reserved.
// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information.
package com.microsoft.signalr;
import static org.junit.jupiter.api.Assertions.assertEquals;
import static org.junit.jupiter.api.Assertions.assertNotNull;
import static org.junit.jupiter.api.Assertions.assertThrows;
import static org.junit.jupiter.api.Assertions.assertTrue;
import java.lang.reflect.Type;
import java.nio.ByteBuffer;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.SortedMap;
import java.util.TreeMap;
import org.junit.jupiter.api.Test;
import com.fasterxml.jackson.core.type.TypeReference;
class MessagePackHubProtocolTest {
private MessagePackHubProtocol messagePackHubProtocol = new MessagePackHubProtocol();
@Test
public void checkProtocolName() {
assertEquals("messagepack", messagePackHubProtocol.getName());
}
@Test
public void checkVersionNumber() {
assertEquals(1, messagePackHubProtocol.getVersion());
}
@Test
public void checkTransferFormat() {
assertEquals(TransferFormat.BINARY, messagePackHubProtocol.getTransferFormat());
}
@Test
public void verifyWriteInvocationMessage() {
InvocationMessage invocationMessage = new InvocationMessage(null, null, "test", new Object[] { 42 }, null);
ByteBuffer result = messagePackHubProtocol.writeMessage(invocationMessage);
byte[] expectedBytes = {0x0C, (byte) 0x96, 0x01, (byte) 0x80, (byte) 0xC0, (byte) 0xA4, 0x74, 0x65, 0x73,
0x74, (byte) 0x91, 0x2A, (byte) 0x90};
ByteString expectedResult = ByteString.of(expectedBytes);
assertEquals(expectedResult, ByteString.of(result));
}
@Test
public void verifyWriteInvocationMessageWithHeaders() {
Map<String, String> headers = new HashMap<String, String>();
headers.put("a", "b");
headers.put("c", "d");
InvocationMessage invocationMessage = new InvocationMessage(headers, null, "test", new Object[] { 42 }, null);
ByteBuffer result = messagePackHubProtocol.writeMessage(invocationMessage);
byte[] expectedBytes = {0x14, (byte) 0x96, 0x01, (byte) 0x82, (byte) 0xA1, 0x61, (byte) 0xA1, 0x62, (byte) 0xA1, 0x63,
(byte) 0xA1, 0x64, (byte) 0xC0, (byte) 0xA4, 0x74, 0x65, 0x73, 0x74, (byte) 0x91, 0x2A, (byte) 0x90};
ByteString expectedResult = ByteString.of(expectedBytes);
assertEquals(expectedResult, ByteString.of(result));
}
@Test
public void verifyWriteStreamItem() {
StreamItem streamItem = new StreamItem(null, "id", 42);
ByteBuffer result = messagePackHubProtocol.writeMessage(streamItem);
byte[] expectedBytes = {0x07, (byte) 0x94, 0x02, (byte) 0x80, (byte) 0xA2, 0x69, 0x64, 0x2A};
ByteString expectedResult = ByteString.of(expectedBytes);
assertEquals(expectedResult, ByteString.of(result));
}
@Test
public void verifyWriteCompletionMessageNonVoid() {
CompletionMessage completionMessage = new CompletionMessage(null, "id", 42, null);
ByteBuffer result = messagePackHubProtocol.writeMessage(completionMessage);
byte[] expectedBytes = {0x08, (byte) 0x95, 0x03, (byte) 0x80, (byte) 0xA2, 0x69, 0x64, 0x03, 0x2A};
ByteString expectedResult = ByteString.of(expectedBytes);
assertEquals(expectedResult, ByteString.of(result));
}
@Test
public void verifyWriteCompletionMessageVoid() {
CompletionMessage completionMessage = new CompletionMessage(null, "id", null, null);
ByteBuffer result = messagePackHubProtocol.writeMessage(completionMessage);
byte[] expectedBytes = {0x07, (byte) 0x94, 0x03, (byte) 0x80, (byte) 0xA2, 0x69, 0x64, 0x02};
ByteString expectedResult = ByteString.of(expectedBytes);
assertEquals(expectedResult, ByteString.of(result));
}
@Test
public void verifyWriteCompletionMessageError() {
CompletionMessage completionMessage = new CompletionMessage(null, "id", null, "error");
ByteBuffer result = messagePackHubProtocol.writeMessage(completionMessage);
byte[] expectedBytes = {0x0D, (byte) 0x95, 0x03, (byte) 0x80, (byte) 0xA2, 0x69, 0x64, 0x01, (byte) 0xA5, 0x65, 0x72, 0x72, 0x6F, 0x72};
ByteString expectedResult = ByteString.of(expectedBytes);
assertEquals(expectedResult, ByteString.of(result));
}
@Test
public void verifyWriteStreamInvocationMessage() {
List<String> streamIds = new ArrayList<String>();
streamIds.add("stream");
StreamInvocationMessage streamInvocationMessage = new StreamInvocationMessage(null, "id", "test", new Object[] {42}, streamIds);
ByteBuffer result = messagePackHubProtocol.writeMessage(streamInvocationMessage);
byte[] expectedBytes = {0x15, (byte) 0x96, 0x04, (byte) 0x80, (byte) 0xA2, 0x69, 0x64, (byte) 0xA4, 0x74, 0x65, 0x73, 0x74, (byte) 0x91,
0x2A, (byte) 0x91, (byte) 0xA6, 0x73, 0x74, 0x72, 0x65, 0x61, 0x6D};
ByteString expectedResult = ByteString.of(expectedBytes);
assertEquals(expectedResult, ByteString.of(result));
}
@Test
public void verifyWriteCancelInvocationMessage() {
CancelInvocationMessage cancelInvocationMessage = new CancelInvocationMessage(null, "id");
ByteBuffer result = messagePackHubProtocol.writeMessage(cancelInvocationMessage);
byte[] expectedBytes = {0x06, (byte) 0x93, 0x05, (byte) 0x80, (byte) 0xA2, 0x69, 0x64};
ByteString expectedResult = ByteString.of(expectedBytes);
assertEquals(expectedResult, ByteString.of(result));
}
@Test
public void verifyWritePingMessage() {
ByteBuffer result = messagePackHubProtocol.writeMessage(PingMessage.getInstance());
byte[] expectedBytes = {0x02, (byte) 0x91, 0x06};
ByteString expectedResult = ByteString.of(expectedBytes);
assertEquals(expectedResult, ByteString.of(result));
}
@Test
public void verifyWriteCloseMessage() {
CloseMessage closeMessage = new CloseMessage();
ByteBuffer result = messagePackHubProtocol.writeMessage(closeMessage);
byte[] expectedBytes = {0x04, (byte) 0x93, 0x07, (byte) 0xC0, (byte) 0xC2};
ByteString expectedResult = ByteString.of(expectedBytes);
assertEquals(expectedResult, ByteString.of(result));
}
@Test
public void verifyWriteCloseMessageWithError() {
CloseMessage closeMessage = new CloseMessage("Error");
ByteBuffer result = messagePackHubProtocol.writeMessage(closeMessage);
byte[] expectedBytes = {0x09, (byte) 0x93, 0x07, (byte) 0xA5, 0x45, 0x72, 0x72, 0x6F, 0x72, (byte) 0xC2};
ByteString expectedResult = ByteString.of(expectedBytes);
assertEquals(expectedResult, ByteString.of(result));
}
@Test
public void parsePingMessage() {
byte[] messageBytes = {0x02, (byte) 0x91, 0x06};
ByteBuffer message = ByteBuffer.wrap(messageBytes);
TestBinder binder = new TestBinder(null, null);
List<HubMessage> messages = messagePackHubProtocol.parseMessages(message, binder);
//We know it's only one message
assertNotNull(messages);
assertEquals(1, messages.size());
assertEquals(HubMessageType.PING, messages.get(0).getMessageType());
}
@Test
public void parseCloseMessage() {
byte[] messageBytes = {0x04, (byte) 0x93, 0x07, (byte) 0xC0, (byte) 0xC2};
ByteBuffer message = ByteBuffer.wrap(messageBytes);
TestBinder binder = new TestBinder(null, null);
List<HubMessage> messages = messagePackHubProtocol.parseMessages(message, binder);
//We know it's only one message
assertNotNull(messages);
assertEquals(1, messages.size());
assertEquals(HubMessageType.CLOSE, messages.get(0).getMessageType());
//We can safely cast here because we know that it's a close message.
CloseMessage closeMessage = (CloseMessage) messages.get(0);
assertEquals(null, closeMessage.getError());
}
@Test
public void parseCloseMessageWithError() {
byte[] messageBytes = {0x09, (byte) 0x93, 0x07, (byte) 0xA5, 0x45, 0x72, 0x72, 0x6F, 0x72, (byte) 0xC2};
ByteBuffer message = ByteBuffer.wrap(messageBytes);
TestBinder binder = new TestBinder(null, null);
List<HubMessage> messages = messagePackHubProtocol.parseMessages(message, binder);
//We know it's only one message
assertNotNull(messages);
assertEquals(1, messages.size());
assertEquals(HubMessageType.CLOSE, messages.get(0).getMessageType());
//We can safely cast here because we know that it's a close message.
CloseMessage closeMessage = (CloseMessage) messages.get(0);
assertEquals("Error", closeMessage.getError());
}
@Test
public void parseSingleInvocationMessage() {
byte[] messageBytes = {0x0C, (byte) 0x96, 0x01, (byte) 0x80, (byte) 0xC0, (byte) 0xA4, 0x74, 0x65, 0x73,
0x74, (byte) 0x91, 0x2A, (byte) 0x90};
ByteBuffer message = ByteBuffer.wrap(messageBytes);
TestBinder binder = new TestBinder(new Type[] { int.class }, null);
List<HubMessage> messages = messagePackHubProtocol.parseMessages(message, binder);
//We know it's only one message
assertNotNull(messages);
assertEquals(1, messages.size());
assertEquals(HubMessageType.INVOCATION, messages.get(0).getMessageType());
//We can safely cast here because we know that it's an invocation message.
InvocationMessage invocationMessage = (InvocationMessage) messages.get(0);
assertEquals("test", invocationMessage.getTarget());
assertEquals(null, invocationMessage.getInvocationId());
assertEquals(null, invocationMessage.getHeaders());
assertEquals(null, invocationMessage.getStreamIds());
int messageResult = (int)invocationMessage.getArguments()[0];
assertEquals(42, messageResult);
}
@Test
public void parseSingleInvocationMessageWithHeaders() {
byte[] messageBytes = {0x14, (byte) 0x96, 0x01, (byte) 0x82, (byte) 0xA1, 0x61, (byte) 0xA1, 0x62, (byte) 0xA1, 0x63,
(byte) 0xA1, 0x64, (byte) 0xC0, (byte) 0xA4, 0x74, 0x65, 0x73, 0x74, (byte) 0x91, 0x2A, (byte) 0x90};
ByteBuffer message = ByteBuffer.wrap(messageBytes);
TestBinder binder = new TestBinder(new Type[] { int.class }, null);
List<HubMessage> messages = messagePackHubProtocol.parseMessages(message, binder);
//We know it's only one message
assertNotNull(messages);
assertEquals(1, messages.size());
assertEquals(HubMessageType.INVOCATION, messages.get(0).getMessageType());
//We can safely cast here because we know that it's an invocation message.
InvocationMessage invocationMessage = (InvocationMessage) messages.get(0);
assertEquals("test", invocationMessage.getTarget());
assertEquals(null, invocationMessage.getInvocationId());
Map<String, String> headers = invocationMessage.getHeaders();
assertEquals(2, headers.size());
assertEquals("b", headers.get("a"));
assertEquals("d", headers.get("c"));
assertEquals(null, invocationMessage.getStreamIds());
int messageResult = (int)invocationMessage.getArguments()[0];
assertEquals(42, messageResult);
}
@Test
public void parseSingleStreamInvocationMessage() {
byte[] messageBytes = {0x12, (byte) 0x96, 0x04, (byte) 0x80, (byte) 0xA6, 0x6D, 0x65, 0x74, 0x68, 0x6F, 0x64,
(byte) 0xA4, 0x74, 0x65, 0x73, 0x74, (byte) 0x91, 0x2A, (byte) 0x90};
ByteBuffer message = ByteBuffer.wrap(messageBytes);
TestBinder binder = new TestBinder(new Type[] { int.class }, null);
List<HubMessage> messages = messagePackHubProtocol.parseMessages(message, binder);
//We know it's only one message
assertNotNull(messages);
assertEquals(1, messages.size());
assertEquals(HubMessageType.STREAM_INVOCATION, messages.get(0).getMessageType());
//We can safely cast here because we know that it's a streaminvocation message.
StreamInvocationMessage streamInvocationMessage = (StreamInvocationMessage) messages.get(0);
assertEquals("test", streamInvocationMessage.getTarget());
assertEquals("method", streamInvocationMessage.getInvocationId());
assertEquals(null, streamInvocationMessage.getHeaders());
assertEquals(null, streamInvocationMessage.getStreamIds());
int messageResult = (int)streamInvocationMessage.getArguments()[0];
assertEquals(42, messageResult);
}
@Test
public void parseSingleCancelInvocationMessage() {
byte[] messageBytes = {0x0A, (byte) 0x93, 0x05, (byte) 0x80, (byte) 0xA6, 0x6D, 0x65, 0x74, 0x68, 0x6F, 0x64};
ByteBuffer message = ByteBuffer.wrap(messageBytes);
TestBinder binder = new TestBinder(null, null);
List<HubMessage> messages = messagePackHubProtocol.parseMessages(message, binder);
//We know it's only one message
assertNotNull(messages);
assertEquals(1, messages.size());
assertEquals(HubMessageType.CANCEL_INVOCATION, messages.get(0).getMessageType());
//We can safely cast here because we know that it's a cancelinvocation message.
CancelInvocationMessage cancelInvocationMessage = (CancelInvocationMessage) messages.get(0);
assertEquals("method", cancelInvocationMessage.getInvocationId());
assertEquals(null, cancelInvocationMessage.getHeaders());
}
@Test
public void parseTwoMessages() {
byte[] messageBytes = {0x0B, (byte) 0x96, 0x01, (byte) 0x80, (byte) 0xC0, (byte) 0xA3, 0x6F, 0x6E, 0x65, (byte) 0x91, 0x2A,
(byte) 0x90, 0x0B, (byte) 0x96, 0x01, (byte) 0x80, (byte) 0xC0, (byte) 0xA3, 0x74, 0x77, 0x6F, (byte) 0x91, 0x2B, (byte) 0x90};
ByteBuffer message = ByteBuffer.wrap(messageBytes);
TestBinder binder = new TestBinder(new Type[] { int.class }, null);
List<HubMessage> messages = messagePackHubProtocol.parseMessages(message, binder);
assertNotNull(messages);
assertEquals(2, messages.size());
// Check the first message
assertEquals(HubMessageType.INVOCATION, messages.get(0).getMessageType());
//Now that we know we have an invocation message we can cast the hubMessage.
InvocationMessage invocationMessage = (InvocationMessage) messages.get(0);
assertEquals("one", invocationMessage.getTarget());
assertEquals(null, invocationMessage.getInvocationId());
assertEquals(null, invocationMessage.getHeaders());
assertEquals(null, invocationMessage.getStreamIds());
int messageResult = (int)invocationMessage.getArguments()[0];
assertEquals(42, messageResult);
// Check the second message
assertEquals(HubMessageType.INVOCATION, messages.get(1).getMessageType());
//Now that we know we have an invocation message we can cast the hubMessage.
InvocationMessage invocationMessage2 = (InvocationMessage) messages.get(1);
assertEquals("two", invocationMessage2.getTarget());
assertEquals(null, invocationMessage2.getInvocationId());
assertEquals(null, invocationMessage2.getHeaders());
assertEquals(null, invocationMessage2.getStreamIds());
int secondMessageResult = (int)invocationMessage2.getArguments()[0];
assertEquals(43, secondMessageResult);
}
@Test
public void parseSingleMessageMutipleArgs() {
byte[] messageBytes = {0x0F, (byte) 0x96, 0x01, (byte) 0x80, (byte) 0xC0, (byte) 0xA4, 0x74, 0x65, 0x73, 0x74, (byte) 0x92,
0x2A, (byte) 0xA2, 0x34, 0x32, (byte) 0x90};
ByteBuffer message = ByteBuffer.wrap(messageBytes);
TestBinder binder = new TestBinder(new Type[] { int.class, String.class }, null);
List<HubMessage> messages = messagePackHubProtocol.parseMessages(message, binder);
assertNotNull(messages);
assertEquals(1, messages.size());
//We know it's only one message
assertEquals(HubMessageType.INVOCATION, messages.get(0).getMessageType());
InvocationMessage invocationMessage = (InvocationMessage)messages.get(0);
assertEquals("test", invocationMessage.getTarget());
assertEquals(null, invocationMessage.getInvocationId());
int messageResult = (int) invocationMessage.getArguments()[0];
String messageResult2 = (String) invocationMessage.getArguments()[1];
assertEquals(42, messageResult);
assertEquals("42", messageResult2);
}
@Test
public void invocationBindingFailureWhileParsingTooManyArguments() {
byte[] messageBytes = {0x0F, (byte) 0x96, 0x01, (byte) 0x80, (byte) 0xC0, (byte) 0xA4, 0x74, 0x65, 0x73, 0x74, (byte) 0x92,
0x2A, (byte) 0xA2, 0x34, 0x32, (byte) 0x90};
ByteBuffer message = ByteBuffer.wrap(messageBytes);
TestBinder binder = new TestBinder(new Type[] { int.class }, null);
List<HubMessage> messages = messagePackHubProtocol.parseMessages(message, binder);
assertNotNull(messages);
assertEquals(1, messages.size());
assertEquals(HubMessageType.INVOCATION_BINDING_FAILURE, messages.get(0).getMessageType());
InvocationBindingFailureMessage invocationBindingFailureMessage = (InvocationBindingFailureMessage)messages.get(0);
assertEquals("Invocation provides 2 argument(s) but target expects 1.", invocationBindingFailureMessage.getException().getMessage());
}
@Test
public void invocationBindingFailureWhileParsingTooFewArguments() {
byte[] messageBytes = {0x0C, (byte) 0x96, 0x01, (byte) 0x80, (byte) 0xC0, (byte) 0xA4, 0x74, 0x65, 0x73, 0x74, (byte) 0x91, 0x2A,
(byte) 0x90};
ByteBuffer message = ByteBuffer.wrap(messageBytes);
TestBinder binder = new TestBinder(new Type[] { int.class, int.class }, null);
List<HubMessage> messages = messagePackHubProtocol.parseMessages(message, binder);
assertNotNull(messages);
assertEquals(1, messages.size());
assertEquals(HubMessageType.INVOCATION_BINDING_FAILURE, messages.get(0).getMessageType());
InvocationBindingFailureMessage invocationBindingFailureMessage = (InvocationBindingFailureMessage) messages.get(0);
assertEquals("Invocation provides 1 argument(s) but target expects 2.", invocationBindingFailureMessage.getException().getMessage());
}
@Test
public void invocationBindingFailureWhenParsingIncorrectType() {
byte[] messageBytes = {0x0C, (byte) 0x96, 0x01, (byte) 0x80, (byte) 0xC0, (byte) 0xA4, 0x74, 0x65, 0x73, 0x74, (byte) 0x91,
(byte) 0xC3, (byte) 0x90};
ByteBuffer message = ByteBuffer.wrap(messageBytes);
TestBinder binder = new TestBinder(new Type[] { int.class }, null);
List<HubMessage> messages = messagePackHubProtocol.parseMessages(message, binder);
assertNotNull(messages);
assertEquals(1, messages.size());
assertEquals(HubMessageType.INVOCATION_BINDING_FAILURE, messages.get(0).getMessageType());
InvocationBindingFailureMessage invocationBindingFailureMessage = (InvocationBindingFailureMessage) messages.get(0);
// We get different exception messages on different platforms, so use a regex
assertTrue(invocationBindingFailureMessage.getException().getMessage().matches("^.*Boolean.*cannot be cast to.*Integer.*"));
}
@Test
public void invocationBindingFailureReadsNextMessageAfterTooManyArguments() {
byte[] messageBytes = {0x0F, (byte) 0x96, 0x01, (byte) 0x80, (byte) 0xC0, (byte) 0xA4, 0x74, 0x65, 0x73, 0x74, (byte) 0x92,
0x2A, (byte) 0xA2, 0x34, 0x32, (byte) 0x90, 0x0B, (byte) 0x96, 0x01, (byte) 0x80, (byte) 0xC0, (byte) 0xA3, 0x74,
0x77, 0x6F, (byte) 0x91, 0x2B, (byte) 0x90};
ByteBuffer message = ByteBuffer.wrap(messageBytes);
TestBinder binder = new TestBinder(new Type[] { int.class }, null);
List<HubMessage> messages = messagePackHubProtocol.parseMessages(message, binder);
assertNotNull(messages);
assertEquals(2, messages.size());
assertEquals(HubMessageType.INVOCATION_BINDING_FAILURE, messages.get(0).getMessageType());
InvocationBindingFailureMessage invocationBindingFailureMessage = (InvocationBindingFailureMessage) messages.get(0);
assertEquals("Invocation provides 2 argument(s) but target expects 1.", invocationBindingFailureMessage.getException().getMessage());
// Check the second message
assertEquals(HubMessageType.INVOCATION, messages.get(1).getMessageType());
//Now that we know we have an invocation message we can cast the hubMessage.
InvocationMessage invocationMessage2 = (InvocationMessage) messages.get(1);
assertEquals("two", invocationMessage2.getTarget());
assertEquals(null, invocationMessage2.getInvocationId());
assertEquals(null, invocationMessage2.getHeaders());
assertEquals(null, invocationMessage2.getStreamIds());
int secondMessageResult = (int)invocationMessage2.getArguments()[0];
assertEquals(43, secondMessageResult);
}
@Test
public void invocationBindingFailureReadsNextMessageAfterTooFewArguments() {
byte[] messageBytes = {0x0C, (byte) 0x96, 0x01, (byte) 0x80, (byte) 0xC0, (byte) 0xA4, 0x74, 0x65, 0x73, 0x74, (byte) 0x91, 0x2A,
(byte) 0x90, 0x0C, (byte) 0x96, 0x01, (byte) 0x80, (byte) 0xC0, (byte) 0xA3, 0x74, 0x77, 0x6F, (byte) 0x92, 0x2A, 0x2B, (byte) 0x90};
ByteBuffer message = ByteBuffer.wrap(messageBytes);
TestBinder binder = new TestBinder(new Type[] { int.class, int.class }, null);
List<HubMessage> messages = messagePackHubProtocol.parseMessages(message, binder);
assertNotNull(messages);
assertEquals(2, messages.size());
assertEquals(HubMessageType.INVOCATION_BINDING_FAILURE, messages.get(0).getMessageType());
InvocationBindingFailureMessage invocationBindingFailureMessage = (InvocationBindingFailureMessage) messages.get(0);
assertEquals("Invocation provides 1 argument(s) but target expects 2.", invocationBindingFailureMessage.getException().getMessage());
// Check the second message
assertEquals(HubMessageType.INVOCATION, messages.get(1).getMessageType());
//Now that we know we have an invocation message we can cast the hubMessage.
InvocationMessage invocationMessage2 = (InvocationMessage) messages.get(1);
assertEquals("two", invocationMessage2.getTarget());
assertEquals(null, invocationMessage2.getInvocationId());
assertEquals(null, invocationMessage2.getHeaders());
assertEquals(null, invocationMessage2.getStreamIds());
int secondMessageResult1 = (int)invocationMessage2.getArguments()[0];
int secondMessageResult2 = (int)invocationMessage2.getArguments()[1];
assertEquals(42, secondMessageResult1);
assertEquals(43, secondMessageResult2);
}
@Test
public void invocationBindingFailureReadsNextMessageAfterIncorrectArgument() {
byte[] messageBytes = {0x0C, (byte) 0x96, 0x01, (byte) 0x80, (byte) 0xC0, (byte) 0xA4, 0x74, 0x65, 0x73, 0x74, (byte) 0x91,
(byte) 0xC3, (byte) 0x90, 0x0C, (byte) 0x96, 0x01, (byte) 0x80, (byte) 0xC0, (byte) 0xA4, 0x74, 0x65, 0x73, 0x74,
(byte) 0x91, 0x2A, (byte) 0x90};
ByteBuffer message = ByteBuffer.wrap(messageBytes);
TestBinder binder = new TestBinder(new Type[] { int.class }, null);
List<HubMessage> messages = messagePackHubProtocol.parseMessages(message, binder);
assertNotNull(messages);
assertEquals(2, messages.size());
assertEquals(HubMessageType.INVOCATION_BINDING_FAILURE, messages.get(0).getMessageType());
InvocationBindingFailureMessage invocationBindingFailureMessage = (InvocationBindingFailureMessage) messages.get(0);
// We get different exception messages on different platforms, so use a regex
assertTrue(invocationBindingFailureMessage.getException().getMessage().matches("^.*Boolean.*cannot be cast to.*Integer.*"));
// Check the second message
assertEquals(HubMessageType.INVOCATION, messages.get(1).getMessageType());
//Now that we know we have an invocation message we can cast the hubMessage.
InvocationMessage invocationMessage2 = (InvocationMessage) messages.get(1);
assertEquals("test", invocationMessage2.getTarget());
assertEquals(null, invocationMessage2.getInvocationId());
assertEquals(null, invocationMessage2.getHeaders());
assertEquals(null, invocationMessage2.getStreamIds());
int secondMessageResult = (int)invocationMessage2.getArguments()[0];
assertEquals(42, secondMessageResult);
}
@Test
public void errorWhenLengthHeaderTooLong() {
byte[] messageBytes = {0x0D, (byte) 0x96, 0x01, (byte) 0x80, (byte) 0xC0, (byte) 0xA4, 0x74, 0x65, 0x73, 0x74, (byte) 0x91,
0x2A, (byte) 0x90};
ByteBuffer message = ByteBuffer.wrap(messageBytes);
TestBinder binder = new TestBinder(new Type[] { int.class }, null);
RuntimeException exception = assertThrows(RuntimeException.class,
() -> messagePackHubProtocol.parseMessages(message, binder));
assertEquals("MessagePack message was length 12 but claimed to be length 13.", exception.getMessage());
}
@Test
public void errorWhenLengthHeaderTooShort() {
byte[] messageBytes = {0x0B, (byte) 0x96, 0x01, (byte) 0x80, (byte) 0xC0, (byte) 0xA4, 0x74, 0x65, 0x73, 0x74, (byte) 0x91,
0x2A, (byte) 0x90};
ByteBuffer message = ByteBuffer.wrap(messageBytes);
TestBinder binder = new TestBinder(new Type[] { int.class }, null);
RuntimeException exception = assertThrows(RuntimeException.class,
() -> messagePackHubProtocol.parseMessages(message, binder));
assertEquals("MessagePack message was length 12 but claimed to be length 11.", exception.getMessage());
}
@Test
public void parseMessageWithTwoByteLengthHeader() {
// Test that a long message w/ a 2-byte length header is still parsed correctly
byte[] messageBytes = {(byte) 0x87, 0x01, (byte) 0x96, 0x01, (byte) 0x80, (byte) 0xC0, (byte) 0xA4, 0x74, 0x65, 0x73, 0x74,
(byte) 0x91, (byte) 0xD9, 0x7A, 0x54, 0x68, 0x69, 0x73, 0x20, 0x69, 0x73, 0x20, 0x61, 0x20, 0x72, 0x65, 0x61, 0x6C, 0x6C,
0x79, 0x20, 0x6C, 0x6F, 0x6E, 0x67, 0x20, 0x61, 0x72, 0x67, 0x75, 0x6D, 0x65, 0x6E, 0x74, 0x20, 0x74, 0x6F, 0x20, 0x6D,
0x61, 0x6B, 0x65, 0x20, 0x74, 0x68, 0x65, 0x20, 0x6C, 0x65, 0x6E, 0x67, 0x74, 0x68, 0x20, 0x6F, 0x66, 0x20, 0x74, 0x68,
0x69, 0x73, 0x20, 0x6D, 0x65, 0x73, 0x73, 0x61, 0x67, 0x65, 0x20, 0x6D, 0x6F, 0x72, 0x65, 0x20, 0x74, 0x68, 0x61, 0x6E,
0x20, 0x31, 0x32, 0x37, 0x20, 0x62, 0x79, 0x74, 0x65, 0x73, 0x2E, 0x20, 0x57, 0x65, 0x20, 0x6A, 0x75, 0x73, 0x74, 0x20,
0x6E, 0x65, 0x65, 0x64, 0x20, 0x61, 0x20, 0x66, 0x65, 0x77, 0x20, 0x6D, 0x6F, 0x72, 0x65, 0x20, 0x63, 0x68, 0x61, 0x72,
0x61, 0x63, 0x74, 0x65, 0x72, 0x73, 0x2E, (byte) 0x90};
ByteBuffer message = ByteBuffer.wrap(messageBytes);
TestBinder binder = new TestBinder(new Type[] { String.class }, null);
List<HubMessage> messages = messagePackHubProtocol.parseMessages(message, binder);
//We know it's only one message
assertNotNull(messages);
assertEquals(1, messages.size());
assertEquals(HubMessageType.INVOCATION, messages.get(0).getMessageType());
//We can safely cast here because we know that it's an invocation message.
InvocationMessage invocationMessage = (InvocationMessage) messages.get(0);
assertEquals("test", invocationMessage.getTarget());
assertEquals(null, invocationMessage.getInvocationId());
assertEquals(null, invocationMessage.getHeaders());
assertEquals(null, invocationMessage.getStreamIds());
String messageResult = (String)invocationMessage.getArguments()[0];
assertEquals("This is a really long argument to make the length of this message more than "
+ "127 bytes. We just need a few more characters.", messageResult);
}
@Test
public void verifyWriteInvocationMessageWithTwoByteLengthHeader() {
InvocationMessage invocationMessage = new InvocationMessage(null, null, "test", new Object[] { "This is a really long argument to make "
+ "the length of this message more than 127 bytes. We just need a few more characters." }, null);
ByteBuffer result = messagePackHubProtocol.writeMessage(invocationMessage);
byte[] expectedBytes = {(byte) 0x87, 0x01, (byte) 0x96, 0x01, (byte) 0x80, (byte) 0xC0, (byte) 0xA4, 0x74, 0x65, 0x73, 0x74,
(byte) 0x91, (byte) 0xD9, 0x7A, 0x54, 0x68, 0x69, 0x73, 0x20, 0x69, 0x73, 0x20, 0x61, 0x20, 0x72, 0x65, 0x61, 0x6C, 0x6C,
0x79, 0x20, 0x6C, 0x6F, 0x6E, 0x67, 0x20, 0x61, 0x72, 0x67, 0x75, 0x6D, 0x65, 0x6E, 0x74, 0x20, 0x74, 0x6F, 0x20, 0x6D,
0x61, 0x6B, 0x65, 0x20, 0x74, 0x68, 0x65, 0x20, 0x6C, 0x65, 0x6E, 0x67, 0x74, 0x68, 0x20, 0x6F, 0x66, 0x20, 0x74, 0x68,
0x69, 0x73, 0x20, 0x6D, 0x65, 0x73, 0x73, 0x61, 0x67, 0x65, 0x20, 0x6D, 0x6F, 0x72, 0x65, 0x20, 0x74, 0x68, 0x61, 0x6E,
0x20, 0x31, 0x32, 0x37, 0x20, 0x62, 0x79, 0x74, 0x65, 0x73, 0x2E, 0x20, 0x57, 0x65, 0x20, 0x6A, 0x75, 0x73, 0x74, 0x20,
0x6E, 0x65, 0x65, 0x64, 0x20, 0x61, 0x20, 0x66, 0x65, 0x77, 0x20, 0x6D, 0x6F, 0x72, 0x65, 0x20, 0x63, 0x68, 0x61, 0x72,
0x61, 0x63, 0x74, 0x65, 0x72, 0x73, 0x2E, (byte) 0x90};
ByteString expectedResult = ByteString.of(expectedBytes);
assertEquals(expectedResult, ByteString.of(result));
}
@Test
public void parseInvocationMessageWithPrimitiveArgs() {
byte[] messageBytes = {0x1E, (byte) 0x96, 0x01, (byte) 0x80, (byte) 0xC0, (byte) 0xA4, 0x74, 0x65, 0x73, 0x74, (byte) 0x96, 0x01, (byte) 0xCB,
0x40, 0x04, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, (byte) 0xC3, 0x11, (byte) 0xA1, 0x63, (byte) 0xCE, (byte) 0xC6, (byte) 0xAE, (byte) 0xA1,
0x55, (byte) 0x90};
ByteBuffer message = ByteBuffer.wrap(messageBytes);
int i = 1;
double d = 2.5d;
boolean bool = true;
byte bite = 0x11;
char c = 'c';
long l = 3333333333l;
TestBinder binder = new TestBinder(new Type[] { int.class, double.class, boolean.class, byte.class, char.class, long.class }, null);
List<HubMessage> messages = messagePackHubProtocol.parseMessages(message, binder);
//We know it's only one message
assertNotNull(messages);
assertEquals(1, messages.size());
assertEquals(HubMessageType.INVOCATION, messages.get(0).getMessageType());
//We can safely cast here because we know that it's an invocation message.
InvocationMessage invocationMessage = (InvocationMessage) messages.get(0);
assertEquals("test", invocationMessage.getTarget());
assertEquals(null, invocationMessage.getInvocationId());
assertEquals(null, invocationMessage.getHeaders());
assertEquals(null, invocationMessage.getStreamIds());
Object[] args = invocationMessage.getArguments();
assertEquals(6, args.length);
assertEquals(i, (int)args[0]);
assertEquals(d, (double)args[1]);
assertEquals(bool, (boolean)args[2]);
assertEquals(bite, (byte)args[3]);
assertEquals(c, (char)args[4]);
assertEquals(l, (long)args[5]);
}
@Test
public void verifyWriteInvocationMessageWithPrimitiveArgs() {
int i = 1;
double d = 2.5d;
boolean bool = true;
byte bite = 0x11;
char c = 'c';
long l = 3333333333l;
InvocationMessage invocationMessage = new InvocationMessage(null, null, "test", new Object[] { i, d, bool, bite, c, l }, null);
ByteBuffer result = messagePackHubProtocol.writeMessage(invocationMessage);
byte[] expectedBytes = {0x1E, (byte) 0x96, 0x01, (byte) 0x80, (byte) 0xC0, (byte) 0xA4, 0x74, 0x65, 0x73, 0x74, (byte) 0x96, 0x01,
(byte) 0xCB, 0x40, 0x04, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, (byte) 0xC3, 0x11, (byte) 0xA1, 0x63, (byte) 0xCE, (byte) 0xC6, (byte) 0xAE,
(byte) 0xA1, 0x55, (byte) 0x90};
ByteString expectedResult = ByteString.of(expectedBytes);
assertEquals(expectedResult, ByteString.of(result));
}
@Test
public void parseInvocationMessageWithArrayArg() {
// Make sure that the same bytes can be parsed as both an Array and a List
byte[] messageBytes = {0x10, (byte) 0x96, 0x01, (byte) 0x80, (byte) 0xC0, (byte) 0xA4, 0x74, 0x65, 0x73, 0x74, (byte) 0x91, (byte) 0x94, 0x01,
0x02, 0x03, 0x04, (byte) 0x90};
ByteBuffer message = ByteBuffer.wrap(messageBytes);
TestBinder arrayBinder = new TestBinder(new Type[] { int[].class }, null);
TestBinder listBinder = new TestBinder(new Type[] { (new TypeReference<ArrayList<Integer>>() { }).getType() }, null);
List<HubMessage> arrayMessages = messagePackHubProtocol.parseMessages(message, arrayBinder);
message.flip();
List<HubMessage> listMessages = messagePackHubProtocol.parseMessages(message, listBinder);
//We know it's only one message
assertNotNull(arrayMessages);
assertEquals(1, arrayMessages.size());
assertNotNull(listMessages);
assertEquals(1, listMessages.size());
assertEquals(HubMessageType.INVOCATION, arrayMessages.get(0).getMessageType());
assertEquals(HubMessageType.INVOCATION, listMessages.get(0).getMessageType());
//We can safely cast here because we know that it's an invocation message.
InvocationMessage arrayInvocationMessage = (InvocationMessage) arrayMessages.get(0);
InvocationMessage listInvocationMessage = (InvocationMessage) listMessages.get(0);
assertEquals("test", arrayInvocationMessage.getTarget());
assertEquals(null, arrayInvocationMessage.getInvocationId());
assertEquals(null, arrayInvocationMessage.getHeaders());
assertEquals(null, arrayInvocationMessage.getStreamIds());
assertEquals("test", listInvocationMessage.getTarget());
assertEquals(null, listInvocationMessage.getInvocationId());
assertEquals(null, listInvocationMessage.getHeaders());
assertEquals(null, listInvocationMessage.getStreamIds());
int[] arrayArg = (int[])arrayInvocationMessage.getArguments()[0];
@SuppressWarnings("unchecked")
List<Integer> listArg = (ArrayList<Integer>)listInvocationMessage.getArguments()[0];
assertEquals(4, arrayArg.length);
assertEquals(4, listArg.size());
for (int i = 0; i < arrayArg.length; i++) {
assertEquals(i + 1, arrayArg[i]);
assertEquals(i + 1, (int) listArg.get(i));
}
}
@Test
public void verifyWriteInvocationMessageWithArrayArg() {
InvocationMessage invocationMessage = new InvocationMessage(null, null, "test", new Object[] { new int[] { 1, 2, 3, 4 } }, null);
ByteBuffer result = messagePackHubProtocol.writeMessage(invocationMessage);
byte[] expectedBytes = {0x10, (byte) 0x96, 0x01, (byte) 0x80, (byte) 0xC0, (byte) 0xA4, 0x74, 0x65, 0x73, 0x74, (byte) 0x91, (byte) 0x94, 0x01,
0x02, 0x03, 0x04, (byte) 0x90};
ByteString expectedResult = ByteString.of(expectedBytes);
assertEquals(expectedResult, ByteString.of(result));
}
@Test
public void parseInvocationMessageWithMapArg() {
byte[] messageBytes = {0x23, (byte) 0x96, 0x01, (byte) 0x80, (byte) 0xC0, (byte) 0xA4, 0x74, 0x65, 0x73, 0x74, (byte) 0x91, (byte) 0x82, (byte) 0xA5,
0x61, 0x70, 0x70, 0x6C, 0x65, (byte) 0xA6, 0x62, 0x61, 0x6E, 0x61, 0x6E, 0x61, (byte) 0xA3, 0x6B, 0x65, 0x79, (byte) 0xA5, 0x76, 0x61, 0x6C, 0x75,
0x65, (byte) 0x90};
ByteBuffer message = ByteBuffer.wrap(messageBytes);
TestBinder binder = new TestBinder(new Type[] { (new TypeReference<HashMap<String, String>>() { }).getType() }, null);
List<HubMessage> messages = messagePackHubProtocol.parseMessages(message, binder);
//We know it's only one message
assertNotNull(messages);
assertEquals(1, messages.size());
assertEquals(HubMessageType.INVOCATION, messages.get(0).getMessageType());
//We can safely cast here because we know that it's an invocation message.
InvocationMessage invocationMessage = (InvocationMessage) messages.get(0);
assertEquals("test", invocationMessage.getTarget());
assertEquals(null, invocationMessage.getInvocationId());
assertEquals(null, invocationMessage.getHeaders());
assertEquals(null, invocationMessage.getStreamIds());
@SuppressWarnings("unchecked")
Map<String, String> result = (HashMap<String, String>)invocationMessage.getArguments()[0];
assertEquals(2, result.size());
assertEquals("value", result.get("key"));
assertEquals("banana", result.get("apple"));
}
@Test
public void verifyWriteInvocationMessageWithMapArg() {
SortedMap<String, String> argument = new TreeMap<String, String>();
argument.put("apple", "banana");
argument.put("key", "value");
InvocationMessage invocationMessage = new InvocationMessage(null, null, "test", new Object[] { argument }, null);
ByteBuffer result = messagePackHubProtocol.writeMessage(invocationMessage);
byte[] expectedBytes = {0x23, (byte) 0x96, 0x01, (byte) 0x80, (byte) 0xC0, (byte) 0xA4, 0x74, 0x65, 0x73, 0x74, (byte) 0x91, (byte) 0x82, (byte) 0xA5,
0x61, 0x70, 0x70, 0x6C, 0x65, (byte) 0xA6, 0x62, 0x61, 0x6E, 0x61, 0x6E, 0x61, (byte) 0xA3, 0x6B, 0x65, 0x79, (byte) 0xA5, 0x76, 0x61, 0x6C, 0x75,
0x65, (byte) 0x90};
ByteString expectedResult = ByteString.of(expectedBytes);
assertEquals(expectedResult, ByteString.of(result));
}
@Test
public void parseInvocationMessageWithNestedCollection() {
byte[] messageBytes = {0x39, (byte) 0x96, 0x01, (byte) 0x80, (byte) 0xC0, (byte) 0xA4, 0x74, 0x65, 0x73, 0x74, (byte) 0x91, (byte) 0x92,
(byte) 0x82, (byte) 0xA3, 0x6F, 0x6E, 0x65, (byte) 0x92, (byte) 0xA1, 0x61, (byte) 0xA1, 0x62, (byte) 0xA3, 0x74, 0x77, 0x6F, (byte) 0x92,
(byte) 0xA3, (byte) 0xEB, (byte) 0xBB, (byte) 0xAF, (byte) 0xA3, (byte) 0xEA, (byte) 0xAF, (byte) 0x8D, (byte) 0x82, (byte) 0xA4, 0x66,
0x6F, 0x75, 0x72, (byte) 0x92, (byte) 0xA1, 0x5E, (byte) 0xA1, 0x2A, (byte) 0xA5, 0x74, 0x68, 0x72, 0x65, 0x65, (byte) 0x92, (byte) 0xA1,
0x35, (byte) 0xA1, 0x39, (byte) 0x90};
ByteBuffer message = ByteBuffer.wrap(messageBytes);
TestBinder binder = new TestBinder(new Type[] { (new TypeReference<ArrayList<HashMap<String, ArrayList<Character>>>>() { }).getType() }, null);
List<HubMessage> messages = messagePackHubProtocol.parseMessages(message, binder);
//We know it's only one message
assertNotNull(messages);
assertEquals(1, messages.size());
assertEquals(HubMessageType.INVOCATION, messages.get(0).getMessageType());
//We can safely cast here because we know that it's an invocation message.
InvocationMessage invocationMessage = (InvocationMessage) messages.get(0);
assertEquals("test", invocationMessage.getTarget());
assertEquals(null, invocationMessage.getInvocationId());
assertEquals(null, invocationMessage.getHeaders());
assertEquals(null, invocationMessage.getStreamIds());
@SuppressWarnings("unchecked")
ArrayList<HashMap<String, ArrayList<Character>>> result = (ArrayList<HashMap<String, ArrayList<Character>>>)invocationMessage.getArguments()[0];
assertEquals(2, result.size());
HashMap<String, ArrayList<Character>> firstMap = result.get(0);
HashMap<String, ArrayList<Character>> secondMap = result.get(1);
assertEquals(2, firstMap.keySet().size());
assertEquals(2, secondMap.keySet().size());
ArrayList<Character> firstList = firstMap.get("one");
ArrayList<Character> secondList = firstMap.get("two");
ArrayList<Character> thirdList = secondMap.get("three");
ArrayList<Character> fourthList = secondMap.get("four");
assertEquals(2, firstList.size());
assertEquals(2, secondList.size());
assertEquals(2, thirdList.size());
assertEquals(2, fourthList.size());
assertEquals('a', (char) firstList.get(0));
assertEquals('b', (char) firstList.get(1));
assertEquals('\ubeef', (char) secondList.get(0));
assertEquals('\uabcd', (char) secondList.get(1));
assertEquals('5', (char) thirdList.get(0));
assertEquals('9', (char) thirdList.get(1));
assertEquals('^', (char) fourthList.get(0));
assertEquals('*', (char) fourthList.get(1));
}
@Test
public void verifyWriteInvocationMessageWithNestedCollection() {
ArrayList<Character> clist1 = new ArrayList<Character>();
ArrayList<Character> clist2 = new ArrayList<Character>();
ArrayList<Character> clist3 = new ArrayList<Character>();
ArrayList<Character> clist4 = new ArrayList<Character>();
clist1.add('a');
clist1.add('b');
clist2.add('\ubeef');
clist2.add('\uabcd');
clist3.add('5');
clist3.add('9');
clist4.add('^');
clist4.add('*');
TreeMap<String, ArrayList<Character>> map1 = new TreeMap<String, ArrayList<Character>>();
TreeMap<String, ArrayList<Character>> map2 = new TreeMap<String, ArrayList<Character>>();
map1.put("one", clist1);
map1.put("two", clist2);
map2.put("three", clist3);
map2.put("four", clist4);
ArrayList<TreeMap<String, ArrayList<Character>>> argument = new ArrayList<TreeMap<String, ArrayList<Character>>>();
argument.add(map1);
argument.add(map2);
InvocationMessage invocationMessage = new InvocationMessage(null, null, "test", new Object[] { argument }, null);
ByteBuffer result = messagePackHubProtocol.writeMessage(invocationMessage);
byte[] expectedBytes = {0x39, (byte) 0x96, 0x01, (byte) 0x80, (byte) 0xC0, (byte) 0xA4, 0x74, 0x65, 0x73, 0x74, (byte) 0x91, (byte) 0x92,
(byte) 0x82, (byte) 0xA3, 0x6F, 0x6E, 0x65, (byte) 0x92, (byte) 0xA1, 0x61, (byte) 0xA1, 0x62, (byte) 0xA3, 0x74, 0x77, 0x6F, (byte) 0x92,
(byte) 0xA3, (byte) 0xEB, (byte) 0xBB, (byte) 0xAF, (byte) 0xA3, (byte) 0xEA, (byte) 0xAF, (byte) 0x8D, (byte) 0x82, (byte) 0xA4, 0x66,
0x6F, 0x75, 0x72, (byte) 0x92, (byte) 0xA1, 0x5E, (byte) 0xA1, 0x2A, (byte) 0xA5, 0x74, 0x68, 0x72, 0x65, 0x65, (byte) 0x92, (byte) 0xA1,
0x35, (byte) 0xA1, 0x39, (byte) 0x90};
ByteString expectedResult = ByteString.of(expectedBytes);
assertEquals(expectedResult, ByteString.of(result));
}
@Test
public void parseInvocationMessageWithCustomPojoArg() {
byte[] messageBytes = {0x32, (byte) 0x96, 0x01, (byte) 0x80, (byte) 0xC0, (byte) 0xA4, 0x74, 0x65, 0x73, 0x74, (byte) 0x91, (byte) 0x84, (byte) 0xA9,
0x66, 0x69, 0x72, 0x73, 0x74, 0x4E, 0x61, 0x6D, 0x65, (byte) 0xA4, 0x4A, 0x6F, 0x68, 0x6E, (byte) 0xA8, 0x6C, 0x61, 0x73, 0x74, 0x4E, 0x61,
0x6D, 0x65, (byte) 0xA3, 0x44, 0x6F, 0x65, (byte) 0xA3, 0x61, 0x67, 0x65, 0x1E, (byte) 0xA1, 0x74, (byte) 0x92, 0x05, 0x08, (byte) 0x90};
ByteBuffer message = ByteBuffer.wrap(messageBytes);
TestBinder binder = new TestBinder(new Type[] { (new TypeReference<PersonPojo<ArrayList<Short>>>() { }).getType() }, null);
List<HubMessage> messages = messagePackHubProtocol.parseMessages(message, binder);
//We know it's only one message
assertNotNull(messages);
assertEquals(1, messages.size());
assertEquals(HubMessageType.INVOCATION, messages.get(0).getMessageType());
//We can safely cast here because we know that it's an invocation message.
InvocationMessage invocationMessage = (InvocationMessage) messages.get(0);
assertEquals("test", invocationMessage.getTarget());
assertEquals(null, invocationMessage.getInvocationId());
assertEquals(null, invocationMessage.getHeaders());
assertEquals(null, invocationMessage.getStreamIds());
@SuppressWarnings("unchecked")
PersonPojo<ArrayList<Short>> result = (PersonPojo<ArrayList<Short>>)invocationMessage.getArguments()[0];
assertEquals("John", result.getFirstName());
assertEquals("Doe", result.getLastName());
assertEquals(30, result.getAge());
ArrayList<Short> generic = result.getT();
assertEquals(2, generic.size());
assertEquals((short)5, (short)generic.get(0));
assertEquals((short)8, (short)generic.get(1));
}
@Test
public void verifyWriteInvocationMessageWithCustomPojoArg() {
ArrayList<Short> shorts = new ArrayList<Short>();
shorts.add((short) 5);
shorts.add((short) 8);
PersonPojo<ArrayList<Short>> person = new PersonPojo<ArrayList<Short>>("John", "Doe", 30, shorts);
InvocationMessage invocationMessage = new InvocationMessage(null, null, "test", new Object[] { person }, null);
ByteBuffer result = messagePackHubProtocol.writeMessage(invocationMessage);
byte[] expectedBytes = {0x32, (byte) 0x96, 0x01, (byte) 0x80, (byte) 0xC0, (byte) 0xA4, 0x74, 0x65, 0x73, 0x74, (byte) 0x91, (byte) 0x84, (byte) 0xA9,
0x66, 0x69, 0x72, 0x73, 0x74, 0x4E, 0x61, 0x6D, 0x65, (byte) 0xA4, 0x4A, 0x6F, 0x68, 0x6E, (byte) 0xA8, 0x6C, 0x61, 0x73, 0x74, 0x4E, 0x61,
0x6D, 0x65, (byte) 0xA3, 0x44, 0x6F, 0x65, (byte) 0xA3, 0x61, 0x67, 0x65, 0x1E, (byte) 0xA1, 0x74, (byte) 0x92, 0x05, 0x08, (byte) 0x90};
ByteString expectedResult = ByteString.of(expectedBytes);
assertEquals(expectedResult, ByteString.of(result));
}
}

View File

@ -3,6 +3,7 @@
package com.microsoft.signalr;
import java.nio.ByteBuffer;
import java.util.ArrayList;
import io.reactivex.Completable;
@ -11,14 +12,14 @@ import io.reactivex.subjects.SingleSubject;
class MockTransport implements Transport {
private OnReceiveCallBack onReceiveCallBack;
private ArrayList<String> sentMessages = new ArrayList<>();
private ArrayList<ByteBuffer> sentMessages = new ArrayList<>();
private String url;
private TransportOnClosedCallback onClose;
final private boolean ignorePings;
final private boolean autoHandshake;
final private CompletableSubject startSubject = CompletableSubject.create();
final private CompletableSubject stopSubject = CompletableSubject.create();
private SingleSubject<String> sendSubject = SingleSubject.create();
private SingleSubject<ByteBuffer> sendSubject = SingleSubject.create();
private static final String RECORD_SEPARATOR = "\u001e";
@ -40,7 +41,7 @@ class MockTransport implements Transport {
this.url = url;
if (autoHandshake) {
try {
onReceiveCallBack.invoke("{}" + RECORD_SEPARATOR);
onReceiveCallBack.invoke(TestUtils.stringToByteBuffer("{}" + RECORD_SEPARATOR));
} catch (Exception e) {
throw new RuntimeException(e);
}
@ -50,8 +51,8 @@ class MockTransport implements Transport {
}
@Override
public Completable send(String message) {
if (!(ignorePings && message.equals("{\"type\":6}" + RECORD_SEPARATOR))) {
public Completable send(ByteBuffer message) {
if (!(ignorePings && isPing(message))) {
sentMessages.add(message);
sendSubject.onSuccess(message);
sendSubject = SingleSubject.create();
@ -65,7 +66,7 @@ class MockTransport implements Transport {
}
@Override
public void onReceive(String message) {
public void onReceive(ByteBuffer message) {
this.onReceiveCallBack.invoke(message);
}
@ -86,14 +87,18 @@ class MockTransport implements Transport {
}
public void receiveMessage(String message) {
this.onReceive(TestUtils.stringToByteBuffer(message));
}
public void receiveMessage(ByteBuffer message) {
this.onReceive(message);
}
public String[] getSentMessages() {
return sentMessages.toArray(new String[sentMessages.size()]);
public ByteBuffer[] getSentMessages() {
return sentMessages.toArray(new ByteBuffer[sentMessages.size()]);
}
public SingleSubject<String> getNextSentMessage() {
public SingleSubject<ByteBuffer> getNextSentMessage() {
return sendSubject;
}
@ -108,4 +113,9 @@ class MockTransport implements Transport {
public Completable getStopTask() {
return stopSubject;
}
private boolean isPing(ByteBuffer message) {
return (TestUtils.byteBufferToString(message).equals("{\"type\":6}" + RECORD_SEPARATOR) ||
(message.array()[0] == 2 && message.array()[1] == -111 && message.array()[2] == 6));
}
}

View File

@ -0,0 +1,43 @@
// Copyright (c) .NET Foundation. All rights reserved.
// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information.
package com.microsoft.signalr;
class PersonPojo<T> implements Comparable<PersonPojo<T>> {
public String firstName;
public String lastName;
public int age;
public T t;
public PersonPojo() {
super();
}
public PersonPojo(String firstName, String lastName, int age, T t) {
this.firstName = firstName;
this.lastName = lastName;
this.age = age;
this.t = t;
}
public String getFirstName() {
return this.firstName;
}
public String getLastName() {
return this.lastName;
}
public int getAge() {
return this.age;
}
public T getT() {
return t;
}
@Override
public int compareTo(PersonPojo<T> ep) {
return 0;
}
}

View File

@ -0,0 +1,32 @@
// Copyright (c) .NET Foundation. All rights reserved.
// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information.
package com.microsoft.signalr;
import java.lang.reflect.Type;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.List;
class TestBinder implements InvocationBinder {
private Type[] paramTypes = null;
private Type returnType = null;
public TestBinder(Type[] paramTypes, Type returnType) {
this.paramTypes = paramTypes;
this.returnType = returnType;
}
@Override
public Type getReturnType(String invocationId) {
return returnType;
}
@Override
public List<Type> getParameterTypes(String methodName) {
if (paramTypes == null) {
return new ArrayList<>();
}
return new ArrayList<Type>(Arrays.asList(paramTypes));
}
}

View File

@ -3,6 +3,7 @@
package com.microsoft.signalr;
import java.nio.ByteBuffer;
import java.util.ArrayList;
import java.util.List;
import java.util.Map;
@ -27,7 +28,7 @@ class TestHttpClient extends HttpClient {
}
@Override
public Single<HttpResponse> send(HttpRequest request, String body) {
public Single<HttpResponse> send(HttpRequest request, ByteBuffer body) {
this.sentRequests.add(request);
return this.handler.invoke(request);
}

View File

@ -3,21 +3,47 @@
package com.microsoft.signalr;
import java.nio.ByteBuffer;
import java.nio.charset.StandardCharsets;
class TestUtils {
static ByteBuffer emptyByteBuffer = stringToByteBuffer("");
static HubConnection createHubConnection(String url) {
return createHubConnection(url, new MockTransport(true), true, new TestHttpClient());
return createHubConnection(url, new MockTransport(true), true, new TestHttpClient(), false);
}
static HubConnection createHubConnection(String url, Transport transport) {
return createHubConnection(url, transport, true, new TestHttpClient());
return createHubConnection(url, transport, true, new TestHttpClient(), false);
}
static HubConnection createHubConnection(String url, boolean withMessagePack) {
return createHubConnection(url, new MockTransport(true), true, new TestHttpClient(), withMessagePack);
}
static HubConnection createHubConnection(String url, Transport transport, boolean withMessagePack) {
return createHubConnection(url, transport, true, new TestHttpClient(), withMessagePack);
}
static HubConnection createHubConnection(String url, Transport transport, boolean skipNegotiate, HttpClient client) {
static HubConnection createHubConnection(String url, Transport transport, boolean skipNegotiate, HttpClient client, boolean withMessagePack) {
HttpHubConnectionBuilder builder = HubConnectionBuilder.create(url)
.withTransportImplementation(transport)
.withHttpClient(client)
.shouldSkipNegotiate(skipNegotiate);
.withTransportImplementation(transport)
.withHttpClient(client)
.shouldSkipNegotiate(skipNegotiate);
if (withMessagePack) {
builder = builder.withMessagePackHubProtocol();
}
return builder.build();
}
static String byteBufferToString(ByteBuffer buffer) {
return new String(buffer.array(), StandardCharsets.UTF_8);
}
static ByteBuffer stringToByteBuffer(String s) {
return ByteBuffer.wrap(s.getBytes(StandardCharsets.UTF_8));
}
}

View File

@ -5,6 +5,7 @@ package com.microsoft.signalr;
import static org.junit.jupiter.api.Assertions.*;
import java.nio.ByteBuffer;
import java.util.HashMap;
import java.util.Map;
import java.util.concurrent.TimeUnit;
@ -35,7 +36,7 @@ class WebSocketTransportTest {
}
@Override
public Single<HttpResponse> send(HttpRequest request, String body) {
public Single<HttpResponse> send(HttpRequest request, ByteBuffer body) {
return null;
}
@ -71,7 +72,7 @@ class WebSocketTransportTest {
}
@Override
public Completable send(String message) {
public Completable send(ByteBuffer message) {
return null;
}