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' implementation 'com.squareup.okhttp3:okhttp:3.11.0'
api 'io.reactivex.rxjava2:rxjava:2.2.3' api 'io.reactivex.rxjava2:rxjava:2.2.3'
implementation 'org.slf4j:slf4j-api:1.7.25' 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 { spotless {

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -3,15 +3,18 @@
package com.microsoft.signalr; package com.microsoft.signalr;
import java.nio.charset.StandardCharsets;
import java.nio.ByteBuffer;
import com.google.gson.Gson; import com.google.gson.Gson;
final class HandshakeProtocol { final class HandshakeProtocol {
private static final Gson gson = new Gson(); private static final Gson gson = new Gson();
private static final String RECORD_SEPARATOR = "\u001e"; 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 // 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) { public static HandshakeResponseMessage parseHandshakeResponse(String message) {

View File

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

View File

@ -16,6 +16,7 @@ public class HttpHubConnectionBuilder {
private final String url; private final String url;
private Transport transport; private Transport transport;
private HttpClient httpClient; private HttpClient httpClient;
private HubProtocol protocol = new JsonHubProtocol();
private boolean skipNegotiate; private boolean skipNegotiate;
private Single<String> accessTokenProvider; private Single<String> accessTokenProvider;
private long handshakeResponseTimeout = 0; private long handshakeResponseTimeout = 0;
@ -55,6 +56,16 @@ public class HttpHubConnectionBuilder {
return this; 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. * Indicates to the {@link HubConnection} that it should skip the negotiate process.
* Note: This option only works with the Websockets transport and the Azure SignalR Service require the negotiate step. * Note: This option only works with the Websockets transport and the Azure SignalR Service require the negotiate step.
@ -133,7 +144,7 @@ public class HttpHubConnectionBuilder {
* @return A new instance of {@link HubConnection}. * @return A new instance of {@link HubConnection}.
*/ */
public HubConnection build() { public HubConnection build() {
return new HubConnection(url, transport, skipNegotiate, httpClient, accessTokenProvider, return new HubConnection(url, transport, skipNegotiate, httpClient, protocol, accessTokenProvider,
handshakeResponseTimeout, headers, transportEnum, configureBuilder); handshakeResponseTimeout, headers, transportEnum, configureBuilder);
} }
} }

View File

@ -4,6 +4,10 @@
package com.microsoft.signalr; package com.microsoft.signalr;
import java.io.StringReader; 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.*;
import java.util.concurrent.*; import java.util.concurrent.*;
import java.util.concurrent.atomic.AtomicInteger; import java.util.concurrent.atomic.AtomicInteger;
@ -26,8 +30,8 @@ import okhttp3.OkHttpClient;
* A connection used to invoke hub methods on a SignalR Server. * A connection used to invoke hub methods on a SignalR Server.
*/ */
public class HubConnection implements AutoCloseable { public class HubConnection implements AutoCloseable {
private static final String RECORD_SEPARATOR = "\u001e"; private static final byte RECORD_SEPARATOR = 0x1e;
private static final List<Class<?>> emptyArray = new ArrayList<>(); private static final List<Type> emptyArray = new ArrayList<>();
private static final int MAX_NEGOTIATE_ATTEMPTS = 100; private static final int MAX_NEGOTIATE_ATTEMPTS = 100;
private String baseUrl; private String baseUrl;
@ -126,7 +130,7 @@ public class HubConnection implements AutoCloseable {
return transport; 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, Single<String> accessTokenProvider, long handshakeResponseTimeout, Map<String, String> headers, TransportEnum transportEnum,
Action1<OkHttpClient.Builder> configureBuilder) { Action1<OkHttpClient.Builder> configureBuilder) {
if (url == null || url.isEmpty()) { if (url == null || url.isEmpty()) {
@ -134,7 +138,7 @@ public class HubConnection implements AutoCloseable {
} }
this.baseUrl = url; this.baseUrl = url;
this.protocol = new JsonHubProtocol(); this.protocol = protocol;
if (accessTokenProvider != null) { if (accessTokenProvider != null) {
this.accessTokenProvider = accessTokenProvider; this.accessTokenProvider = accessTokenProvider;
@ -165,8 +169,20 @@ public class HubConnection implements AutoCloseable {
this.callback = (payload) -> { this.callback = (payload) -> {
resetServerTimeout(); resetServerTimeout();
if (!handshakeReceived) { if (!handshakeReceived) {
int handshakeLength = payload.indexOf(RECORD_SEPARATOR) + 1; List<Byte> handshakeByteList = new ArrayList<Byte>();
String handshakeResponseString = payload.substring(0, handshakeLength - 1); 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; HandshakeResponseMessage handshakeResponse;
try { try {
handshakeResponse = HandshakeProtocol.parseHandshakeResponse(handshakeResponseString); handshakeResponse = HandshakeProtocol.parseHandshakeResponse(handshakeResponseString);
@ -185,14 +201,13 @@ public class HubConnection implements AutoCloseable {
handshakeReceived = true; handshakeReceived = true;
handshakeResponseSubject.onComplete(); handshakeResponseSubject.onComplete();
payload = payload.substring(handshakeLength);
// The payload only contained the handshake response so we can return. // The payload only contained the handshake response so we can return.
if (payload.length() == 0) { if (!payload.hasRemaining()) {
return; return;
} }
} }
HubMessage[] messages = protocol.parseMessages(payload, connectionState); List<HubMessage> messages = protocol.parseMessages(payload, connectionState);
for (HubMessage message : messages) { for (HubMessage message : messages) {
logger.debug("Received message of type {}.", message.getMessageType()); 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.", throw new RuntimeException(String.format("Unexpected status code returned from negotiate: %d %s.",
response.getStatusCode(), response.getStatusText())); 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); NegotiateResponse negotiateResponse = new NegotiateResponse(reader);
if (negotiateResponse.getError() != null) { if (negotiateResponse.getError() != null) {
@ -372,7 +387,7 @@ public class HubConnection implements AutoCloseable {
transport.setOnClose((message) -> stopConnection(message)); transport.setOnClose((message) -> stopConnection(message));
return transport.start(negotiateResponse.getFinalUrl()).andThen(Completable.defer(() -> { return transport.start(negotiateResponse.getFinalUrl()).andThen(Completable.defer(() -> {
String handshake = HandshakeProtocol.createHandshakeRequestMessage( ByteBuffer handshake = HandshakeProtocol.createHandshakeRequestMessage(
new HandshakeRequestMessage(protocol.getName(), protocol.getVersion())); new HandshakeRequestMessage(protocol.getName(), protocol.getVersion()));
connectionState = new ConnectionState(this); connectionState = new ConnectionState(this);
@ -585,9 +600,9 @@ public class HubConnection implements AutoCloseable {
args = checkUploadStream(args, streamIds); args = checkUploadStream(args, streamIds);
InvocationMessage invocationMessage; InvocationMessage invocationMessage;
if (isStreamInvocation) { if (isStreamInvocation) {
invocationMessage = new StreamInvocationMessage(id, method, args, streamIds); invocationMessage = new StreamInvocationMessage(null, id, method, args, streamIds);
} else { } else {
invocationMessage = new InvocationMessage(id, method, args, streamIds); invocationMessage = new InvocationMessage(null, id, method, args, streamIds);
} }
sendHubMessage(invocationMessage); sendHubMessage(invocationMessage);
@ -602,13 +617,13 @@ public class HubConnection implements AutoCloseable {
for (String streamId: streamIds) { for (String streamId: streamIds) {
Observable observable = this.streamMap.get(streamId); Observable observable = this.streamMap.get(streamId);
observable.subscribe( observable.subscribe(
(item) -> sendHubMessage(new StreamItem(streamId, item)), (item) -> sendHubMessage(new StreamItem(null, streamId, item)),
(error) -> { (error) -> {
sendHubMessage(new CompletionMessage(streamId, null, error.toString())); sendHubMessage(new CompletionMessage(null, streamId, null, error.toString()));
this.streamMap.remove(streamId); this.streamMap.remove(streamId);
}, },
() -> { () -> {
sendHubMessage(new CompletionMessage(streamId, null, null)); sendHubMessage(new CompletionMessage(null, streamId, null, null));
this.streamMap.remove(streamId); this.streamMap.remove(streamId);
}); });
} }
@ -678,8 +693,26 @@ public class HubConnection implements AutoCloseable {
* @param <T> The expected return type. * @param <T> The expected return type.
* @return A Single that yields the return value when the invocation has completed. * @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) { 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(); hubConnectionStateLock.lock();
try { try {
if (hubConnectionState != HubConnectionState.CONNECTED) { if (hubConnectionState != HubConnectionState.CONNECTED) {
@ -697,10 +730,10 @@ public class HubConnection implements AutoCloseable {
Subject<Object> pendingCall = irq.getPendingCall(); Subject<Object> pendingCall = irq.getPendingCall();
pendingCall.subscribe(result -> { pendingCall.subscribe(result -> {
// Primitive types can't be cast with the Class cast function // Primitive types can't be cast with the Class cast function
if (returnType.isPrimitive()) { if (returnClass.isPrimitive()) {
subject.onSuccess((T)result); subject.onSuccess((T)result);
} else { } else {
subject.onSuccess(returnType.cast(result)); subject.onSuccess((T)returnClass.cast(result));
} }
}, error -> subject.onError(error)); }, error -> subject.onError(error));
@ -722,8 +755,26 @@ public class HubConnection implements AutoCloseable {
* @param <T> The expected return type. * @param <T> The expected return type.
* @return An observable that yields the streaming results from the server. * @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) { 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; String invocationId;
InvocationRequest irq; InvocationRequest irq;
hubConnectionStateLock.lock(); hubConnectionStateLock.lock();
@ -741,10 +792,10 @@ public class HubConnection implements AutoCloseable {
Subject<Object> pendingCall = irq.getPendingCall(); Subject<Object> pendingCall = irq.getPendingCall();
pendingCall.subscribe(result -> { pendingCall.subscribe(result -> {
// Primitive types can't be cast with the Class cast function // Primitive types can't be cast with the Class cast function
if (returnType.isPrimitive()) { if (returnClass.isPrimitive()) {
subject.onNext((T)result); subject.onNext((T)result);
} else { } else {
subject.onNext(returnType.cast(result)); subject.onNext((T)returnClass.cast(result));
} }
}, error -> subject.onError(error), }, error -> subject.onError(error),
() -> subject.onComplete()); () -> subject.onComplete());
@ -753,7 +804,7 @@ public class HubConnection implements AutoCloseable {
sendInvocationMessage(method, args, invocationId, true); sendInvocationMessage(method, args, invocationId, true);
return observable.doOnDispose(() -> { return observable.doOnDispose(() -> {
if (subscriptionCount.decrementAndGet() == 0) { if (subscriptionCount.decrementAndGet() == 0) {
CancelInvocationMessage cancelInvocationMessage = new CancelInvocationMessage(invocationId); CancelInvocationMessage cancelInvocationMessage = new CancelInvocationMessage(null, invocationId);
sendHubMessage(cancelInvocationMessage); sendHubMessage(cancelInvocationMessage);
if (connectionState != null) { if (connectionState != null) {
connectionState.tryRemoveInvocation(invocationId); connectionState.tryRemoveInvocation(invocationId);
@ -767,7 +818,7 @@ public class HubConnection implements AutoCloseable {
} }
private void sendHubMessage(HubMessage message) { private void sendHubMessage(HubMessage message) {
String serializedMessage = protocol.writeMessage(message); ByteBuffer serializedMessage = protocol.writeMessage(message);
if (message.getMessageType() == HubMessageType.INVOCATION ) { if (message.getMessageType() == HubMessageType.INVOCATION ) {
logger.debug("Sending {} message '{}'.", message.getMessageType().name(), ((InvocationMessage)message).getInvocationId()); logger.debug("Sending {} message '{}'.", message.getMessageType().name(), ((InvocationMessage)message).getInvocationId());
} else if (message.getMessageType() == HubMessageType.STREAM_INVOCATION) { } 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. * 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 target The name of the hub method to define.
* @param callback The handler that will be raised when the hub method is invoked. * @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. * 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 target The name of the hub method to define.
* @param callback The handler that will be raised when the hub method is invoked. * @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. * 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 target The name of the hub method to define.
* @param callback The handler that will be raised when the hub method is invoked. * @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. * 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 target The name of the hub method to define.
* @param callback The handler that will be raised when the hub method is invoked. * @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. * 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 target The name of the hub method to define.
* @param callback The handler that will be raised when the hub method is invoked. * @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. * 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 target The name of the hub method to define.
* @param callback The handler that will be raised when the hub method is invoked. * @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. * 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 target The name of the hub method to define.
* @param callback The handler that will be raised when the hub method is invoked. * @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. * 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 target The name of the hub method to define.
* @param callback The handler that will be raised when the hub method is invoked. * @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); 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); InvocationHandler handler = handlers.put(target, action, types);
logger.debug("Registering handler for client method: '{}'.", target); logger.debug("Registering handler for client method: '{}'.", target);
return new Subscription(handlers, handler, target); return new Subscription(handlers, handler, target);
@ -1087,7 +1363,7 @@ public class HubConnection implements AutoCloseable {
} }
@Override @Override
public Class<?> getReturnType(String invocationId) { public Type getReturnType(String invocationId) {
InvocationRequest irq = getInvocation(invocationId); InvocationRequest irq = getInvocation(invocationId);
if (irq == null) { if (irq == null) {
return null; return null;
@ -1097,7 +1373,7 @@ public class HubConnection implements AutoCloseable {
} }
@Override @Override
public List<Class<?>> getParameterTypes(String methodName) { public List<Type> getParameterTypes(String methodName) {
List<InvocationHandler> handlers = connection.handlers.get(methodName); List<InvocationHandler> handlers = connection.handlers.get(methodName);
if (handlers == null) { if (handlers == null) {
logger.warn("Failed to find handler for '{}' method.", methodName); 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)); 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), CANCEL_INVOCATION(5),
PING(6), PING(6),
CLOSE(7), CLOSE(7),
INVOCATION_BINDING_FAILURE(-1); INVOCATION_BINDING_FAILURE(-1),
STREAM_BINDING_FAILURE(-2);
public int value; public int value;
HubMessageType(int id) { this.value = id; } HubMessageType(int id) { this.value = id; }

View File

@ -3,6 +3,9 @@
package com.microsoft.signalr; package com.microsoft.signalr;
import java.nio.ByteBuffer;
import java.util.List;
/** /**
* A protocol abstraction for communicating with SignalR hubs. * A protocol abstraction for communicating with SignalR hubs.
*/ */
@ -13,15 +16,15 @@ interface HubProtocol {
/** /**
* Creates a new list of {@link HubMessage}s. * 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. * @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. * Writes the specified {@link HubMessage} to a String.
* @param message The message to write. * @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; package com.microsoft.signalr;
import java.lang.reflect.Type;
import java.util.List; import java.util.List;
/**
* An abstraction for passing around information about method signatures.
*/
interface InvocationBinder { interface InvocationBinder {
Class<?> getReturnType(String invocationId); Type getReturnType(String invocationId);
List<Class<?>> getParameterTypes(String methodName); List<Type> getParameterTypes(String methodName);
} }

View File

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

View File

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

View File

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

View File

@ -5,7 +5,11 @@ package com.microsoft.signalr;
import java.io.IOException; import java.io.IOException;
import java.io.StringReader; 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.ArrayList;
import java.util.Arrays;
import java.util.List; import java.util.List;
import com.google.gson.Gson; import com.google.gson.Gson;
@ -36,15 +40,26 @@ class JsonHubProtocol implements HubProtocol {
} }
@Override @Override
public HubMessage[] parseMessages(String payload, InvocationBinder binder) { public List<HubMessage> parseMessages(ByteBuffer payload, InvocationBinder binder) {
if (payload.length() == 0) { String payloadStr;
return new HubMessage[]{}; // 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."); throw new RuntimeException("Message is incomplete.");
} }
String[] messages = payload.split(RECORD_SEPARATOR); String[] messages = payloadStr.split(RECORD_SEPARATOR);
List<HubMessage> hubMessages = new ArrayList<>(); List<HubMessage> hubMessages = new ArrayList<>();
try { try {
for (String str : messages) { for (String str : messages) {
@ -87,7 +102,7 @@ class JsonHubProtocol implements HubProtocol {
if (target != null) { if (target != null) {
boolean startedArray = false; boolean startedArray = false;
try { try {
List<Class<?>> types = binder.getParameterTypes(target); List<Type> types = binder.getParameterTypes(target);
startedArray = true; startedArray = true;
arguments = bindArguments(reader, types); arguments = bindArguments(reader, types);
} catch (Exception ex) { } catch (Exception ex) {
@ -125,7 +140,7 @@ class JsonHubProtocol implements HubProtocol {
case INVOCATION: case INVOCATION:
if (argumentsToken != null) { if (argumentsToken != null) {
try { try {
List<Class<?>> types = binder.getParameterTypes(target); List<Type> types = binder.getParameterTypes(target);
arguments = bindArguments(argumentsToken, types); arguments = bindArguments(argumentsToken, types);
} catch (Exception ex) { } catch (Exception ex) {
argumentBindingException = ex; argumentBindingException = ex;
@ -135,25 +150,25 @@ class JsonHubProtocol implements HubProtocol {
hubMessages.add(new InvocationBindingFailureMessage(invocationId, target, argumentBindingException)); hubMessages.add(new InvocationBindingFailureMessage(invocationId, target, argumentBindingException));
} else { } else {
if (arguments == null) { if (arguments == null) {
hubMessages.add(new InvocationMessage(invocationId, target, new Object[0])); hubMessages.add(new InvocationMessage(null, invocationId, target, new Object[0], null));
} else { } else {
hubMessages.add(new InvocationMessage(invocationId, target, arguments.toArray())); hubMessages.add(new InvocationMessage(null, invocationId, target, arguments.toArray(), null));
} }
} }
break; break;
case COMPLETION: case COMPLETION:
if (resultToken != null) { if (resultToken != null) {
Class<?> returnType = binder.getReturnType(invocationId); Type returnType = binder.getReturnType(invocationId);
result = gson.fromJson(resultToken, returnType != null ? returnType : Object.class); 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; break;
case STREAM_ITEM: case STREAM_ITEM:
if (resultToken != null) { if (resultToken != null) {
Class<?> returnType = binder.getReturnType(invocationId); Type returnType = binder.getReturnType(invocationId);
result = gson.fromJson(resultToken, returnType != null ? returnType : Object.class); result = gson.fromJson(resultToken, returnType != null ? returnType : Object.class);
} }
hubMessages.add(new StreamItem(invocationId, result)); hubMessages.add(new StreamItem(null, invocationId, result));
break; break;
case STREAM_INVOCATION: case STREAM_INVOCATION:
case CANCEL_INVOCATION: case CANCEL_INVOCATION:
@ -176,15 +191,15 @@ class JsonHubProtocol implements HubProtocol {
throw new RuntimeException("Error reading JSON.", ex); throw new RuntimeException("Error reading JSON.", ex);
} }
return hubMessages.toArray(new HubMessage[hubMessages.size()]); return hubMessages;
} }
@Override @Override
public String writeMessage(HubMessage hubMessage) { public ByteBuffer writeMessage(HubMessage hubMessage) {
return gson.toJson(hubMessage) + RECORD_SEPARATOR; 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()) { if (argumentsToken.size() != paramTypes.size()) {
throw new RuntimeException(String.format("Invocation provides %d argument(s) but target expects %d.", 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; 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(); reader.beginArray();
int paramCount = paramTypes.size(); int paramCount = paramTypes.size();
int argCount = 0; int argCount = 0;

View File

@ -3,6 +3,8 @@
package com.microsoft.signalr; package com.microsoft.signalr;
import java.nio.ByteBuffer;
import java.nio.charset.StandardCharsets;
import java.util.Map; import java.util.Map;
import java.util.concurrent.ExecutorService; import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors; import java.util.concurrent.Executors;
@ -100,7 +102,7 @@ class LongPollingTransport implements Transport {
} else { } else {
if (response.getContent() != null) { if (response.getContent() != null) {
logger.debug("Message received."); logger.debug("Message received.");
onReceiveThread.submit(() ->this.onReceive(response.getContent())); onReceiveThread.submit(() -> this.onReceive(response.getContent()));
} else { } else {
logger.debug("Poll timed out, reissuing."); logger.debug("Poll timed out, reissuing.");
} }
@ -121,7 +123,7 @@ class LongPollingTransport implements Transport {
} }
@Override @Override
public Completable send(String message) { public Completable send(ByteBuffer message) {
if (!this.active) { if (!this.active) {
return Completable.error(new Exception("Cannot send unless the transport is active.")); return Completable.error(new Exception("Cannot send unless the transport is active."));
} }
@ -138,7 +140,7 @@ class LongPollingTransport implements Transport {
} }
@Override @Override
public void onReceive(String message) { public void onReceive(ByteBuffer message) {
this.onReceiveCallBack.invoke(message); this.onReceiveCallBack.invoke(message);
logger.debug("OnReceived callback has been invoked."); 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; package com.microsoft.signalr;
import java.nio.ByteBuffer;
import java.nio.charset.StandardCharsets;
import java.util.Map; import java.util.Map;
import java.util.concurrent.locks.ReentrantLock; import java.util.concurrent.locks.ReentrantLock;
@ -17,6 +19,7 @@ import okhttp3.Request;
import okhttp3.Response; import okhttp3.Response;
import okhttp3.WebSocket; import okhttp3.WebSocket;
import okhttp3.WebSocketListener; import okhttp3.WebSocketListener;
import okio.ByteString;
class OkHttpWebSocketWrapper extends WebSocketWrapper { class OkHttpWebSocketWrapper extends WebSocketWrapper {
private WebSocket websocketClient; private WebSocket websocketClient;
@ -60,8 +63,9 @@ class OkHttpWebSocketWrapper extends WebSocketWrapper {
} }
@Override @Override
public Completable send(String message) { public Completable send(ByteBuffer message) {
websocketClient.send(message); ByteString bs = ByteString.of(message);
websocketClient.send(bs);
return Completable.complete(); return Completable.complete();
} }
@ -83,7 +87,12 @@ class OkHttpWebSocketWrapper extends WebSocketWrapper {
@Override @Override
public void onMessage(WebSocket webSocket, String message) { 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 @Override

View File

@ -3,6 +3,8 @@
package com.microsoft.signalr; package com.microsoft.signalr;
import java.nio.ByteBuffer;
interface OnReceiveCallBack { 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; package com.microsoft.signalr;
import java.util.Collection; import java.util.Collection;
import java.util.Map;
final class StreamInvocationMessage extends InvocationMessage { final class StreamInvocationMessage extends InvocationMessage {
public StreamInvocationMessage(String invocationId, String target, Object[] args) { public StreamInvocationMessage(Map<String, String> headers, String invocationId, String target, Object[] args, Collection<String> streamIds) {
super(invocationId, target, args); super(headers, invocationId, target, args, streamIds);
super.type = HubMessageType.STREAM_INVOCATION.value;
}
public StreamInvocationMessage(String invocationId, String target, Object[] args, Collection<String> streamIds) {
super(invocationId, target, args, streamIds);
super.type = HubMessageType.STREAM_INVOCATION.value; super.type = HubMessageType.STREAM_INVOCATION.value;
} }

View File

@ -3,16 +3,26 @@
package com.microsoft.signalr; package com.microsoft.signalr;
import java.util.Map;
final class StreamItem extends HubMessage { final class StreamItem extends HubMessage {
private final int type = HubMessageType.STREAM_ITEM.value; private final int type = HubMessageType.STREAM_ITEM.value;
private Map<String, String> headers;
private final String invocationId; private final String invocationId;
private final Object item; 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.invocationId = invocationId;
this.item = item; this.item = item;
} }
public Map<String, String> getHeaders() {
return headers;
}
public String getInvocationId() { public String getInvocationId() {
return invocationId; return invocationId;
} }

View File

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

View File

@ -3,6 +3,16 @@
package com.microsoft.signalr; 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 { class Utils {
public static String appendQueryString(String original, String queryStringValue) { public static String appendQueryString(String original, String queryStringValue) {
if (original.contains("?")) { if (original.contains("?")) {
@ -11,4 +21,92 @@ class Utils {
return original + "?" + queryStringValue; 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; package com.microsoft.signalr;
import java.nio.ByteBuffer;
import java.util.Map; import java.util.Map;
import org.slf4j.Logger; import org.slf4j.Logger;
@ -59,7 +60,7 @@ class WebSocketTransport implements Transport {
} }
@Override @Override
public Completable send(String message) { public Completable send(ByteBuffer message) {
return webSocketClient.send(message); return webSocketClient.send(message);
} }
@ -70,7 +71,7 @@ class WebSocketTransport implements Transport {
} }
@Override @Override
public void onReceive(String message) { public void onReceive(ByteBuffer message) {
this.onReceiveCallBack.invoke(message); this.onReceiveCallBack.invoke(message);
} }

View File

@ -3,6 +3,8 @@
package com.microsoft.signalr; package com.microsoft.signalr;
import java.nio.ByteBuffer;
import io.reactivex.Completable; import io.reactivex.Completable;
abstract class WebSocketWrapper { abstract class WebSocketWrapper {
@ -10,7 +12,7 @@ abstract class WebSocketWrapper {
public abstract Completable stop(); public abstract Completable stop();
public abstract Completable send(String message); public abstract Completable send(ByteBuffer message);
public abstract void setOnReceive(OnReceiveCallBack onReceive); 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 static org.junit.jupiter.api.Assertions.*;
import java.nio.ByteBuffer;
import org.junit.jupiter.api.Test; import org.junit.jupiter.api.Test;
class HandshakeProtocolTest { class HandshakeProtocolTest {
@Test @Test
public void VerifyCreateHandshakerequestMessage() { public void VerifyCreateHandshakerequestMessage() {
HandshakeRequestMessage handshakeRequest = new HandshakeRequestMessage("json", 1); HandshakeRequestMessage handshakeRequest = new HandshakeRequestMessage("json", 1);
String result = HandshakeProtocol.createHandshakeRequestMessage(handshakeRequest); ByteBuffer result = HandshakeProtocol.createHandshakeRequestMessage(handshakeRequest);
String expectedResult = "{\"protocol\":\"json\",\"version\":1}\u001E"; String expectedResult = "{\"protocol\":\"json\",\"version\":1}\u001E";
assertEquals(expectedResult, result); assertEquals(expectedResult, TestUtils.byteBufferToString(result));
} }
@Test @Test

View File

@ -5,12 +5,15 @@ package com.microsoft.signalr;
import static org.junit.jupiter.api.Assertions.*; import static org.junit.jupiter.api.Assertions.*;
import java.lang.reflect.Type;
import java.nio.ByteBuffer;
import java.util.ArrayList; import java.util.ArrayList;
import java.util.Arrays; import java.util.HashMap;
import java.util.List; import java.util.List;
import org.junit.jupiter.api.Test; import org.junit.jupiter.api.Test;
import com.fasterxml.jackson.core.type.TypeReference;
class JsonHubProtocolTest { class JsonHubProtocolTest {
private JsonHubProtocol jsonHubProtocol = new JsonHubProtocol(); private JsonHubProtocol jsonHubProtocol = new JsonHubProtocol();
@ -32,8 +35,8 @@ class JsonHubProtocolTest {
@Test @Test
public void verifyWriteMessage() { public void verifyWriteMessage() {
InvocationMessage invocationMessage = new InvocationMessage(null, "test", new Object[] {"42"}); InvocationMessage invocationMessage = new InvocationMessage(null, null, "test", new Object[] {"42"}, null);
String result = jsonHubProtocol.writeMessage(invocationMessage); String result = TestUtils.byteBufferToString(jsonHubProtocol.writeMessage(invocationMessage));
String expectedResult = "{\"type\":1,\"target\":\"test\",\"arguments\":[\"42\"]}\u001E"; String expectedResult = "{\"type\":1,\"target\":\"test\",\"arguments\":[\"42\"]}\u001E";
assertEquals(expectedResult, result); assertEquals(expectedResult, result);
} }
@ -41,29 +44,33 @@ class JsonHubProtocolTest {
@Test @Test
public void parsePingMessage() { public void parsePingMessage() {
String stringifiedMessage = "{\"type\":6}\u001E"; 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 //We know it's only one message
assertEquals(1, messages.length); assertNotNull(messages);
assertEquals(HubMessageType.PING, messages[0].getMessageType()); assertEquals(1, messages.size());
assertEquals(HubMessageType.PING, messages.get(0).getMessageType());
} }
@Test @Test
public void parseCloseMessage() { public void parseCloseMessage() {
String stringifiedMessage = "{\"type\":7}\u001E"; 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 //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. //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()); assertEquals(null, closeMessage.getError());
} }
@ -71,17 +78,19 @@ class JsonHubProtocolTest {
@Test @Test
public void parseCloseMessageWithError() { public void parseCloseMessageWithError() {
String stringifiedMessage = "{\"type\":7,\"error\": \"There was an error\"}\u001E"; 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 //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. //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()); assertEquals("There was an error", closeMessage.getError());
} }
@ -89,17 +98,19 @@ class JsonHubProtocolTest {
@Test @Test
public void parseSingleMessage() { public void parseSingleMessage() {
String stringifiedMessage = "{\"type\":1,\"target\":\"test\",\"arguments\":[42]}\u001E"; 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 //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. //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("test", invocationMessage.getTarget());
assertEquals(null, invocationMessage.getInvocationId()); assertEquals(null, invocationMessage.getInvocationId());
@ -111,34 +122,39 @@ class JsonHubProtocolTest {
@Test @Test
public void parseSingleUnsupportedStreamInvocationMessage() { public void parseSingleUnsupportedStreamInvocationMessage() {
String stringifiedMessage = "{\"type\":4,\"Id\":1,\"target\":\"test\",\"arguments\":[42]}\u001E"; 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()); assertEquals("The message type STREAM_INVOCATION is not supported yet.", exception.getMessage());
} }
@Test @Test
public void parseSingleUnsupportedCancelInvocationMessage() { public void parseSingleUnsupportedCancelInvocationMessage() {
String stringifiedMessage = "{\"type\":5,\"invocationId\":123}\u001E"; 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()); assertEquals("The message type CANCEL_INVOCATION is not supported yet.", exception.getMessage());
} }
@Test @Test
public void parseTwoMessages() { public void parseTwoMessages() {
String twoMessages = "{\"type\":1,\"target\":\"one\",\"arguments\":[42]}\u001E{\"type\":1,\"target\":\"two\",\"arguments\":[43]}\u001E"; String stringifiedMessage = "{\"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 })); ByteBuffer message = TestUtils.stringToByteBuffer(stringifiedMessage);
TestBinder binder = new TestBinder(new Type[] { int.class }, null);
HubMessage[] messages = jsonHubProtocol.parseMessages(twoMessages, binder); List<HubMessage> messages = jsonHubProtocol.parseMessages(message, binder);
assertEquals(2, messages.length);
assertNotNull(messages);
assertEquals(2, messages.size());
// Check the first message // 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. //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("one", invocationMessage.getTarget());
assertEquals(null, invocationMessage.getInvocationId()); assertEquals(null, invocationMessage.getInvocationId());
@ -146,10 +162,10 @@ class JsonHubProtocolTest {
assertEquals(42, messageResult); assertEquals(42, messageResult);
// Check the second message // 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. //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("two", invocationMessage2.getTarget());
assertEquals(null, invocationMessage2.getInvocationId()); assertEquals(null, invocationMessage2.getInvocationId());
@ -160,37 +176,135 @@ class JsonHubProtocolTest {
@Test @Test
public void parseSingleMessageMutipleArgs() { public void parseSingleMessageMutipleArgs() {
String stringifiedMessage = "{\"type\":1,\"target\":\"test\",\"arguments\":[42, 24]}\u001E"; 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 //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]; InvocationMessage invocationMessage = (InvocationMessage)messages.get(0);
assertEquals("test", message.getTarget()); assertEquals("test", invocationMessage.getTarget());
assertEquals(null, message.getInvocationId()); assertEquals(null, invocationMessage.getInvocationId());
int messageResult = (int) message.getArguments()[0]; int messageResult = (int) invocationMessage.getArguments()[0];
int messageResult2 = (int) message.getArguments()[1]; int messageResult2 = (int) invocationMessage.getArguments()[1];
assertEquals(42, messageResult); assertEquals(42, messageResult);
assertEquals(24, messageResult2); 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 @Test
public void parseMessageWithOutOfOrderProperties() { public void parseMessageWithOutOfOrderProperties() {
String stringifiedMessage = "{\"arguments\":[42, 24],\"type\":1,\"target\":\"test\"}\u001E"; 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 // 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]; InvocationMessage invocationMessage = (InvocationMessage) messages.get(0);
assertEquals("test", message.getTarget()); assertEquals("test", invocationMessage.getTarget());
assertEquals(null, message.getInvocationId()); assertEquals(null, invocationMessage.getInvocationId());
int messageResult = (int) message.getArguments()[0]; int messageResult = (int) invocationMessage.getArguments()[0];
int messageResult2 = (int) message.getArguments()[1]; int messageResult2 = (int) invocationMessage.getArguments()[1];
assertEquals(42, messageResult); assertEquals(42, messageResult);
assertEquals(24, messageResult2); assertEquals(24, messageResult2);
} }
@ -198,134 +312,111 @@ class JsonHubProtocolTest {
@Test @Test
public void parseCompletionMessageWithOutOfOrderProperties() { public void parseCompletionMessageWithOutOfOrderProperties() {
String stringifiedMessage = "{\"type\":3,\"result\":42,\"invocationId\":\"1\"}\u001E"; 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 // 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]; CompletionMessage completionMessage = (CompletionMessage) messages.get(0);
assertEquals(null, message.getError()); assertEquals(null, completionMessage.getError());
assertEquals(42 , message.getResult()); assertEquals(42 , completionMessage.getResult());
} }
@Test @Test
public void invocationBindingFailureWhileParsingTooManyArgumentsWithOutOfOrderProperties() { public void invocationBindingFailureWhileParsingTooManyArgumentsWithOutOfOrderProperties() {
String stringifiedMessage = "{\"arguments\":[42, 24],\"type\":1,\"target\":\"test\"}\u001E"; 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); List<HubMessage> messages = jsonHubProtocol.parseMessages(message, binder);
assertEquals(1, messages.length);
assertEquals(InvocationBindingFailureMessage.class, messages[0].getClass()); assertNotNull(messages);
InvocationBindingFailureMessage message = (InvocationBindingFailureMessage)messages[0]; assertEquals(1, messages.size());
assertEquals("Invocation provides 2 argument(s) but target expects 1.", message.getException().getMessage());
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 @Test
public void invocationBindingFailureWhileParsingTooManyArguments() { public void invocationBindingFailureWhileParsingTooManyArguments() {
String stringifiedMessage = "{\"type\":1,\"target\":\"test\",\"arguments\":[42, 24]}\u001E"; 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); List<HubMessage> messages = jsonHubProtocol.parseMessages(message, binder);
assertEquals(1, messages.length);
assertEquals(InvocationBindingFailureMessage.class, messages[0].getClass()); assertNotNull(messages);
InvocationBindingFailureMessage message = (InvocationBindingFailureMessage) messages[0]; assertEquals(1, messages.size());
assertEquals("Invocation provides 2 argument(s) but target expects 1.", message.getException().getMessage());
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 @Test
public void invocationBindingFailureWhileParsingTooFewArguments() { public void invocationBindingFailureWhileParsingTooFewArguments() {
String stringifiedMessage = "{\"type\":1,\"target\":\"test\",\"arguments\":[42]}\u001E"; 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); List<HubMessage> messages = jsonHubProtocol.parseMessages(message, binder);
assertEquals(1, messages.length);
assertEquals(InvocationBindingFailureMessage.class, messages[0].getClass()); assertNotNull(messages);
InvocationBindingFailureMessage message = (InvocationBindingFailureMessage) messages[0]; assertEquals(1, messages.size());
assertEquals("Invocation provides 1 argument(s) but target expects 2.", message.getException().getMessage());
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 @Test
public void invocationBindingFailureWhenParsingIncorrectType() { public void invocationBindingFailureWhenParsingIncorrectType() {
String stringifiedMessage = "{\"type\":1,\"target\":\"test\",\"arguments\":[\"true\"]}\u001E"; 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); List<HubMessage> messages = jsonHubProtocol.parseMessages(message, binder);
assertEquals(1, messages.length);
assertEquals(InvocationBindingFailureMessage.class, messages[0].getClass()); assertNotNull(messages);
InvocationBindingFailureMessage message = (InvocationBindingFailureMessage) messages[0]; assertEquals(1, messages.size());
assertEquals("java.lang.NumberFormatException: For input string: \"true\"", message.getException().getMessage());
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 @Test
public void invocationBindingFailureStillReadsJsonPayloadAfterFailure() { public void invocationBindingFailureStillReadsJsonPayloadAfterFailure() {
String stringifiedMessage = "{\"type\":1,\"target\":\"test\",\"arguments\":[\"true\"],\"invocationId\":\"123\"}\u001E"; 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); List<HubMessage> messages = jsonHubProtocol.parseMessages(message, binder);
assertEquals(1, messages.length);
assertEquals(InvocationBindingFailureMessage.class, messages[0].getClass()); assertNotNull(messages);
InvocationBindingFailureMessage message = (InvocationBindingFailureMessage) messages[0]; assertEquals(1, messages.size());
assertEquals("java.lang.NumberFormatException: For input string: \"true\"", message.getException().getMessage());
assertEquals("123", message.getInvocationId()); 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 @Test
public void errorWhileParsingIncompleteMessage() { public void errorWhileParsingIncompleteMessage() {
String stringifiedMessage = "{\"type\":1,\"target\":\"test\",\"arguments\":"; 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, RuntimeException exception = assertThrows(RuntimeException.class,
() -> jsonHubProtocol.parseMessages(stringifiedMessage, binder)); () -> jsonHubProtocol.parseMessages(message, binder));
assertEquals("Message is incomplete.", exception.getMessage()); 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 static org.junit.jupiter.api.Assertions.*;
import java.nio.ByteBuffer;
import java.util.HashMap; import java.util.HashMap;
import java.util.Map; import java.util.Map;
import java.util.concurrent.TimeUnit; import java.util.concurrent.TimeUnit;
@ -22,7 +23,7 @@ public class LongPollingTransportTest {
@Test @Test
public void LongPollingFailsToConnectWith404Response() { public void LongPollingFailsToConnectWith404Response() {
TestHttpClient client = new TestHttpClient() 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<>(); Map<String, String> headers = new HashMap<>();
LongPollingTransport transport = new LongPollingTransport(headers, client, Single.just("")); LongPollingTransport transport = new LongPollingTransport(headers, client, Single.just(""));
@ -35,11 +36,12 @@ public class LongPollingTransportTest {
@Test @Test
public void LongPollingTransportCantSendBeforeStart() { public void LongPollingTransportCantSendBeforeStart() {
TestHttpClient client = new TestHttpClient() 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<>(); Map<String, String> headers = new HashMap<>();
LongPollingTransport transport = new LongPollingTransport(headers, client, Single.just("")); 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(Exception.class, exception.getCause().getClass());
assertEquals("Cannot send unless the transport is active.", exception.getCause().getMessage()); assertEquals("Cannot send unless the transport is active.", exception.getCause().getMessage());
assertFalse(transport.isActive()); assertFalse(transport.isActive());
@ -53,9 +55,9 @@ public class LongPollingTransportTest {
.on("GET", (req) -> { .on("GET", (req) -> {
if (firstPoll.get()) { if (firstPoll.get()) {
firstPoll.set(false); 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<>(); Map<String, String> headers = new HashMap<>();
@ -81,9 +83,9 @@ public class LongPollingTransportTest {
.on("GET", (req) -> { .on("GET", (req) -> {
if (firstPoll.get()) { if (firstPoll.get()) {
firstPoll.set(false); 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<>(); Map<String, String> headers = new HashMap<>();
@ -104,7 +106,7 @@ public class LongPollingTransportTest {
@Test @Test
public void CanSetAndTriggerOnReceive() { public void CanSetAndTriggerOnReceive() {
TestHttpClient client = new TestHttpClient() 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<>(); Map<String, String> headers = new HashMap<>();
LongPollingTransport transport = new LongPollingTransport(headers, client, Single.just("")); LongPollingTransport transport = new LongPollingTransport(headers, client, Single.just(""));
@ -112,12 +114,13 @@ public class LongPollingTransportTest {
AtomicBoolean onReceivedRan = new AtomicBoolean(false); AtomicBoolean onReceivedRan = new AtomicBoolean(false);
transport.setOnReceive((message) -> { transport.setOnReceive((message) -> {
onReceivedRan.set(true); 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 // The transport doesn't need to be active to trigger onReceive for the case
// when we are handling the last outstanding poll. // when we are handling the last outstanding poll.
transport.onReceive("TEST"); ByteBuffer onReceiveBuffer = TestUtils.stringToByteBuffer("TEST");
transport.onReceive(onReceiveBuffer);
assertTrue(onReceivedRan.get()); assertTrue(onReceivedRan.get());
} }
@ -129,13 +132,13 @@ public class LongPollingTransportTest {
.on("GET", (req) -> { .on("GET", (req) -> {
if (requestCount.get() == 0) { if (requestCount.get() == 0) {
requestCount.incrementAndGet(); requestCount.incrementAndGet();
return Single.just(new HttpResponse(200, "", "")); return Single.just(new HttpResponse(200, "", TestUtils.emptyByteBuffer));
} else if (requestCount.get() == 1) { } else if (requestCount.get() == 1) {
requestCount.incrementAndGet(); 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<>(); Map<String, String> headers = new HashMap<>();
@ -145,7 +148,7 @@ public class LongPollingTransportTest {
AtomicReference<String> message = new AtomicReference<>(); AtomicReference<String> message = new AtomicReference<>();
transport.setOnReceive((msg -> { transport.setOnReceive((msg -> {
onReceiveCalled.set(true); onReceiveCalled.set(true);
message.set(msg); message.set(TestUtils.byteBufferToString(msg));
block.onComplete(); block.onComplete();
}) ); }) );
@ -165,19 +168,19 @@ public class LongPollingTransportTest {
.on("GET", (req) -> { .on("GET", (req) -> {
if (requestCount.get() == 0) { if (requestCount.get() == 0) {
requestCount.incrementAndGet(); requestCount.incrementAndGet();
return Single.just(new HttpResponse(200, "", "")); return Single.just(new HttpResponse(200, "", TestUtils.emptyByteBuffer));
} else if (requestCount.get() == 1) { } else if (requestCount.get() == 1) {
requestCount.incrementAndGet(); requestCount.incrementAndGet();
return Single.just(new HttpResponse(200, "", "FIRST")); return Single.just(new HttpResponse(200, "", TestUtils.stringToByteBuffer("FIRST")));
} else if (requestCount.get() == 2) { } else if (requestCount.get() == 2) {
requestCount.incrementAndGet(); requestCount.incrementAndGet();
return Single.just(new HttpResponse(200, "", "SECOND")); return Single.just(new HttpResponse(200, "", TestUtils.stringToByteBuffer("SECOND")));
} else if (requestCount.get() == 3) { } else if (requestCount.get() == 3) {
requestCount.incrementAndGet(); 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<>(); Map<String, String> headers = new HashMap<>();
@ -188,7 +191,7 @@ public class LongPollingTransportTest {
AtomicInteger messageCount = new AtomicInteger(); AtomicInteger messageCount = new AtomicInteger();
transport.setOnReceive((msg) -> { transport.setOnReceive((msg) -> {
onReceiveCalled.set(true); onReceiveCalled.set(true);
message.set(message.get() + msg); message.set(message.get() + TestUtils.byteBufferToString(msg));
if (messageCount.incrementAndGet() == 3) { if (messageCount.incrementAndGet() == 3) {
blocker.onComplete(); blocker.onComplete();
} }
@ -211,14 +214,14 @@ public class LongPollingTransportTest {
.on("GET", (req) -> { .on("GET", (req) -> {
if (requestCount.get() == 0) { if (requestCount.get() == 0) {
requestCount.incrementAndGet(); requestCount.incrementAndGet();
return Single.just(new HttpResponse(200, "", "")); return Single.just(new HttpResponse(200, "", TestUtils.emptyByteBuffer));
} }
assertTrue(close.blockingAwait(1, TimeUnit.SECONDS)); assertTrue(close.blockingAwait(1, TimeUnit.SECONDS));
return Single.just(new HttpResponse(204, "", "")); return Single.just(new HttpResponse(204, "", TestUtils.emptyByteBuffer));
}).on("POST", (req) -> { }).on("POST", (req) -> {
assertFalse(req.getHeaders().isEmpty()); assertFalse(req.getHeaders().isEmpty());
headerValue.set(req.getHeaders().get("KEY")); 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<>(); Map<String, String> headers = new HashMap<>();
@ -227,7 +230,8 @@ public class LongPollingTransportTest {
transport.setOnClose((error) -> {}); transport.setOnClose((error) -> {});
transport.start("http://example.com").timeout(1, TimeUnit.SECONDS).blockingAwait(); 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(); close.onComplete();
assertEquals(headerValue.get(), "VALUE"); assertEquals(headerValue.get(), "VALUE");
} }
@ -241,15 +245,15 @@ public class LongPollingTransportTest {
.on("GET", (req) -> { .on("GET", (req) -> {
if (requestCount.get() == 0) { if (requestCount.get() == 0) {
requestCount.incrementAndGet(); requestCount.incrementAndGet();
return Single.just(new HttpResponse(200, "", "")); return Single.just(new HttpResponse(200, "", TestUtils.emptyByteBuffer));
} }
assertTrue(close.blockingAwait(1, TimeUnit.SECONDS)); assertTrue(close.blockingAwait(1, TimeUnit.SECONDS));
return Single.just(new HttpResponse(204, "", "")); return Single.just(new HttpResponse(204, "", TestUtils.emptyByteBuffer));
}) })
.on("POST", (req) -> { .on("POST", (req) -> {
assertFalse(req.getHeaders().isEmpty()); assertFalse(req.getHeaders().isEmpty());
headerValue.set(req.getHeaders().get("Authorization")); 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<>(); Map<String, String> headers = new HashMap<>();
@ -258,7 +262,8 @@ public class LongPollingTransportTest {
transport.setOnClose((error) -> {}); transport.setOnClose((error) -> {});
transport.start("http://example.com").timeout(1, TimeUnit.SECONDS).blockingAwait(); 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"); assertEquals(headerValue.get(), "Bearer TOKEN");
close.onComplete(); close.onComplete();
} }
@ -273,17 +278,17 @@ public class LongPollingTransportTest {
.on("GET", (req) -> { .on("GET", (req) -> {
if (requestCount.get() == 0) { if (requestCount.get() == 0) {
requestCount.incrementAndGet(); requestCount.incrementAndGet();
return Single.just(new HttpResponse(200, "", "")); return Single.just(new HttpResponse(200, "", TestUtils.emptyByteBuffer));
} }
assertEquals("Bearer TOKEN1", req.getHeaders().get("Authorization")); assertEquals("Bearer TOKEN1", req.getHeaders().get("Authorization"));
secondGet.onComplete(); secondGet.onComplete();
assertTrue(close.blockingAwait(1, TimeUnit.SECONDS)); assertTrue(close.blockingAwait(1, TimeUnit.SECONDS));
return Single.just(new HttpResponse(204, "", "")); return Single.just(new HttpResponse(204, "", TestUtils.emptyByteBuffer));
}) })
.on("POST", (req) -> { .on("POST", (req) -> {
assertFalse(req.getHeaders().isEmpty()); assertFalse(req.getHeaders().isEmpty());
headerValue.set(req.getHeaders().get("Authorization")); 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); AtomicInteger i = new AtomicInteger(0);
@ -294,7 +299,8 @@ public class LongPollingTransportTest {
transport.start("http://example.com").timeout(1, TimeUnit.SECONDS).blockingAwait(); transport.start("http://example.com").timeout(1, TimeUnit.SECONDS).blockingAwait();
secondGet.blockingAwait(1, TimeUnit.SECONDS); 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()); assertEquals("Bearer TOKEN2", headerValue.get());
close.onComplete(); close.onComplete();
} }
@ -307,9 +313,9 @@ public class LongPollingTransportTest {
.on("GET", (req) -> { .on("GET", (req) -> {
if (firstPoll.get()) { if (firstPoll.get()) {
firstPoll.set(false); 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<>(); Map<String, String> headers = new HashMap<>();
@ -341,16 +347,16 @@ public class LongPollingTransportTest {
.on("GET", (req) -> { .on("GET", (req) -> {
if (firstPoll.get()) { if (firstPoll.get()) {
firstPoll.set(false); firstPoll.set(false);
return Single.just(new HttpResponse(200, "", "")); return Single.just(new HttpResponse(200, "", TestUtils.emptyByteBuffer));
} else { } else {
assertTrue(block.blockingAwait(1, TimeUnit.SECONDS)); assertTrue(block.blockingAwait(1, TimeUnit.SECONDS));
return Single.just(new HttpResponse(204, "", "")); return Single.just(new HttpResponse(204, "", TestUtils.emptyByteBuffer));
} }
}) })
.on("DELETE", (req) ->{ .on("DELETE", (req) ->{
//Unblock the last poll when we sent the DELETE request. //Unblock the last poll when we sent the DELETE request.
block.onComplete(); block.onComplete();
return Single.just(new HttpResponse(200, "", "")); return Single.just(new HttpResponse(200, "", TestUtils.emptyByteBuffer));
}); });
Map<String, String> headers = new HashMap<>(); 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; package com.microsoft.signalr;
import java.nio.ByteBuffer;
import java.util.ArrayList; import java.util.ArrayList;
import io.reactivex.Completable; import io.reactivex.Completable;
@ -11,14 +12,14 @@ import io.reactivex.subjects.SingleSubject;
class MockTransport implements Transport { class MockTransport implements Transport {
private OnReceiveCallBack onReceiveCallBack; private OnReceiveCallBack onReceiveCallBack;
private ArrayList<String> sentMessages = new ArrayList<>(); private ArrayList<ByteBuffer> sentMessages = new ArrayList<>();
private String url; private String url;
private TransportOnClosedCallback onClose; private TransportOnClosedCallback onClose;
final private boolean ignorePings; final private boolean ignorePings;
final private boolean autoHandshake; final private boolean autoHandshake;
final private CompletableSubject startSubject = CompletableSubject.create(); final private CompletableSubject startSubject = CompletableSubject.create();
final private CompletableSubject stopSubject = 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"; private static final String RECORD_SEPARATOR = "\u001e";
@ -40,7 +41,7 @@ class MockTransport implements Transport {
this.url = url; this.url = url;
if (autoHandshake) { if (autoHandshake) {
try { try {
onReceiveCallBack.invoke("{}" + RECORD_SEPARATOR); onReceiveCallBack.invoke(TestUtils.stringToByteBuffer("{}" + RECORD_SEPARATOR));
} catch (Exception e) { } catch (Exception e) {
throw new RuntimeException(e); throw new RuntimeException(e);
} }
@ -50,8 +51,8 @@ class MockTransport implements Transport {
} }
@Override @Override
public Completable send(String message) { public Completable send(ByteBuffer message) {
if (!(ignorePings && message.equals("{\"type\":6}" + RECORD_SEPARATOR))) { if (!(ignorePings && isPing(message))) {
sentMessages.add(message); sentMessages.add(message);
sendSubject.onSuccess(message); sendSubject.onSuccess(message);
sendSubject = SingleSubject.create(); sendSubject = SingleSubject.create();
@ -65,7 +66,7 @@ class MockTransport implements Transport {
} }
@Override @Override
public void onReceive(String message) { public void onReceive(ByteBuffer message) {
this.onReceiveCallBack.invoke(message); this.onReceiveCallBack.invoke(message);
} }
@ -86,14 +87,18 @@ class MockTransport implements Transport {
} }
public void receiveMessage(String message) { public void receiveMessage(String message) {
this.onReceive(TestUtils.stringToByteBuffer(message));
}
public void receiveMessage(ByteBuffer message) {
this.onReceive(message); this.onReceive(message);
} }
public String[] getSentMessages() { public ByteBuffer[] getSentMessages() {
return sentMessages.toArray(new String[sentMessages.size()]); return sentMessages.toArray(new ByteBuffer[sentMessages.size()]);
} }
public SingleSubject<String> getNextSentMessage() { public SingleSubject<ByteBuffer> getNextSentMessage() {
return sendSubject; return sendSubject;
} }
@ -108,4 +113,9 @@ class MockTransport implements Transport {
public Completable getStopTask() { public Completable getStopTask() {
return stopSubject; 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; package com.microsoft.signalr;
import java.nio.ByteBuffer;
import java.util.ArrayList; import java.util.ArrayList;
import java.util.List; import java.util.List;
import java.util.Map; import java.util.Map;
@ -27,7 +28,7 @@ class TestHttpClient extends HttpClient {
} }
@Override @Override
public Single<HttpResponse> send(HttpRequest request, String body) { public Single<HttpResponse> send(HttpRequest request, ByteBuffer body) {
this.sentRequests.add(request); this.sentRequests.add(request);
return this.handler.invoke(request); return this.handler.invoke(request);
} }

View File

@ -3,21 +3,47 @@
package com.microsoft.signalr; package com.microsoft.signalr;
import java.nio.ByteBuffer;
import java.nio.charset.StandardCharsets;
class TestUtils { class TestUtils {
static ByteBuffer emptyByteBuffer = stringToByteBuffer("");
static HubConnection createHubConnection(String url) { 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) { 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, Transport transport, boolean skipNegotiate, HttpClient client) { 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, boolean withMessagePack) {
HttpHubConnectionBuilder builder = HubConnectionBuilder.create(url) HttpHubConnectionBuilder builder = HubConnectionBuilder.create(url)
.withTransportImplementation(transport) .withTransportImplementation(transport)
.withHttpClient(client) .withHttpClient(client)
.shouldSkipNegotiate(skipNegotiate); .shouldSkipNegotiate(skipNegotiate);
if (withMessagePack) {
builder = builder.withMessagePackHubProtocol();
}
return builder.build(); 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 static org.junit.jupiter.api.Assertions.*;
import java.nio.ByteBuffer;
import java.util.HashMap; import java.util.HashMap;
import java.util.Map; import java.util.Map;
import java.util.concurrent.TimeUnit; import java.util.concurrent.TimeUnit;
@ -35,7 +36,7 @@ class WebSocketTransportTest {
} }
@Override @Override
public Single<HttpResponse> send(HttpRequest request, String body) { public Single<HttpResponse> send(HttpRequest request, ByteBuffer body) {
return null; return null;
} }
@ -71,7 +72,7 @@ class WebSocketTransportTest {
} }
@Override @Override
public Completable send(String message) { public Completable send(ByteBuffer message) {
return null; return null;
} }