diff --git a/src/SignalR/clients/java/signalr/build.gradle b/src/SignalR/clients/java/signalr/build.gradle index b845d839497d..d55927441423 100644 --- a/src/SignalR/clients/java/signalr/build.gradle +++ b/src/SignalR/clients/java/signalr/build.gradle @@ -39,6 +39,8 @@ dependencies { implementation 'com.squareup.okhttp3:okhttp:3.11.0' api 'io.reactivex.rxjava2:rxjava:2.2.3' implementation 'org.slf4j:slf4j-api:1.7.25' + compile 'org.msgpack:msgpack-core:0.8.20' + compile 'org.msgpack:jackson-dataformat-msgpack:0.8.20' } spotless { diff --git a/src/SignalR/clients/java/signalr/src/main/java/com/microsoft/signalr/CallbackMap.java b/src/SignalR/clients/java/signalr/src/main/java/com/microsoft/signalr/CallbackMap.java index a6298aed52cf..5bb317cd1fb5 100644 --- a/src/SignalR/clients/java/signalr/src/main/java/com/microsoft/signalr/CallbackMap.java +++ b/src/SignalR/clients/java/signalr/src/main/java/com/microsoft/signalr/CallbackMap.java @@ -3,6 +3,7 @@ package com.microsoft.signalr; +import java.lang.reflect.Type; import java.util.ArrayList; import java.util.HashMap; import java.util.List; @@ -13,10 +14,10 @@ class CallbackMap { private final Map> handlers = new HashMap<>(); private final ReentrantLock lock = new ReentrantLock(); - public InvocationHandler put(String target, ActionBase action, Class... classes) { + public InvocationHandler put(String target, ActionBase action, Type... types) { try { lock.lock(); - InvocationHandler handler = new InvocationHandler(action, classes); + InvocationHandler handler = new InvocationHandler(action, types); if (!handlers.containsKey(target)) { handlers.put(target, new ArrayList<>()); } diff --git a/src/SignalR/clients/java/signalr/src/main/java/com/microsoft/signalr/CancelInvocationMessage.java b/src/SignalR/clients/java/signalr/src/main/java/com/microsoft/signalr/CancelInvocationMessage.java index 096c49faf025..9f375aea72c2 100644 --- a/src/SignalR/clients/java/signalr/src/main/java/com/microsoft/signalr/CancelInvocationMessage.java +++ b/src/SignalR/clients/java/signalr/src/main/java/com/microsoft/signalr/CancelInvocationMessage.java @@ -3,14 +3,28 @@ package com.microsoft.signalr; +import java.util.Map; + final class CancelInvocationMessage extends HubMessage { private final int type = HubMessageType.CANCEL_INVOCATION.value; + private Map headers; private final String invocationId; - - public CancelInvocationMessage(String invocationId) { + + public CancelInvocationMessage(Map headers, String invocationId) { + if (headers != null && !headers.isEmpty()) { + this.headers = headers; + } this.invocationId = invocationId; } + public Map getHeaders() { + return headers; + } + + public String getInvocationId() { + return invocationId; + } + @Override public HubMessageType getMessageType() { return HubMessageType.CANCEL_INVOCATION; diff --git a/src/SignalR/clients/java/signalr/src/main/java/com/microsoft/signalr/CloseMessage.java b/src/SignalR/clients/java/signalr/src/main/java/com/microsoft/signalr/CloseMessage.java index 2c0dd006442f..1d896950579c 100644 --- a/src/SignalR/clients/java/signalr/src/main/java/com/microsoft/signalr/CloseMessage.java +++ b/src/SignalR/clients/java/signalr/src/main/java/com/microsoft/signalr/CloseMessage.java @@ -5,6 +5,7 @@ final class CloseMessage extends HubMessage { private final String error; + private final boolean allowReconnect; @Override public HubMessageType getMessageType() { @@ -12,14 +13,27 @@ public HubMessageType getMessageType() { } public CloseMessage() { - this(null); + this(null, false); } public CloseMessage(String error) { + this(error, false); + } + + public CloseMessage(boolean allowReconnect) { + this(null, allowReconnect); + } + + public CloseMessage(String error, boolean allowReconnect) { this.error = error; + this.allowReconnect = allowReconnect; } public String getError() { return this.error; } + + public boolean getAllowReconnect() { + return this.allowReconnect; + } } diff --git a/src/SignalR/clients/java/signalr/src/main/java/com/microsoft/signalr/CompletionMessage.java b/src/SignalR/clients/java/signalr/src/main/java/com/microsoft/signalr/CompletionMessage.java index 4cd5f68263ae..7256c7dfd4de 100644 --- a/src/SignalR/clients/java/signalr/src/main/java/com/microsoft/signalr/CompletionMessage.java +++ b/src/SignalR/clients/java/signalr/src/main/java/com/microsoft/signalr/CompletionMessage.java @@ -3,13 +3,19 @@ package com.microsoft.signalr; +import java.util.Map; + final class CompletionMessage extends HubMessage { private final int type = HubMessageType.COMPLETION.value; + private Map headers; private final String invocationId; private final Object result; private final String error; - - public CompletionMessage(String invocationId, Object result, String error) { + + public CompletionMessage(Map headers, String invocationId, Object result, String error) { + if (headers != null && !headers.isEmpty()) { + this.headers = headers; + } if (error != null && result != null) { throw new IllegalArgumentException("Expected either 'error' or 'result' to be provided, but not both."); } @@ -17,6 +23,10 @@ public CompletionMessage(String invocationId, Object result, String error) { this.result = result; this.error = error; } + + public Map getHeaders() { + return headers; + } public Object getResult() { return result; diff --git a/src/SignalR/clients/java/signalr/src/main/java/com/microsoft/signalr/DefaultHttpClient.java b/src/SignalR/clients/java/signalr/src/main/java/com/microsoft/signalr/DefaultHttpClient.java index 8f4f4f0df91d..f8ddc42c6b8e 100644 --- a/src/SignalR/clients/java/signalr/src/main/java/com/microsoft/signalr/DefaultHttpClient.java +++ b/src/SignalR/clients/java/signalr/src/main/java/com/microsoft/signalr/DefaultHttpClient.java @@ -4,6 +4,7 @@ package com.microsoft.signalr; import java.io.IOException; +import java.nio.ByteBuffer; import java.util.ArrayList; import java.util.Collection; import java.util.List; @@ -15,6 +16,7 @@ import io.reactivex.Single; import io.reactivex.subjects.SingleSubject; import okhttp3.*; +import okio.ByteString; final class DefaultHttpClient extends HttpClient { private OkHttpClient client = null; @@ -104,7 +106,7 @@ public Single send(HttpRequest httpRequest) { } @Override - public Single send(HttpRequest httpRequest, String bodyContent) { + public Single send(HttpRequest httpRequest, ByteBuffer bodyContent) { Request.Builder requestBuilder = new Request.Builder().url(httpRequest.getUrl()); switch (httpRequest.getMethod()) { @@ -114,7 +116,7 @@ public Single send(HttpRequest httpRequest, String bodyContent) { case "POST": RequestBody body; if (bodyContent != null) { - body = RequestBody.create(MediaType.parse("text/plain"), bodyContent); + body = RequestBody.create(MediaType.parse("text/plain"), ByteString.of(bodyContent)); } else { body = RequestBody.create(null, new byte[]{}); } @@ -150,7 +152,7 @@ public void onFailure(Call call, IOException e) { @Override public void onResponse(Call call, Response response) throws IOException { try (ResponseBody body = response.body()) { - HttpResponse httpResponse = new HttpResponse(response.code(), response.message(), body.string()); + HttpResponse httpResponse = new HttpResponse(response.code(), response.message(), ByteBuffer.wrap(body.bytes())); responseSubject.onSuccess(httpResponse); } } diff --git a/src/SignalR/clients/java/signalr/src/main/java/com/microsoft/signalr/HandshakeProtocol.java b/src/SignalR/clients/java/signalr/src/main/java/com/microsoft/signalr/HandshakeProtocol.java index 4c1d2ad8962b..c7df51fd0337 100644 --- a/src/SignalR/clients/java/signalr/src/main/java/com/microsoft/signalr/HandshakeProtocol.java +++ b/src/SignalR/clients/java/signalr/src/main/java/com/microsoft/signalr/HandshakeProtocol.java @@ -3,15 +3,18 @@ package com.microsoft.signalr; +import java.nio.charset.StandardCharsets; +import java.nio.ByteBuffer; + import com.google.gson.Gson; final class HandshakeProtocol { private static final Gson gson = new Gson(); private static final String RECORD_SEPARATOR = "\u001e"; - public static String createHandshakeRequestMessage(HandshakeRequestMessage message) { + public static ByteBuffer createHandshakeRequestMessage(HandshakeRequestMessage message) { // The handshake request is always in the JSON format - return gson.toJson(message) + RECORD_SEPARATOR; + return ByteBuffer.wrap((gson.toJson(message) + RECORD_SEPARATOR).getBytes(StandardCharsets.UTF_8)); } public static HandshakeResponseMessage parseHandshakeResponse(String message) { diff --git a/src/SignalR/clients/java/signalr/src/main/java/com/microsoft/signalr/HttpClient.java b/src/SignalR/clients/java/signalr/src/main/java/com/microsoft/signalr/HttpClient.java index 97efaa980486..9a5ecf352be5 100644 --- a/src/SignalR/clients/java/signalr/src/main/java/com/microsoft/signalr/HttpClient.java +++ b/src/SignalR/clients/java/signalr/src/main/java/com/microsoft/signalr/HttpClient.java @@ -3,6 +3,7 @@ package com.microsoft.signalr; +import java.nio.ByteBuffer; import java.util.HashMap; import java.util.Map; @@ -45,23 +46,23 @@ public Map getHeaders() { class HttpResponse { private final int statusCode; private final String statusText; - private final String content; + private final ByteBuffer content; public HttpResponse(int statusCode) { this(statusCode, ""); } public HttpResponse(int statusCode, String statusText) { - this(statusCode, statusText, ""); + this(statusCode, statusText, ByteBuffer.wrap(new byte[] {})); } - public HttpResponse(int statusCode, String statusText, String content) { + public HttpResponse(int statusCode, String statusText, ByteBuffer content) { this.statusCode = statusCode; this.statusText = statusText; this.content = content; } - public String getContent() { + public ByteBuffer getContent() { return content; } @@ -95,7 +96,7 @@ public Single post(String url) { return this.send(request); } - public Single post(String url, String body, HttpRequest options) { + public Single post(String url, ByteBuffer body, HttpRequest options) { options.setUrl(url); options.setMethod("POST"); return this.send(options, body); @@ -122,7 +123,7 @@ public Single delete(String url, HttpRequest options) { public abstract Single send(HttpRequest request); - public abstract Single send(HttpRequest request, String body); + public abstract Single send(HttpRequest request, ByteBuffer body); public abstract WebSocketWrapper createWebSocket(String url, Map headers); diff --git a/src/SignalR/clients/java/signalr/src/main/java/com/microsoft/signalr/HttpHubConnectionBuilder.java b/src/SignalR/clients/java/signalr/src/main/java/com/microsoft/signalr/HttpHubConnectionBuilder.java index e1f38e6888a4..ce29e1a2cecf 100644 --- a/src/SignalR/clients/java/signalr/src/main/java/com/microsoft/signalr/HttpHubConnectionBuilder.java +++ b/src/SignalR/clients/java/signalr/src/main/java/com/microsoft/signalr/HttpHubConnectionBuilder.java @@ -16,6 +16,7 @@ public class HttpHubConnectionBuilder { private final String url; private Transport transport; private HttpClient httpClient; + private HubProtocol protocol = new JsonHubProtocol(); private boolean skipNegotiate; private Single accessTokenProvider; private long handshakeResponseTimeout = 0; @@ -54,6 +55,16 @@ HttpHubConnectionBuilder withHttpClient(HttpClient httpClient) { this.httpClient = httpClient; return this; } + + /** + * Sets MessagePack as the {@link HubProtocol} to be used by the {@link HubConnection}. + * + * @return This instance of the HttpHubConnectionBuilder. + */ + public HttpHubConnectionBuilder withMessagePackHubProtocol() { + this.protocol = new MessagePackHubProtocol(); + return this; + } /** * Indicates to the {@link HubConnection} that it should skip the negotiate process. @@ -133,7 +144,7 @@ public HttpHubConnectionBuilder setHttpClientBuilderCallback(Action1> emptyArray = new ArrayList<>(); + private static final byte RECORD_SEPARATOR = 0x1e; + private static final List emptyArray = new ArrayList<>(); private static final int MAX_NEGOTIATE_ATTEMPTS = 100; private String baseUrl; @@ -126,7 +130,7 @@ Transport getTransport() { return transport; } - HubConnection(String url, Transport transport, boolean skipNegotiate, HttpClient httpClient, + HubConnection(String url, Transport transport, boolean skipNegotiate, HttpClient httpClient, HubProtocol protocol, Single accessTokenProvider, long handshakeResponseTimeout, Map headers, TransportEnum transportEnum, Action1 configureBuilder) { if (url == null || url.isEmpty()) { @@ -134,7 +138,7 @@ Transport getTransport() { } this.baseUrl = url; - this.protocol = new JsonHubProtocol(); + this.protocol = protocol; if (accessTokenProvider != null) { this.accessTokenProvider = accessTokenProvider; @@ -165,8 +169,20 @@ Transport getTransport() { this.callback = (payload) -> { resetServerTimeout(); if (!handshakeReceived) { - int handshakeLength = payload.indexOf(RECORD_SEPARATOR) + 1; - String handshakeResponseString = payload.substring(0, handshakeLength - 1); + List handshakeByteList = new ArrayList(); + byte curr = payload.get(); + // Add the handshake to handshakeBytes, but not the record separator + while (curr != RECORD_SEPARATOR) { + handshakeByteList.add(curr); + curr = payload.get(); + } + int handshakeLength = handshakeByteList.size() + 1; + byte[] handshakeBytes = new byte[handshakeLength - 1]; + for (int i = 0; i < handshakeLength - 1; i++) { + handshakeBytes[i] = handshakeByteList.get(i); + } + // The handshake will always be a UTF8 Json string + String handshakeResponseString = new String(handshakeBytes, StandardCharsets.UTF_8); HandshakeResponseMessage handshakeResponse; try { handshakeResponse = HandshakeProtocol.parseHandshakeResponse(handshakeResponseString); @@ -185,14 +201,13 @@ Transport getTransport() { handshakeReceived = true; handshakeResponseSubject.onComplete(); - payload = payload.substring(handshakeLength); // The payload only contained the handshake response so we can return. - if (payload.length() == 0) { + if (!payload.hasRemaining()) { return; } } - - HubMessage[] messages = protocol.parseMessages(payload, connectionState); + + List messages = protocol.parseMessages(payload, connectionState); for (HubMessage message : messages) { logger.debug("Received message of type {}.", message.getMessageType()); @@ -271,7 +286,7 @@ private Single handleNegotiate(String url) { throw new RuntimeException(String.format("Unexpected status code returned from negotiate: %d %s.", response.getStatusCode(), response.getStatusText())); } - JsonReader reader = new JsonReader(new StringReader(response.getContent())); + JsonReader reader = new JsonReader(new StringReader(new String(response.getContent().array(), StandardCharsets.UTF_8))); NegotiateResponse negotiateResponse = new NegotiateResponse(reader); if (negotiateResponse.getError() != null) { @@ -372,7 +387,7 @@ public Completable start() { transport.setOnClose((message) -> stopConnection(message)); return transport.start(negotiateResponse.getFinalUrl()).andThen(Completable.defer(() -> { - String handshake = HandshakeProtocol.createHandshakeRequestMessage( + ByteBuffer handshake = HandshakeProtocol.createHandshakeRequestMessage( new HandshakeRequestMessage(protocol.getName(), protocol.getVersion())); connectionState = new ConnectionState(this); @@ -585,9 +600,9 @@ private void sendInvocationMessage(String method, Object[] args, String id, Bool args = checkUploadStream(args, streamIds); InvocationMessage invocationMessage; if (isStreamInvocation) { - invocationMessage = new StreamInvocationMessage(id, method, args, streamIds); + invocationMessage = new StreamInvocationMessage(null, id, method, args, streamIds); } else { - invocationMessage = new InvocationMessage(id, method, args, streamIds); + invocationMessage = new InvocationMessage(null, id, method, args, streamIds); } sendHubMessage(invocationMessage); @@ -602,13 +617,13 @@ void launchStreams(List streamIds) { for (String streamId: streamIds) { Observable observable = this.streamMap.get(streamId); observable.subscribe( - (item) -> sendHubMessage(new StreamItem(streamId, item)), + (item) -> sendHubMessage(new StreamItem(null, streamId, item)), (error) -> { - sendHubMessage(new CompletionMessage(streamId, null, error.toString())); + sendHubMessage(new CompletionMessage(null, streamId, null, error.toString())); this.streamMap.remove(streamId); }, () -> { - sendHubMessage(new CompletionMessage(streamId, null, null)); + sendHubMessage(new CompletionMessage(null, streamId, null, null)); this.streamMap.remove(streamId); }); } @@ -678,8 +693,26 @@ public Completable invoke(String method, Object... args) { * @param The expected return type. * @return A Single that yields the return value when the invocation has completed. */ - @SuppressWarnings("unchecked") public Single invoke(Class returnType, String method, Object... args) { + return this.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 The expected return type. + * @return A Single that yields the return value when the invocation has completed. + */ + public Single invoke(Type returnType, String method, Object... args) { + Class returnClass = Utils.typeToClass(returnType); + return this.invoke(returnType, returnClass, method, args); + } + + @SuppressWarnings("unchecked") + private Single invoke(Type returnType, Class returnClass, String method, Object... args) { hubConnectionStateLock.lock(); try { if (hubConnectionState != HubConnectionState.CONNECTED) { @@ -697,10 +730,10 @@ public Single invoke(Class returnType, String method, Object... args) Subject pendingCall = irq.getPendingCall(); pendingCall.subscribe(result -> { // Primitive types can't be cast with the Class cast function - if (returnType.isPrimitive()) { + if (returnClass.isPrimitive()) { subject.onSuccess((T)result); } else { - subject.onSuccess(returnType.cast(result)); + subject.onSuccess((T)returnClass.cast(result)); } }, error -> subject.onError(error)); @@ -722,8 +755,26 @@ public Single invoke(Class returnType, String method, Object... args) * @param The expected return type. * @return An observable that yields the streaming results from the server. */ - @SuppressWarnings("unchecked") public Observable stream(Class returnType, String method, Object ... args) { + return this.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 The expected return type. + * @return An observable that yields the streaming results from the server. + */ + public Observable stream(Type returnType, String method, Object ... args) { + Class returnClass = Utils.typeToClass(returnType); + return this.stream(returnType, returnClass, method, args); + } + + @SuppressWarnings("unchecked") + private Observable stream(Type returnType, Class returnClass, String method, Object ... args) { String invocationId; InvocationRequest irq; hubConnectionStateLock.lock(); @@ -741,10 +792,10 @@ public Observable stream(Class returnType, String method, Object ... a Subject pendingCall = irq.getPendingCall(); pendingCall.subscribe(result -> { // Primitive types can't be cast with the Class cast function - if (returnType.isPrimitive()) { + if (returnClass.isPrimitive()) { subject.onNext((T)result); } else { - subject.onNext(returnType.cast(result)); + subject.onNext((T)returnClass.cast(result)); } }, error -> subject.onError(error), () -> subject.onComplete()); @@ -753,7 +804,7 @@ public Observable stream(Class returnType, String method, Object ... a sendInvocationMessage(method, args, invocationId, true); return observable.doOnDispose(() -> { if (subscriptionCount.decrementAndGet() == 0) { - CancelInvocationMessage cancelInvocationMessage = new CancelInvocationMessage(invocationId); + CancelInvocationMessage cancelInvocationMessage = new CancelInvocationMessage(null, invocationId); sendHubMessage(cancelInvocationMessage); if (connectionState != null) { connectionState.tryRemoveInvocation(invocationId); @@ -767,7 +818,7 @@ public Observable stream(Class returnType, String method, Object ... a } private void sendHubMessage(HubMessage message) { - String serializedMessage = protocol.writeMessage(message); + ByteBuffer serializedMessage = protocol.writeMessage(message); if (message.getMessageType() == HubMessageType.INVOCATION ) { logger.debug("Sending {} message '{}'.", message.getMessageType().name(), ((InvocationMessage)message).getInvocationId()); } else if (message.getMessageType() == HubMessageType.STREAM_INVOCATION) { @@ -825,6 +876,7 @@ public Subscription on(String target, Action callback) { /** * Registers a handler that will be invoked when the hub method with the specified method name is invoked. + * Should be used for primitives and non-generic classes. * * @param target The name of the hub method to define. * @param callback The handler that will be raised when the hub method is invoked. @@ -840,6 +892,7 @@ public Subscription on(String target, Action1 callback, Class param /** * Registers a handler that will be invoked when the hub method with the specified method name is invoked. + * Should be used for primitives and non-generic classes. * * @param target The name of the hub method to define. * @param callback The handler that will be raised when the hub method is invoked. @@ -858,6 +911,7 @@ public Subscription on(String target, Action2 callback, Class Subscription on(String target, Action3 callback, /** * Registers a handler that will be invoked when the hub method with the specified method name is invoked. + * Should be used for primitives and non-generic classes. * * @param target The name of the hub method to define. * @param callback The handler that will be raised when the hub method is invoked. @@ -902,6 +957,7 @@ public Subscription on(String target, Action4 c /** * Registers a handler that will be invoked when the hub method with the specified method name is invoked. + * Should be used for primitives and non-generic classes. * * @param target The name of the hub method to define. * @param callback The handler that will be raised when the hub method is invoked. @@ -928,6 +984,7 @@ public Subscription on(String target, Action5 Subscription on(String target, Action6 Subscription on(String target, Action7 Subscription on(String target, Action8... 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 The first argument type. + * @return A {@link Subscription} that can be disposed to unsubscribe from the hub method. + */ + @SuppressWarnings("unchecked") + public Subscription on(String target, Action1 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 The first parameter type. + * @param The second parameter type. + * @return A {@link Subscription} that can be disposed to unsubscribe from the hub method. + */ + @SuppressWarnings("unchecked") + public Subscription on(String target, Action2 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 The first parameter type. + * @param The second parameter type. + * @param The third parameter type. + * @return A {@link Subscription} that can be disposed to unsubscribe from the hub method. + */ + @SuppressWarnings("unchecked") + public Subscription on(String target, Action3 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 The first parameter type. + * @param The second parameter type. + * @param The third parameter type. + * @param The fourth parameter type. + * @return A {@link Subscription} that can be disposed to unsubscribe from the hub method. + */ + @SuppressWarnings("unchecked") + public Subscription on(String target, Action4 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 The first parameter type. + * @param The second parameter type. + * @param The third parameter type. + * @param The fourth parameter type. + * @param The fifth parameter type. + * @return A {@link Subscription} that can be disposed to unsubscribe from the hub method. + */ + @SuppressWarnings("unchecked") + public Subscription on(String target, Action5 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 The first parameter type. + * @param The second parameter type. + * @param The third parameter type. + * @param The fourth parameter type. + * @param The fifth parameter type. + * @param The sixth parameter type. + * @return A {@link Subscription} that can be disposed to unsubscribe from the hub method. + */ + @SuppressWarnings("unchecked") + public Subscription on(String target, Action6 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 The first parameter type. + * @param The second parameter type. + * @param The third parameter type. + * @param The fourth parameter type. + * @param The fifth parameter type. + * @param The sixth parameter type. + * @param The seventh parameter type. + * @return A {@link Subscription} that can be disposed to unsubscribe from the hub method. + */ + @SuppressWarnings("unchecked") + public Subscription on(String target, Action7 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 The first parameter type. + * @param The second parameter type. + * @param The third parameter type. + * @param The fourth parameter type. + * @param The fifth parameter type. + * @param The sixth parameter type. + * @param The seventh parameter type. + * @param The eighth parameter type. + * @return A {@link Subscription} that can be disposed to unsubscribe from the hub method. + */ + @SuppressWarnings("unchecked") + public Subscription on(String target, Action8 callback, + Type param1, Type param2, Type param3, Type param4, Type param5, Type param6, Type param7, + Type param8) { + ActionBase action = params -> { + callback.invoke((T1)Utils.typeToClass(param1).cast(params[0]), (T2)Utils.typeToClass(param2).cast(params[1]), + (T3)Utils.typeToClass(param3).cast(params[2]), (T4)Utils.typeToClass(param4).cast(params[3]), + (T5)Utils.typeToClass(param5).cast(params[4]), (T6)Utils.typeToClass(param6).cast(params[5]), + (T7)Utils.typeToClass(param7).cast(params[6]), (T8)Utils.typeToClass(param8).cast(params[7])); + }; + return registerHandler(target, action, param1, param2, param3, param4, param5, param6, param7, param8); + } + + private Subscription registerHandler(String target, ActionBase action, Type... types) { InvocationHandler handler = handlers.put(target, action, types); logger.debug("Registering handler for client method: '{}'.", target); return new Subscription(handlers, handler, target); @@ -1087,7 +1363,7 @@ public InvocationRequest tryRemoveInvocation(String id) { } @Override - public Class getReturnType(String invocationId) { + public Type getReturnType(String invocationId) { InvocationRequest irq = getInvocation(invocationId); if (irq == null) { return null; @@ -1097,7 +1373,7 @@ public Class getReturnType(String invocationId) { } @Override - public List> getParameterTypes(String methodName) { + public List getParameterTypes(String methodName) { List handlers = connection.handlers.get(methodName); if (handlers == null) { logger.warn("Failed to find handler for '{}' method.", methodName); @@ -1108,7 +1384,7 @@ public List> getParameterTypes(String 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(); } } diff --git a/src/SignalR/clients/java/signalr/src/main/java/com/microsoft/signalr/HubMessageType.java b/src/SignalR/clients/java/signalr/src/main/java/com/microsoft/signalr/HubMessageType.java index 588a82e6d1a0..a1bfeebfcf3b 100644 --- a/src/SignalR/clients/java/signalr/src/main/java/com/microsoft/signalr/HubMessageType.java +++ b/src/SignalR/clients/java/signalr/src/main/java/com/microsoft/signalr/HubMessageType.java @@ -11,7 +11,8 @@ enum HubMessageType { CANCEL_INVOCATION(5), PING(6), CLOSE(7), - INVOCATION_BINDING_FAILURE(-1); + INVOCATION_BINDING_FAILURE(-1), + STREAM_BINDING_FAILURE(-2); public int value; HubMessageType(int id) { this.value = id; } diff --git a/src/SignalR/clients/java/signalr/src/main/java/com/microsoft/signalr/HubProtocol.java b/src/SignalR/clients/java/signalr/src/main/java/com/microsoft/signalr/HubProtocol.java index 072fa6b5065e..844a458c8cde 100644 --- a/src/SignalR/clients/java/signalr/src/main/java/com/microsoft/signalr/HubProtocol.java +++ b/src/SignalR/clients/java/signalr/src/main/java/com/microsoft/signalr/HubProtocol.java @@ -3,6 +3,9 @@ package com.microsoft.signalr; +import java.nio.ByteBuffer; +import java.util.List; + /** * A protocol abstraction for communicating with SignalR hubs. */ @@ -13,15 +16,15 @@ interface HubProtocol { /** * Creates a new list of {@link HubMessage}s. - * @param message A string representation of one or more {@link HubMessage}s. + * @param message A ByteBuffer representation of one or more {@link HubMessage}s. * @return A list of {@link HubMessage}s. */ - HubMessage[] parseMessages(String message, InvocationBinder binder); + List parseMessages(ByteBuffer message, InvocationBinder binder); /** * Writes the specified {@link HubMessage} to a String. * @param message The message to write. - * @return A string representation of the message. + * @return A ByteBuffer representation of the message. */ - String writeMessage(HubMessage message); + ByteBuffer writeMessage(HubMessage message); } diff --git a/src/SignalR/clients/java/signalr/src/main/java/com/microsoft/signalr/InvocationBinder.java b/src/SignalR/clients/java/signalr/src/main/java/com/microsoft/signalr/InvocationBinder.java index 3f3457f730dd..40767a7a985f 100644 --- a/src/SignalR/clients/java/signalr/src/main/java/com/microsoft/signalr/InvocationBinder.java +++ b/src/SignalR/clients/java/signalr/src/main/java/com/microsoft/signalr/InvocationBinder.java @@ -3,9 +3,13 @@ package com.microsoft.signalr; +import java.lang.reflect.Type; import java.util.List; +/** + * An abstraction for passing around information about method signatures. + */ interface InvocationBinder { - Class getReturnType(String invocationId); - List> getParameterTypes(String methodName); + Type getReturnType(String invocationId); + List getParameterTypes(String methodName); } \ No newline at end of file diff --git a/src/SignalR/clients/java/signalr/src/main/java/com/microsoft/signalr/InvocationHandler.java b/src/SignalR/clients/java/signalr/src/main/java/com/microsoft/signalr/InvocationHandler.java index 64f8ed3a1d83..0ee7b745e91c 100644 --- a/src/SignalR/clients/java/signalr/src/main/java/com/microsoft/signalr/InvocationHandler.java +++ b/src/SignalR/clients/java/signalr/src/main/java/com/microsoft/signalr/InvocationHandler.java @@ -3,20 +3,21 @@ package com.microsoft.signalr; +import java.lang.reflect.Type; import java.util.Arrays; import java.util.List; class InvocationHandler { - private final List> classes; + private final List types; private final ActionBase action; - InvocationHandler(ActionBase action, Class... classes) { + InvocationHandler(ActionBase action, Type... types) { this.action = action; - this.classes = Arrays.asList(classes); + this.types = Arrays.asList(types); } - public List> getClasses() { - return classes; + public List getTypes() { + return types; } public ActionBase getAction() { diff --git a/src/SignalR/clients/java/signalr/src/main/java/com/microsoft/signalr/InvocationMessage.java b/src/SignalR/clients/java/signalr/src/main/java/com/microsoft/signalr/InvocationMessage.java index ffb842c537c2..ecc3650ffef5 100644 --- a/src/SignalR/clients/java/signalr/src/main/java/com/microsoft/signalr/InvocationMessage.java +++ b/src/SignalR/clients/java/signalr/src/main/java/com/microsoft/signalr/InvocationMessage.java @@ -4,19 +4,20 @@ package com.microsoft.signalr; import java.util.Collection; +import java.util.Map; class InvocationMessage extends HubMessage { int type = HubMessageType.INVOCATION.value; + private Map headers; private final String invocationId; private final String target; private final Object[] arguments; private Collection streamIds; - - public InvocationMessage(String invocationId, String target, Object[] args) { - this(invocationId, target, args, null); - } - - public InvocationMessage(String invocationId, String target, Object[] args, Collection streamIds) { + + public InvocationMessage(Map headers, String invocationId, String target, Object[] args, Collection streamIds) { + if (headers != null && !headers.isEmpty()) { + this.headers = headers; + } this.invocationId = invocationId; this.target = target; this.arguments = args; @@ -24,6 +25,10 @@ public InvocationMessage(String invocationId, String target, Object[] args, Coll this.streamIds = streamIds; } } + + public Map getHeaders() { + return headers; + } public String getInvocationId() { return invocationId; @@ -36,6 +41,10 @@ public String getTarget() { public Object[] getArguments() { return arguments; } + + public Collection getStreamIds() { + return streamIds; + } @Override public HubMessageType getMessageType() { diff --git a/src/SignalR/clients/java/signalr/src/main/java/com/microsoft/signalr/InvocationRequest.java b/src/SignalR/clients/java/signalr/src/main/java/com/microsoft/signalr/InvocationRequest.java index 7de8890db73f..7c57d6177c1c 100644 --- a/src/SignalR/clients/java/signalr/src/main/java/com/microsoft/signalr/InvocationRequest.java +++ b/src/SignalR/clients/java/signalr/src/main/java/com/microsoft/signalr/InvocationRequest.java @@ -3,17 +3,18 @@ package com.microsoft.signalr; +import java.lang.reflect.Type; import java.util.concurrent.CancellationException; import io.reactivex.subjects.ReplaySubject; import io.reactivex.subjects.Subject; class InvocationRequest { - private final Class returnType; + private final Type returnType; private final Subject pendingCall = ReplaySubject.create(); private final String invocationId; - InvocationRequest(Class returnType, String invocationId) { + InvocationRequest(Type returnType, String invocationId) { this.returnType = returnType; this.invocationId = invocationId; } @@ -47,7 +48,7 @@ public Subject getPendingCall() { return pendingCall; } - public Class getReturnType() { + public Type getReturnType() { return returnType; } diff --git a/src/SignalR/clients/java/signalr/src/main/java/com/microsoft/signalr/JsonHubProtocol.java b/src/SignalR/clients/java/signalr/src/main/java/com/microsoft/signalr/JsonHubProtocol.java index fa4d3212739d..892f600ea71a 100644 --- a/src/SignalR/clients/java/signalr/src/main/java/com/microsoft/signalr/JsonHubProtocol.java +++ b/src/SignalR/clients/java/signalr/src/main/java/com/microsoft/signalr/JsonHubProtocol.java @@ -5,7 +5,11 @@ import java.io.IOException; import java.io.StringReader; +import java.lang.reflect.Type; +import java.nio.ByteBuffer; +import java.nio.charset.StandardCharsets; import java.util.ArrayList; +import java.util.Arrays; import java.util.List; import com.google.gson.Gson; @@ -36,15 +40,26 @@ public TransferFormat getTransferFormat() { } @Override - public HubMessage[] parseMessages(String payload, InvocationBinder binder) { - if (payload.length() == 0) { - return new HubMessage[]{}; + public List parseMessages(ByteBuffer payload, InvocationBinder binder) { + String payloadStr; + // If the payload is readOnly, we have to copy the bytes from its array to make the payload string + if (payload.isReadOnly()) { + byte[] payloadBytes = new byte[payload.remaining()]; + payload.get(payloadBytes, 0, payloadBytes.length); + payloadStr = new String(payloadBytes, StandardCharsets.UTF_8); + // Otherwise we can allocate directly from its array + } else { + // The position of the ByteBuffer may have been incremented - make sure we only grab the remaining bytes + payloadStr = new String(payload.array(), payload.position(), payload.remaining(), StandardCharsets.UTF_8); } - if (!(payload.substring(payload.length() - 1).equals(RECORD_SEPARATOR))) { + if (payloadStr.length() == 0) { + return null; + } + if (!(payloadStr.substring(payloadStr.length() - 1).equals(RECORD_SEPARATOR))) { throw new RuntimeException("Message is incomplete."); } - String[] messages = payload.split(RECORD_SEPARATOR); + String[] messages = payloadStr.split(RECORD_SEPARATOR); List hubMessages = new ArrayList<>(); try { for (String str : messages) { @@ -87,7 +102,7 @@ public HubMessage[] parseMessages(String payload, InvocationBinder binder) { if (target != null) { boolean startedArray = false; try { - List> types = binder.getParameterTypes(target); + List types = binder.getParameterTypes(target); startedArray = true; arguments = bindArguments(reader, types); } catch (Exception ex) { @@ -125,7 +140,7 @@ public HubMessage[] parseMessages(String payload, InvocationBinder binder) { case INVOCATION: if (argumentsToken != null) { try { - List> types = binder.getParameterTypes(target); + List types = binder.getParameterTypes(target); arguments = bindArguments(argumentsToken, types); } catch (Exception ex) { argumentBindingException = ex; @@ -135,25 +150,25 @@ public HubMessage[] parseMessages(String payload, InvocationBinder binder) { hubMessages.add(new InvocationBindingFailureMessage(invocationId, target, argumentBindingException)); } else { if (arguments == null) { - hubMessages.add(new InvocationMessage(invocationId, target, new Object[0])); + hubMessages.add(new InvocationMessage(null, invocationId, target, new Object[0], null)); } else { - hubMessages.add(new InvocationMessage(invocationId, target, arguments.toArray())); + hubMessages.add(new InvocationMessage(null, invocationId, target, arguments.toArray(), null)); } } break; case COMPLETION: if (resultToken != null) { - Class returnType = binder.getReturnType(invocationId); + Type returnType = binder.getReturnType(invocationId); result = gson.fromJson(resultToken, returnType != null ? returnType : Object.class); } - hubMessages.add(new CompletionMessage(invocationId, result, error)); + hubMessages.add(new CompletionMessage(null, invocationId, result, error)); break; case STREAM_ITEM: if (resultToken != null) { - Class returnType = binder.getReturnType(invocationId); + Type returnType = binder.getReturnType(invocationId); result = gson.fromJson(resultToken, returnType != null ? returnType : Object.class); } - hubMessages.add(new StreamItem(invocationId, result)); + hubMessages.add(new StreamItem(null, invocationId, result)); break; case STREAM_INVOCATION: case CANCEL_INVOCATION: @@ -176,15 +191,15 @@ public HubMessage[] parseMessages(String payload, InvocationBinder binder) { throw new RuntimeException("Error reading JSON.", ex); } - return hubMessages.toArray(new HubMessage[hubMessages.size()]); + return hubMessages; } @Override - public String writeMessage(HubMessage hubMessage) { - return gson.toJson(hubMessage) + RECORD_SEPARATOR; + public ByteBuffer writeMessage(HubMessage hubMessage) { + return ByteBuffer.wrap((gson.toJson(hubMessage) + RECORD_SEPARATOR).getBytes(StandardCharsets.UTF_8)); } - private ArrayList bindArguments(JsonArray argumentsToken, List> paramTypes) { + private ArrayList bindArguments(JsonArray argumentsToken, List paramTypes) { if (argumentsToken.size() != paramTypes.size()) { throw new RuntimeException(String.format("Invocation provides %d argument(s) but target expects %d.", argumentsToken.size(), paramTypes.size())); } @@ -200,7 +215,7 @@ private ArrayList bindArguments(JsonArray argumentsToken, List> return arguments; } - private ArrayList bindArguments(JsonReader reader, List> paramTypes) throws IOException { + private ArrayList bindArguments(JsonReader reader, List paramTypes) throws IOException { reader.beginArray(); int paramCount = paramTypes.size(); int argCount = 0; diff --git a/src/SignalR/clients/java/signalr/src/main/java/com/microsoft/signalr/LongPollingTransport.java b/src/SignalR/clients/java/signalr/src/main/java/com/microsoft/signalr/LongPollingTransport.java index 32eaac224419..d17c20214820 100644 --- a/src/SignalR/clients/java/signalr/src/main/java/com/microsoft/signalr/LongPollingTransport.java +++ b/src/SignalR/clients/java/signalr/src/main/java/com/microsoft/signalr/LongPollingTransport.java @@ -3,6 +3,8 @@ package com.microsoft.signalr; +import java.nio.ByteBuffer; +import java.nio.charset.StandardCharsets; import java.util.Map; import java.util.concurrent.ExecutorService; import java.util.concurrent.Executors; @@ -100,7 +102,7 @@ private Completable poll(String url) { } else { if (response.getContent() != null) { logger.debug("Message received."); - onReceiveThread.submit(() ->this.onReceive(response.getContent())); + onReceiveThread.submit(() -> this.onReceive(response.getContent())); } else { logger.debug("Poll timed out, reissuing."); } @@ -121,7 +123,7 @@ private Completable poll(String url) { } @Override - public Completable send(String message) { + public Completable send(ByteBuffer message) { if (!this.active) { return Completable.error(new Exception("Cannot send unless the transport is active.")); } @@ -138,7 +140,7 @@ public void setOnReceive(OnReceiveCallBack callback) { } @Override - public void onReceive(String message) { + public void onReceive(ByteBuffer message) { this.onReceiveCallBack.invoke(message); logger.debug("OnReceived callback has been invoked."); } diff --git a/src/SignalR/clients/java/signalr/src/main/java/com/microsoft/signalr/MessagePackHubProtocol.java b/src/SignalR/clients/java/signalr/src/main/java/com/microsoft/signalr/MessagePackHubProtocol.java new file mode 100644 index 000000000000..bd7f9cd7b2e5 --- /dev/null +++ b/src/SignalR/clients/java/signalr/src/main/java/com/microsoft/signalr/MessagePackHubProtocol.java @@ -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 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 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 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 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 types = binder.getParameterTypes(target); + arguments = bindArguments(unpacker, types, payload); + } catch (Exception ex) { + return new InvocationBindingFailureMessage(invocationId, target, ex); + } + + Collection 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 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 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 headers = readHeaders(unpacker); + String invocationId = unpacker.unpackString(); + String target = unpacker.unpackString(); + + Object[] arguments = null; + try { + List types = binder.getParameterTypes(target); + arguments = bindArguments(unpacker, types, payload); + } catch (Exception ex) { + return new InvocationBindingFailureMessage(invocationId, target, ex); + } + + Collection streams = readStreamIds(unpacker); + + return new StreamInvocationMessage(headers, invocationId, target, arguments, streams); + } + + private HubMessage createCancelInvocationMessage(MessageUnpacker unpacker) throws IOException { + Map 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 readHeaders(MessageUnpacker unpacker) throws IOException { + int headerCount = unpacker.unpackMapHeader(); + if (headerCount > 0) { + Map headers = new HashMap(); + for (int i = 0; i < headerCount; i++) { + headers.put(unpacker.unpackString(), unpacker.unpackString()); + } + return headers; + } else { + return null; + } + } + + private void writeHeaders(Map 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 readStreamIds(MessageUnpacker unpacker) throws IOException { + int streamCount = unpacker.unpackArrayHeader(); + Collection streams = null; + + if (streamCount > 0) { + streams = new ArrayList(); + for (int i = 0; i < streamCount; i++) { + streams.add(unpacker.unpackString()); + } + } + + return streams; + } + + private void writeStreamIds(Collection 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 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)); + } +} diff --git a/src/SignalR/clients/java/signalr/src/main/java/com/microsoft/signalr/OkHttpWebSocketWrapper.java b/src/SignalR/clients/java/signalr/src/main/java/com/microsoft/signalr/OkHttpWebSocketWrapper.java index b76f02fe2608..ae273637204d 100644 --- a/src/SignalR/clients/java/signalr/src/main/java/com/microsoft/signalr/OkHttpWebSocketWrapper.java +++ b/src/SignalR/clients/java/signalr/src/main/java/com/microsoft/signalr/OkHttpWebSocketWrapper.java @@ -3,6 +3,8 @@ package com.microsoft.signalr; +import java.nio.ByteBuffer; +import java.nio.charset.StandardCharsets; import java.util.Map; import java.util.concurrent.locks.ReentrantLock; @@ -17,6 +19,7 @@ import okhttp3.Response; import okhttp3.WebSocket; import okhttp3.WebSocketListener; +import okio.ByteString; class OkHttpWebSocketWrapper extends WebSocketWrapper { private WebSocket websocketClient; @@ -60,8 +63,9 @@ public Completable stop() { } @Override - public Completable send(String message) { - websocketClient.send(message); + public Completable send(ByteBuffer message) { + ByteString bs = ByteString.of(message); + websocketClient.send(bs); return Completable.complete(); } @@ -83,7 +87,12 @@ public void onOpen(WebSocket webSocket, Response response) { @Override public void onMessage(WebSocket webSocket, String message) { - onReceive.invoke(message); + onReceive.invoke(ByteBuffer.wrap(message.getBytes(StandardCharsets.UTF_8))); + } + + @Override + public void onMessage(WebSocket webSocket, ByteString bytes) { + onReceive.invoke(bytes.asByteBuffer()); } @Override diff --git a/src/SignalR/clients/java/signalr/src/main/java/com/microsoft/signalr/OnReceiveCallBack.java b/src/SignalR/clients/java/signalr/src/main/java/com/microsoft/signalr/OnReceiveCallBack.java index 71faede3f000..4dde2204404f 100644 --- a/src/SignalR/clients/java/signalr/src/main/java/com/microsoft/signalr/OnReceiveCallBack.java +++ b/src/SignalR/clients/java/signalr/src/main/java/com/microsoft/signalr/OnReceiveCallBack.java @@ -3,6 +3,8 @@ package com.microsoft.signalr; +import java.nio.ByteBuffer; + interface OnReceiveCallBack { - void invoke(String message); + void invoke(ByteBuffer message); } diff --git a/src/SignalR/clients/java/signalr/src/main/java/com/microsoft/signalr/StreamBindingFailureMessage.java b/src/SignalR/clients/java/signalr/src/main/java/com/microsoft/signalr/StreamBindingFailureMessage.java new file mode 100644 index 000000000000..d7b145fa3f71 --- /dev/null +++ b/src/SignalR/clients/java/signalr/src/main/java/com/microsoft/signalr/StreamBindingFailureMessage.java @@ -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; + } +} diff --git a/src/SignalR/clients/java/signalr/src/main/java/com/microsoft/signalr/StreamInvocationMessage.java b/src/SignalR/clients/java/signalr/src/main/java/com/microsoft/signalr/StreamInvocationMessage.java index 046ec6003684..0a8c6211b6bb 100644 --- a/src/SignalR/clients/java/signalr/src/main/java/com/microsoft/signalr/StreamInvocationMessage.java +++ b/src/SignalR/clients/java/signalr/src/main/java/com/microsoft/signalr/StreamInvocationMessage.java @@ -4,16 +4,12 @@ package com.microsoft.signalr; import java.util.Collection; +import java.util.Map; final class StreamInvocationMessage extends InvocationMessage { - - public StreamInvocationMessage(String invocationId, String target, Object[] args) { - super(invocationId, target, args); - super.type = HubMessageType.STREAM_INVOCATION.value; - } - - public StreamInvocationMessage(String invocationId, String target, Object[] args, Collection streamIds) { - super(invocationId, target, args, streamIds); + + public StreamInvocationMessage(Map headers, String invocationId, String target, Object[] args, Collection streamIds) { + super(headers, invocationId, target, args, streamIds); super.type = HubMessageType.STREAM_INVOCATION.value; } diff --git a/src/SignalR/clients/java/signalr/src/main/java/com/microsoft/signalr/StreamItem.java b/src/SignalR/clients/java/signalr/src/main/java/com/microsoft/signalr/StreamItem.java index 3b422daf4681..9b18f0bdc3de 100644 --- a/src/SignalR/clients/java/signalr/src/main/java/com/microsoft/signalr/StreamItem.java +++ b/src/SignalR/clients/java/signalr/src/main/java/com/microsoft/signalr/StreamItem.java @@ -3,15 +3,25 @@ package com.microsoft.signalr; +import java.util.Map; + final class StreamItem extends HubMessage { private final int type = HubMessageType.STREAM_ITEM.value; + private Map headers; private final String invocationId; private final Object item; - - public StreamItem(String invocationId, Object item) { + + public StreamItem(Map headers, String invocationId, Object item) { + if (headers != null && !headers.isEmpty()) { + this.headers = headers; + } this.invocationId = invocationId; this.item = item; } + + public Map getHeaders() { + return headers; + } public String getInvocationId() { return invocationId; diff --git a/src/SignalR/clients/java/signalr/src/main/java/com/microsoft/signalr/Transport.java b/src/SignalR/clients/java/signalr/src/main/java/com/microsoft/signalr/Transport.java index 97e3a81896ae..ab0d547898bc 100644 --- a/src/SignalR/clients/java/signalr/src/main/java/com/microsoft/signalr/Transport.java +++ b/src/SignalR/clients/java/signalr/src/main/java/com/microsoft/signalr/Transport.java @@ -3,13 +3,15 @@ package com.microsoft.signalr; +import java.nio.ByteBuffer; + import io.reactivex.Completable; interface Transport { Completable start(String url); - Completable send(String message); + Completable send(ByteBuffer message); void setOnReceive(OnReceiveCallBack callback); - void onReceive(String message); + void onReceive(ByteBuffer message); void setOnClose(TransportOnClosedCallback onCloseCallback); Completable stop(); } diff --git a/src/SignalR/clients/java/signalr/src/main/java/com/microsoft/signalr/Utils.java b/src/SignalR/clients/java/signalr/src/main/java/com/microsoft/signalr/Utils.java index d08c6fb91455..88d589a93683 100644 --- a/src/SignalR/clients/java/signalr/src/main/java/com/microsoft/signalr/Utils.java +++ b/src/SignalR/clients/java/signalr/src/main/java/com/microsoft/signalr/Utils.java @@ -3,6 +3,16 @@ package com.microsoft.signalr; +import java.io.IOException; +import java.lang.reflect.Array; +import java.lang.reflect.GenericArrayType; +import java.lang.reflect.ParameterizedType; +import java.lang.reflect.Type; +import java.lang.reflect.TypeVariable; +import java.lang.reflect.WildcardType; +import java.nio.ByteBuffer; +import java.util.ArrayList; + class Utils { public static String appendQueryString(String original, String queryStringValue) { if (original.contains("?")) { @@ -11,4 +21,92 @@ public static String appendQueryString(String original, String 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 getLengthHeader(int length) { + // This code writes length prefix of the message as a VarInt. Read the comment in + // the readLengthHeader for details. + + ArrayList header = new ArrayList(); + 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()); + } + } } \ No newline at end of file diff --git a/src/SignalR/clients/java/signalr/src/main/java/com/microsoft/signalr/WebSocketTransport.java b/src/SignalR/clients/java/signalr/src/main/java/com/microsoft/signalr/WebSocketTransport.java index f15ffd4bcee6..42ef00231f11 100644 --- a/src/SignalR/clients/java/signalr/src/main/java/com/microsoft/signalr/WebSocketTransport.java +++ b/src/SignalR/clients/java/signalr/src/main/java/com/microsoft/signalr/WebSocketTransport.java @@ -3,6 +3,7 @@ package com.microsoft.signalr; +import java.nio.ByteBuffer; import java.util.Map; import org.slf4j.Logger; @@ -59,7 +60,7 @@ public Completable start(String url) { } @Override - public Completable send(String message) { + public Completable send(ByteBuffer message) { return webSocketClient.send(message); } @@ -70,7 +71,7 @@ public void setOnReceive(OnReceiveCallBack callback) { } @Override - public void onReceive(String message) { + public void onReceive(ByteBuffer message) { this.onReceiveCallBack.invoke(message); } diff --git a/src/SignalR/clients/java/signalr/src/main/java/com/microsoft/signalr/WebSocketWrapper.java b/src/SignalR/clients/java/signalr/src/main/java/com/microsoft/signalr/WebSocketWrapper.java index 06d57a159bd3..773f40ab0e07 100644 --- a/src/SignalR/clients/java/signalr/src/main/java/com/microsoft/signalr/WebSocketWrapper.java +++ b/src/SignalR/clients/java/signalr/src/main/java/com/microsoft/signalr/WebSocketWrapper.java @@ -3,6 +3,8 @@ package com.microsoft.signalr; +import java.nio.ByteBuffer; + import io.reactivex.Completable; abstract class WebSocketWrapper { @@ -10,7 +12,7 @@ abstract class WebSocketWrapper { public abstract Completable stop(); - public abstract Completable send(String message); + public abstract Completable send(ByteBuffer message); public abstract void setOnReceive(OnReceiveCallBack onReceive); diff --git a/src/SignalR/clients/java/signalr/src/test/java/com/microsoft/signalr/ByteString.java b/src/SignalR/clients/java/signalr/src/test/java/com/microsoft/signalr/ByteString.java new file mode 100644 index 000000000000..2546d9245487 --- /dev/null +++ b/src/SignalR/clients/java/signalr/src/test/java/com/microsoft/signalr/ByteString.java @@ -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; + } +} diff --git a/src/SignalR/clients/java/signalr/src/test/java/com/microsoft/signalr/HandshakeProtocolTest.java b/src/SignalR/clients/java/signalr/src/test/java/com/microsoft/signalr/HandshakeProtocolTest.java index 811d39c5580e..e98b55ba0112 100644 --- a/src/SignalR/clients/java/signalr/src/test/java/com/microsoft/signalr/HandshakeProtocolTest.java +++ b/src/SignalR/clients/java/signalr/src/test/java/com/microsoft/signalr/HandshakeProtocolTest.java @@ -5,15 +5,16 @@ import static org.junit.jupiter.api.Assertions.*; +import java.nio.ByteBuffer; import org.junit.jupiter.api.Test; class HandshakeProtocolTest { @Test public void VerifyCreateHandshakerequestMessage() { HandshakeRequestMessage handshakeRequest = new HandshakeRequestMessage("json", 1); - String result = HandshakeProtocol.createHandshakeRequestMessage(handshakeRequest); + ByteBuffer result = HandshakeProtocol.createHandshakeRequestMessage(handshakeRequest); String expectedResult = "{\"protocol\":\"json\",\"version\":1}\u001E"; - assertEquals(expectedResult, result); + assertEquals(expectedResult, TestUtils.byteBufferToString(result)); } @Test diff --git a/src/SignalR/clients/java/signalr/src/test/java/com/microsoft/signalr/HubConnectionTest.java b/src/SignalR/clients/java/signalr/src/test/java/com/microsoft/signalr/HubConnectionTest.java index ab43980dd9fe..8bc833239e94 100644 --- a/src/SignalR/clients/java/signalr/src/test/java/com/microsoft/signalr/HubConnectionTest.java +++ b/src/SignalR/clients/java/signalr/src/test/java/com/microsoft/signalr/HubConnectionTest.java @@ -5,6 +5,8 @@ import static org.junit.jupiter.api.Assertions.*; +import java.lang.reflect.Type; +import java.nio.ByteBuffer; import java.util.Iterator; import java.util.List; import java.util.concurrent.CancellationException; @@ -16,6 +18,8 @@ import org.junit.jupiter.api.Test; +import com.fasterxml.jackson.core.type.TypeReference; + import io.reactivex.Completable; import io.reactivex.Observable; import io.reactivex.Single; @@ -27,6 +31,10 @@ class HubConnectionTest { private static final String RECORD_SEPARATOR = "\u001e"; + private static final Type booleanType = (new TypeReference() { }).getType(); + private static final Type doubleType = (new TypeReference() { }).getType(); + private static final Type integerType = (new TypeReference() { }).getType(); + private static final Type stringType = (new TypeReference() { }).getType(); @Test public void checkHubConnectionState() { @@ -202,7 +210,8 @@ public void hubConnectionReceiveHandshakeResponseWithError() { hubConnection.start(); mockTransport.getStartTask().timeout(1, TimeUnit.SECONDS).blockingAwait(); - Throwable exception = assertThrows(RuntimeException.class, () -> mockTransport.receiveMessage("{\"error\":\"Requested protocol 'messagepack' is not available.\"}" + RECORD_SEPARATOR)); + Throwable exception = assertThrows(RuntimeException.class, () -> + mockTransport.receiveMessage("{\"error\":\"Requested protocol 'messagepack' is not available.\"}" + RECORD_SEPARATOR)); assertEquals("Error in handshake Requested protocol 'messagepack' is not available.", exception.getMessage()); } @@ -220,7 +229,7 @@ public void registeringMultipleHandlersAndBothGetTriggered() { hubConnection.start().timeout(1, TimeUnit.SECONDS).blockingAwait(); - String message = mockTransport.getSentMessages()[0]; + String message = TestUtils.byteBufferToString(mockTransport.getSentMessages()[0]); String expectedHanshakeRequest = "{\"protocol\":\"json\",\"version\":1}" + RECORD_SEPARATOR; assertEquals(expectedHanshakeRequest, message); @@ -243,7 +252,7 @@ public void removeHandlerByName() { assertEquals(Double.valueOf(0), value.get()); hubConnection.start().timeout(1, TimeUnit.SECONDS).blockingAwait(); - String message = mockTransport.getSentMessages()[0]; + String message = TestUtils.byteBufferToString(mockTransport.getSentMessages()[0]); String expectedHanshakeRequest = "{\"protocol\":\"json\",\"version\":1}" + RECORD_SEPARATOR; assertEquals(expectedHanshakeRequest, message); @@ -270,7 +279,7 @@ public void addAndRemoveHandlerImmediately() { assertEquals(Double.valueOf(0), value.get()); hubConnection.start().timeout(1, TimeUnit.SECONDS).blockingAwait(); - String message = mockTransport.getSentMessages()[0]; + String message = TestUtils.byteBufferToString(mockTransport.getSentMessages()[0]); String expectedHanshakeRequest = "{\"protocol\":\"json\",\"version\":1}" + RECORD_SEPARATOR; assertEquals(expectedHanshakeRequest, message); @@ -295,7 +304,7 @@ public void removingMultipleHandlersWithOneCallToRemove() { assertEquals(Double.valueOf(0), value.get()); hubConnection.start().timeout(1, TimeUnit.SECONDS).blockingAwait(); - String message = mockTransport.getSentMessages()[0]; + String message = TestUtils.byteBufferToString(mockTransport.getSentMessages()[0]); String expectedHanshakeRequest = "{\"protocol\":\"json\",\"version\":1}" + RECORD_SEPARATOR; assertEquals(expectedHanshakeRequest, message); @@ -324,7 +333,7 @@ public void removeHandlerWithUnsubscribe() { assertEquals(Double.valueOf(0), value.get()); hubConnection.start().timeout(1, TimeUnit.SECONDS).blockingAwait(); - String message = mockTransport.getSentMessages()[0]; + String message = TestUtils.byteBufferToString(mockTransport.getSentMessages()[0]); String expectedHanshakeRequest = "{\"protocol\":\"json\",\"version\":1}" + RECORD_SEPARATOR; assertEquals(expectedHanshakeRequest, message); @@ -356,7 +365,7 @@ public void unsubscribeTwice() { assertEquals(Double.valueOf(0), value.get()); hubConnection.start().timeout(1, TimeUnit.SECONDS).blockingAwait(); - String message = mockTransport.getSentMessages()[0]; + String message = TestUtils.byteBufferToString(mockTransport.getSentMessages()[0]); String expectedHanshakeRequest = "{\"protocol\":\"json\",\"version\":1}" + RECORD_SEPARATOR; assertEquals(expectedHanshakeRequest, message); @@ -391,7 +400,7 @@ public void removeSingleHandlerWithUnsubscribe() { assertEquals(Double.valueOf(0), value.get()); hubConnection.start().timeout(1, TimeUnit.SECONDS).blockingAwait(); - String message = mockTransport.getSentMessages()[0]; + String message = TestUtils.byteBufferToString(mockTransport.getSentMessages()[0]); String expectedHanshakeRequest = "{\"protocol\":\"json\",\"version\":1}" + RECORD_SEPARATOR; assertEquals(expectedHanshakeRequest, message); @@ -460,12 +469,12 @@ public void checkStreamUploadSingleItemThroughSend() { hubConnection.send("UploadStream", stream); stream.onNext("FirstItem"); - String[] messages = mockTransport.getSentMessages(); - assertEquals("{\"type\":2,\"invocationId\":\"1\",\"item\":\"FirstItem\"}\u001E", messages[2]); + ByteBuffer[] messages = mockTransport.getSentMessages(); + assertEquals("{\"type\":2,\"invocationId\":\"1\",\"item\":\"FirstItem\"}\u001E", TestUtils.byteBufferToString(messages[2])); stream.onComplete(); messages = mockTransport.getSentMessages(); - assertEquals("{\"type\":3,\"invocationId\":\"1\"}\u001E", messages[3]); + assertEquals("{\"type\":3,\"invocationId\":\"1\"}\u001E", TestUtils.byteBufferToString(messages[3])); } @Test @@ -482,17 +491,17 @@ public void checkStreamUploadMultipleStreamsThroughSend() { firstStream.onNext("First Stream 1"); secondStream.onNext("Second Stream 1"); - String[] messages = mockTransport.getSentMessages(); + ByteBuffer[] messages = mockTransport.getSentMessages(); assertEquals(4, messages.length); - assertEquals("{\"type\":2,\"invocationId\":\"1\",\"item\":\"First Stream 1\"}\u001E", messages[2]); - assertEquals("{\"type\":2,\"invocationId\":\"2\",\"item\":\"Second Stream 1\"}\u001E", messages[3]); + assertEquals("{\"type\":2,\"invocationId\":\"1\",\"item\":\"First Stream 1\"}\u001E", TestUtils.byteBufferToString(messages[2])); + assertEquals("{\"type\":2,\"invocationId\":\"2\",\"item\":\"Second Stream 1\"}\u001E", TestUtils.byteBufferToString(messages[3])); firstStream.onComplete(); secondStream.onComplete(); messages = mockTransport.getSentMessages(); assertEquals(6, messages.length); - assertEquals("{\"type\":3,\"invocationId\":\"1\"}\u001E", messages[4]); - assertEquals("{\"type\":3,\"invocationId\":\"2\"}\u001E", messages[5]); + assertEquals("{\"type\":3,\"invocationId\":\"1\"}\u001E", TestUtils.byteBufferToString(messages[4])); + assertEquals("{\"type\":3,\"invocationId\":\"2\"}\u001E", TestUtils.byteBufferToString(messages[5])); } @Test @@ -506,13 +515,13 @@ public void checkStreamUploadThroughSendWithArgs() { hubConnection.send("UploadStream", stream, 12); stream.onNext("FirstItem"); - String[] messages = mockTransport.getSentMessages(); - assertEquals("{\"type\":1,\"target\":\"UploadStream\",\"arguments\":[12],\"streamIds\":[\"1\"]}\u001E", messages[1]); - assertEquals("{\"type\":2,\"invocationId\":\"1\",\"item\":\"FirstItem\"}\u001E", messages[2]); + ByteBuffer[] messages = mockTransport.getSentMessages(); + assertEquals("{\"type\":1,\"target\":\"UploadStream\",\"arguments\":[12],\"streamIds\":[\"1\"]}\u001E", TestUtils.byteBufferToString(messages[1])); + assertEquals("{\"type\":2,\"invocationId\":\"1\",\"item\":\"FirstItem\"}\u001E", TestUtils.byteBufferToString(messages[2])); stream.onComplete(); messages = mockTransport.getSentMessages(); - assertEquals("{\"type\":3,\"invocationId\":\"1\"}\u001E", messages[3]); + assertEquals("{\"type\":3,\"invocationId\":\"1\"}\u001E", TestUtils.byteBufferToString(messages[3])); } @Test @@ -526,13 +535,13 @@ public void streamMapIsClearedOnClose() { hubConnection.send("UploadStream", stream, 12); stream.onNext("FirstItem"); - String[] messages = mockTransport.getSentMessages(); - assertEquals("{\"type\":1,\"target\":\"UploadStream\",\"arguments\":[12],\"streamIds\":[\"1\"]}\u001E", messages[1]); - assertEquals("{\"type\":2,\"invocationId\":\"1\",\"item\":\"FirstItem\"}\u001E", messages[2]); + ByteBuffer[] messages = mockTransport.getSentMessages(); + assertEquals("{\"type\":1,\"target\":\"UploadStream\",\"arguments\":[12],\"streamIds\":[\"1\"]}\u001E", TestUtils.byteBufferToString(messages[1])); + assertEquals("{\"type\":2,\"invocationId\":\"1\",\"item\":\"FirstItem\"}\u001E", TestUtils.byteBufferToString(messages[2])); stream.onComplete(); messages = mockTransport.getSentMessages(); - assertEquals("{\"type\":3,\"invocationId\":\"1\"}\u001E", messages[3]); + assertEquals("{\"type\":3,\"invocationId\":\"1\"}\u001E", TestUtils.byteBufferToString(messages[3])); hubConnection.stop().timeout(1, TimeUnit.SECONDS).blockingAwait(); @@ -555,11 +564,11 @@ public void streamMapEntriesRemovedOnStreamClose() { stream.onNext("FirstItem"); secondStream.onNext("SecondItem"); - String[] messages = mockTransport.getSentMessages(); - assertEquals("{\"type\":1,\"target\":\"UploadStream\",\"arguments\":[12],\"streamIds\":[\"1\"]}\u001E", messages[1]); - assertEquals("{\"type\":1,\"target\":\"SecondUploadStream\",\"arguments\":[13],\"streamIds\":[\"2\"]}\u001E", messages[2]); - assertEquals("{\"type\":2,\"invocationId\":\"1\",\"item\":\"FirstItem\"}\u001E", messages[3]); - assertEquals("{\"type\":2,\"invocationId\":\"2\",\"item\":\"SecondItem\"}\u001E", messages[4]); + ByteBuffer[] messages = mockTransport.getSentMessages(); + assertEquals("{\"type\":1,\"target\":\"UploadStream\",\"arguments\":[12],\"streamIds\":[\"1\"]}\u001E", TestUtils.byteBufferToString(messages[1])); + assertEquals("{\"type\":1,\"target\":\"SecondUploadStream\",\"arguments\":[13],\"streamIds\":[\"2\"]}\u001E", TestUtils.byteBufferToString(messages[2])); + assertEquals("{\"type\":2,\"invocationId\":\"1\",\"item\":\"FirstItem\"}\u001E", TestUtils.byteBufferToString(messages[3])); + assertEquals("{\"type\":2,\"invocationId\":\"2\",\"item\":\"SecondItem\"}\u001E", TestUtils.byteBufferToString(messages[4])); assertEquals(2, hubConnection.getStreamMap().size()); @@ -576,8 +585,8 @@ public void streamMapEntriesRemovedOnStreamClose() { assertTrue(hubConnection.getStreamMap().isEmpty()); messages = mockTransport.getSentMessages(); - assertEquals("{\"type\":3,\"invocationId\":\"1\"}\u001E", messages[5]); - assertEquals("{\"type\":3,\"invocationId\":\"2\",\"error\":\"java.lang.Exception: Exception\"}\u001E", messages[6]); + assertEquals("{\"type\":3,\"invocationId\":\"1\"}\u001E", TestUtils.byteBufferToString(messages[5])); + assertEquals("{\"type\":3,\"invocationId\":\"2\",\"error\":\"java.lang.Exception: Exception\"}\u001E", TestUtils.byteBufferToString(messages[6])); hubConnection.stop().timeout(1, TimeUnit.SECONDS).blockingAwait(); assertTrue(hubConnection.getStreamMap().isEmpty()); @@ -594,17 +603,17 @@ public void useSameSubjectMultipleTimes() { hubConnection.send("UploadStream", stream, stream); stream.onNext("FirstItem"); - String[] messages = mockTransport.getSentMessages(); + ByteBuffer[] messages = mockTransport.getSentMessages(); assertEquals(4, messages.length); - assertEquals("{\"type\":1,\"target\":\"UploadStream\",\"arguments\":[],\"streamIds\":[\"1\",\"2\"]}\u001E", messages[1]); - assertEquals("{\"type\":2,\"invocationId\":\"1\",\"item\":\"FirstItem\"}\u001E", messages[2]); - assertEquals("{\"type\":2,\"invocationId\":\"2\",\"item\":\"FirstItem\"}\u001E", messages[3]); + assertEquals("{\"type\":1,\"target\":\"UploadStream\",\"arguments\":[],\"streamIds\":[\"1\",\"2\"]}\u001E", TestUtils.byteBufferToString(messages[1])); + assertEquals("{\"type\":2,\"invocationId\":\"1\",\"item\":\"FirstItem\"}\u001E", TestUtils.byteBufferToString(messages[2])); + assertEquals("{\"type\":2,\"invocationId\":\"2\",\"item\":\"FirstItem\"}\u001E", TestUtils.byteBufferToString(messages[3])); stream.onComplete(); messages = mockTransport.getSentMessages(); assertEquals(6, messages.length); - assertEquals("{\"type\":3,\"invocationId\":\"1\"}\u001E", messages[4]); - assertEquals("{\"type\":3,\"invocationId\":\"2\"}\u001E", messages[5]); + assertEquals("{\"type\":3,\"invocationId\":\"1\"}\u001E", TestUtils.byteBufferToString(messages[4])); + assertEquals("{\"type\":3,\"invocationId\":\"2\"}\u001E", TestUtils.byteBufferToString(messages[5])); } @Test @@ -618,14 +627,50 @@ public void checkStreamUploadSingleItemThroughInvoke() { hubConnection.invoke(String.class, "UploadStream", stream); stream.onNext("FirstItem"); - String[] messages = mockTransport.getSentMessages(); + ByteBuffer[] messages = mockTransport.getSentMessages(); + for (ByteBuffer bb: messages) { + System.out.println(TestUtils.byteBufferToString(bb)); + } + assertEquals(3, messages.length); + assertEquals("{\"type\":1,\"invocationId\":\"1\",\"target\":\"UploadStream\",\"arguments\":[],\"streamIds\":[\"2\"]}\u001E", + TestUtils.byteBufferToString(messages[1])); + assertEquals("{\"type\":2,\"invocationId\":\"2\",\"item\":\"FirstItem\"}\u001E", TestUtils.byteBufferToString(messages[2])); + + stream.onComplete(); + messages = mockTransport.getSentMessages(); + assertEquals("{\"type\":3,\"invocationId\":\"2\"}\u001E", TestUtils.byteBufferToString(messages[3])); + } + + @Test + public void checkStreamUploadSingleItemThroughInvokeWithMessagePack() { + MockTransport mockTransport = new MockTransport(); + HubConnection hubConnection = TestUtils.createHubConnection("http://example.com", mockTransport, true); + + hubConnection.start().timeout(1, TimeUnit.SECONDS).blockingAwait(); + + ReplaySubject stream = ReplaySubject.create(); + hubConnection.invoke(stringType, "UploadStream", stream); + + stream.onNext("FirstItem"); + ByteBuffer[] messages = mockTransport.getSentMessages(); + for (ByteBuffer bb: messages) { + System.out.println(TestUtils.byteBufferToString(bb)); + } assertEquals(3, messages.length); - assertEquals("{\"type\":1,\"invocationId\":\"1\",\"target\":\"UploadStream\",\"arguments\":[],\"streamIds\":[\"2\"]}\u001E", messages[1]); - assertEquals("{\"type\":2,\"invocationId\":\"2\",\"item\":\"FirstItem\"}\u001E", messages[2]); + + byte[] firstMessageExpectedBytes = new byte[] { 0x16, (byte) 0x96, 0x01, (byte) 0x80, (byte) 0xA1, 0x31, (byte) 0xAC, 0x55, 0x70, 0x6C, 0x6F, + 0x61, 0x64, 0x53, 0x74, 0x72, 0x65, 0x61, 0x6D, (byte) 0x90, (byte) 0x91, (byte) 0xA1, 0x32 }; + assertEquals(ByteString.of(firstMessageExpectedBytes), ByteString.of(messages[1])); + + byte[] secondMessageExpectedBytes = new byte[] { 0x0F, (byte) 0x94, 0x02, (byte) 0x80, (byte) 0xA1, 0x32, (byte) 0xA9, 0x46, 0x69, 0x72, 0x73, + 0x74, 0x49, 0x74, 0x65, 0x6D }; + assertEquals(ByteString.of(secondMessageExpectedBytes), ByteString.of(messages[2])); stream.onComplete(); messages = mockTransport.getSentMessages(); - assertEquals("{\"type\":3,\"invocationId\":\"2\"}\u001E", messages[3]); + + byte[] thirdMessageExpectedBytes = new byte[] { 0x06, (byte) 0x94, 0x03, (byte) 0x80, (byte) 0xA1, 0x32, 0x02 }; + assertEquals(ByteString.of(thirdMessageExpectedBytes), ByteString.of(messages[3])); } @Test @@ -640,15 +685,47 @@ public void checkStreamUploadSingleItemThroughStream() { stream.onNext("FirstItem"); - String[] messages = mockTransport.getSentMessages(); + ByteBuffer[] messages = mockTransport.getSentMessages(); assertEquals(3, messages.length); - assertEquals("{\"type\":4,\"invocationId\":\"1\",\"target\":\"UploadStream\",\"arguments\":[],\"streamIds\":[\"2\"]}\u001E", messages[1]); - assertEquals("{\"type\":2,\"invocationId\":\"2\",\"item\":\"FirstItem\"}\u001E", messages[2]); + assertEquals("{\"type\":4,\"invocationId\":\"1\",\"target\":\"UploadStream\",\"arguments\":[],\"streamIds\":[\"2\"]}\u001E", + TestUtils.byteBufferToString(messages[1])); + assertEquals("{\"type\":2,\"invocationId\":\"2\",\"item\":\"FirstItem\"}\u001E", TestUtils.byteBufferToString(messages[2])); stream.onComplete(); messages = mockTransport.getSentMessages(); assertEquals(4, messages.length); - assertEquals("{\"type\":3,\"invocationId\":\"2\"}\u001E", messages[3]); + assertEquals("{\"type\":3,\"invocationId\":\"2\"}\u001E", TestUtils.byteBufferToString(messages[3])); + } + + @Test + public void checkStreamUploadSingleItemThroughStreamWithMessagePack() { + MockTransport mockTransport = new MockTransport(); + HubConnection hubConnection = TestUtils.createHubConnection("http://example.com", mockTransport, true); + + hubConnection.start().timeout(1, TimeUnit.SECONDS).blockingAwait(); + + ReplaySubject stream = ReplaySubject.create(); + hubConnection.stream(stringType, "UploadStream", stream); + + stream.onNext("FirstItem"); + + ByteBuffer[] messages = mockTransport.getSentMessages(); + assertEquals(3, messages.length); + + byte[] firstMessageExpectedBytes = new byte[] { 0x16, (byte) 0x96, 0x04, (byte) 0x80, (byte) 0xA1, 0x31, (byte) 0xAC, 0x55, 0x70, 0x6C, 0x6F, + 0x61, 0x64, 0x53, 0x74, 0x72, 0x65, 0x61, 0x6D, (byte) 0x90, (byte) 0x91, (byte) 0xA1, 0x32 }; + assertEquals(ByteString.of(firstMessageExpectedBytes), ByteString.of(messages[1])); + + byte[] secondMessageExpectedBytes = new byte[] { 0x0F, (byte) 0x94, 0x02, (byte) 0x80, (byte) 0xA1, 0x32, (byte) 0xA9, 0x46, 0x69, 0x72, 0x73, + 0x74, 0x49, 0x74, 0x65, 0x6D }; + assertEquals(ByteString.of(secondMessageExpectedBytes), ByteString.of(messages[2])); + + stream.onComplete(); + messages = mockTransport.getSentMessages(); + assertEquals(4, messages.length); + + byte[] thirdMessageExpectedBytes = new byte[] { 0x06, (byte) 0x94, 0x03, (byte) 0x80, (byte) 0xA1, 0x32, 0x02 }; + assertEquals(ByteString.of(thirdMessageExpectedBytes), ByteString.of(messages[3])); } @Test @@ -663,26 +740,86 @@ public void useSameSubjectInMutlipleStreamsFromDifferentMethods() { hubConnection.invoke(String.class, "UploadStream", stream); hubConnection.stream(String.class, "UploadStream", stream); - String[] messages = mockTransport.getSentMessages(); + ByteBuffer[] messages = mockTransport.getSentMessages(); + assertEquals(4, messages.length); + assertEquals("{\"type\":1,\"target\":\"UploadStream\",\"arguments\":[],\"streamIds\":[\"1\"]}\u001E", TestUtils.byteBufferToString(messages[1])); + assertEquals("{\"type\":1,\"invocationId\":\"2\",\"target\":\"UploadStream\",\"arguments\":[],\"streamIds\":[\"3\"]}\u001E", + TestUtils.byteBufferToString(messages[2])); + assertEquals("{\"type\":4,\"invocationId\":\"4\",\"target\":\"UploadStream\",\"arguments\":[],\"streamIds\":[\"5\"]}\u001E", + TestUtils.byteBufferToString(messages[3])); + + stream.onNext("FirstItem"); + + messages = mockTransport.getSentMessages(); + assertEquals(7, messages.length); + assertEquals("{\"type\":2,\"invocationId\":\"1\",\"item\":\"FirstItem\"}\u001E", TestUtils.byteBufferToString(messages[4])); + assertEquals("{\"type\":2,\"invocationId\":\"3\",\"item\":\"FirstItem\"}\u001E", TestUtils.byteBufferToString(messages[5])); + assertEquals("{\"type\":2,\"invocationId\":\"5\",\"item\":\"FirstItem\"}\u001E", TestUtils.byteBufferToString(messages[6])); + + stream.onComplete(); + messages = mockTransport.getSentMessages(); + assertEquals(10, messages.length); + assertEquals("{\"type\":3,\"invocationId\":\"1\"}\u001E", TestUtils.byteBufferToString(messages[7])); + assertEquals("{\"type\":3,\"invocationId\":\"3\"}\u001E", TestUtils.byteBufferToString(messages[8])); + assertEquals("{\"type\":3,\"invocationId\":\"5\"}\u001E", TestUtils.byteBufferToString(messages[9])); + } + + @Test + public void useSameSubjectInMutlipleStreamsFromDifferentMethodsWithMessagePack() { + MockTransport mockTransport = new MockTransport(); + HubConnection hubConnection = TestUtils.createHubConnection("http://example.com", mockTransport, true); + + hubConnection.start().timeout(1, TimeUnit.SECONDS).blockingAwait(); + + ReplaySubject stream = ReplaySubject.create(); + hubConnection.send("UploadStream", stream); + hubConnection.invoke(stringType, "UploadStream", stream); + hubConnection.stream(stringType, "UploadStream", stream); + + ByteBuffer[] messages = mockTransport.getSentMessages(); assertEquals(4, messages.length); - assertEquals("{\"type\":1,\"target\":\"UploadStream\",\"arguments\":[],\"streamIds\":[\"1\"]}\u001E", messages[1]); - assertEquals("{\"type\":1,\"invocationId\":\"2\",\"target\":\"UploadStream\",\"arguments\":[],\"streamIds\":[\"3\"]}\u001E", messages[2]); - assertEquals("{\"type\":4,\"invocationId\":\"4\",\"target\":\"UploadStream\",\"arguments\":[],\"streamIds\":[\"5\"]}\u001E", messages[3]); + + byte[] firstMessageExpectedBytes = new byte[] { 0x15, (byte) 0x96, 0x01, (byte) 0x80, (byte) 0xC0, (byte) 0xAC, 0x55, 0x70, 0x6C, 0x6F, 0x61, + 0x64, 0x53, 0x74, 0x72, 0x65, 0x61, 0x6D, (byte) 0x90, (byte) 0x91, (byte) 0xA1, 0x31 }; + assertEquals(ByteString.of(firstMessageExpectedBytes), ByteString.of(messages[1])); + + byte[] secondMessageExpectedBytes = new byte[] { 0x16, (byte) 0x96, 0x01, (byte) 0x80, (byte) 0xA1, 0x32, (byte) 0xAC, 0x55, 0x70, 0x6C, 0x6F, + 0x61, 0x64, 0x53, 0x74, 0x72, 0x65, 0x61, 0x6D, (byte) 0x90, (byte) 0x91, (byte) 0xA1, 0x33 }; + assertEquals(ByteString.of(secondMessageExpectedBytes), ByteString.of(messages[2])); + + byte[] thirdMessageExpectedBytes = new byte[] { 0x16, (byte) 0x96, 0x04, (byte) 0x80, (byte) 0xA1, 0x34, (byte) 0xAC, 0x55, 0x70, 0x6C, 0x6F, + 0x61, 0x64, 0x53, 0x74, 0x72, 0x65, 0x61, 0x6D, (byte) 0x90, (byte) 0x91, (byte) 0xA1, 0x35 }; + assertEquals(ByteString.of(thirdMessageExpectedBytes), ByteString.of(messages[3])); stream.onNext("FirstItem"); messages = mockTransport.getSentMessages(); assertEquals(7, messages.length); - assertEquals("{\"type\":2,\"invocationId\":\"1\",\"item\":\"FirstItem\"}\u001E", messages[4]); - assertEquals("{\"type\":2,\"invocationId\":\"3\",\"item\":\"FirstItem\"}\u001E", messages[5]); - assertEquals("{\"type\":2,\"invocationId\":\"5\",\"item\":\"FirstItem\"}\u001E", messages[6]); + + byte[] fourthMessageExpectedBytes = new byte[] { 0x0F, (byte) 0x94, 0x02, (byte) 0x80, (byte) 0xA1, 0x31, (byte) 0xA9, 0x46, 0x69, 0x72, 0x73, 0x74, + 0x49, 0x74, 0x65, 0x6D }; + assertEquals(ByteString.of(fourthMessageExpectedBytes), ByteString.of(messages[4])); + + byte[] fifthMessageExpectedBytes = new byte[] { 0x0F, (byte) 0x94, 0x02, (byte) 0x80, (byte) 0xA1, 0x33, (byte) 0xA9, 0x46, 0x69, 0x72, 0x73, 0x74, + 0x49, 0x74, 0x65, 0x6D }; + assertEquals(ByteString.of(fifthMessageExpectedBytes), ByteString.of(messages[5])); + + byte[] sixthMessageExpectedBytes = new byte[] { 0x0F, (byte) 0x94, 0x02, (byte) 0x80, (byte) 0xA1, 0x35, (byte) 0xA9, 0x46, 0x69, 0x72, 0x73, 0x74, + 0x49, 0x74, 0x65, 0x6D }; + assertEquals(ByteString.of(sixthMessageExpectedBytes), ByteString.of(messages[6])); stream.onComplete(); messages = mockTransport.getSentMessages(); assertEquals(10, messages.length); - assertEquals("{\"type\":3,\"invocationId\":\"1\"}\u001E", messages[7]); - assertEquals("{\"type\":3,\"invocationId\":\"3\"}\u001E", messages[8]); - assertEquals("{\"type\":3,\"invocationId\":\"5\"}\u001E", messages[9]); + + byte[] seventhMessageExpectedBytes = new byte[] { 0x06, (byte) 0x94, 0x03, (byte) 0x80, (byte) 0xA1, 0x31, 0x02 }; + assertEquals(ByteString.of(seventhMessageExpectedBytes), ByteString.of(messages[7])); + + byte[] eighthMessageExpectedBytes = new byte[] { 0x06, (byte) 0x94, 0x03, (byte) 0x80, (byte) 0xA1, 0x33, 0x02 }; + assertEquals(ByteString.of(eighthMessageExpectedBytes), ByteString.of(messages[8])); + + byte[] ninthMessageExpectedBytes = new byte[] { 0x06, (byte) 0x94, 0x03, (byte) 0x80, (byte) 0xA1, 0x35, 0x02 }; + assertEquals(ByteString.of(ninthMessageExpectedBytes), ByteString.of(messages[9])); } @Test @@ -697,10 +834,11 @@ public void streamUploadCallOnError() { stream.onNext("FirstItem"); stream.onError(new RuntimeException("onError called")); - String[] messages = mockTransport.getSentMessages(); + ByteBuffer[] messages = mockTransport.getSentMessages(); assertEquals(4, messages.length); - assertEquals("{\"type\":2,\"invocationId\":\"1\",\"item\":\"FirstItem\"}\u001E", messages[2]); - assertEquals("{\"type\":3,\"invocationId\":\"1\",\"error\":\"java.lang.RuntimeException: onError called\"}\u001E", messages[3]); + assertEquals("{\"type\":2,\"invocationId\":\"1\",\"item\":\"FirstItem\"}\u001E", TestUtils.byteBufferToString(messages[2])); + assertEquals("{\"type\":3,\"invocationId\":\"1\",\"error\":\"java.lang.RuntimeException: onError called\"}\u001E", + TestUtils.byteBufferToString(messages[3])); // onComplete doesn't send a completion message after onError. stream.onComplete(); @@ -722,16 +860,16 @@ public void checkStreamUploadMultipleItemsThroughSend() { stream.onNext("SecondItem"); stream.onNext("ThirdItem"); - String[] messages = mockTransport.getSentMessages(); + ByteBuffer[] messages = mockTransport.getSentMessages(); assertEquals(5, messages.length); - assertEquals("{\"type\":2,\"invocationId\":\"1\",\"item\":\"FirstItem\"}\u001E", messages[2]); - assertEquals("{\"type\":2,\"invocationId\":\"1\",\"item\":\"SecondItem\"}\u001E", messages[3]); - assertEquals("{\"type\":2,\"invocationId\":\"1\",\"item\":\"ThirdItem\"}\u001E", messages[4]); + assertEquals("{\"type\":2,\"invocationId\":\"1\",\"item\":\"FirstItem\"}\u001E", TestUtils.byteBufferToString(messages[2])); + assertEquals("{\"type\":2,\"invocationId\":\"1\",\"item\":\"SecondItem\"}\u001E", TestUtils.byteBufferToString(messages[3])); + assertEquals("{\"type\":2,\"invocationId\":\"1\",\"item\":\"ThirdItem\"}\u001E", TestUtils.byteBufferToString(messages[4])); stream.onComplete(); messages = mockTransport.getSentMessages(); assertEquals(6, messages.length); - assertEquals("{\"type\":3,\"invocationId\":\"1\"}\u001E", messages[5]); + assertEquals("{\"type\":3,\"invocationId\":\"1\"}\u001E", TestUtils.byteBufferToString(messages[5])); } @Test @@ -747,15 +885,47 @@ public void checkStreamUploadMultipleItemsThroughInvoke() { stream.onNext("FirstItem"); stream.onNext("SecondItem"); - String[] messages = mockTransport.getSentMessages(); + ByteBuffer[] messages = mockTransport.getSentMessages(); + assertEquals(4, messages.length); + assertEquals("{\"type\":2,\"invocationId\":\"2\",\"item\":\"FirstItem\"}\u001E", TestUtils.byteBufferToString(messages[2])); + assertEquals("{\"type\":2,\"invocationId\":\"2\",\"item\":\"SecondItem\"}\u001E", TestUtils.byteBufferToString(messages[3])); + + stream.onComplete(); + messages = mockTransport.getSentMessages(); + assertEquals(5, messages.length); + assertEquals("{\"type\":3,\"invocationId\":\"2\"}\u001E", TestUtils.byteBufferToString(messages[4])); + } + + @Test + public void checkStreamUploadMultipleItemsThroughInvokeWithMessagePack() { + MockTransport mockTransport = new MockTransport(); + HubConnection hubConnection = TestUtils.createHubConnection("http://example.com", mockTransport, true); + + hubConnection.start().timeout(1, TimeUnit.SECONDS).blockingAwait(); + + ReplaySubject stream = ReplaySubject.create(); + hubConnection.invoke(stringType, "UploadStream", stream); + + stream.onNext("FirstItem"); + stream.onNext("SecondItem"); + + ByteBuffer[] messages = mockTransport.getSentMessages(); assertEquals(4, messages.length); - assertEquals("{\"type\":2,\"invocationId\":\"2\",\"item\":\"FirstItem\"}\u001E", messages[2]); - assertEquals("{\"type\":2,\"invocationId\":\"2\",\"item\":\"SecondItem\"}\u001E", messages[3]); + + byte[] firstMessageExpectedBytes = new byte[] { 0x0F, (byte) 0x94, 0x02, (byte) 0x80, (byte) 0xA1, 0x32, (byte) 0xA9, 0x46, 0x69, 0x72, 0x73, 0x74, + 0x49, 0x74, 0x65, 0x6D }; + assertEquals(ByteString.of(firstMessageExpectedBytes), ByteString.of(messages[2])); + + byte[] secondMessageExpectedBytes = new byte[] { 0x10, (byte) 0x94, 0x02, (byte) 0x80, (byte) 0xA1, 0x32, (byte) 0xAA, 0x53, 0x65, 0x63, 0x6F, 0x6E, + 0x64, 0x49, 0x74, 0x65, 0x6D }; + assertEquals(ByteString.of(secondMessageExpectedBytes), ByteString.of(messages[3])); stream.onComplete(); messages = mockTransport.getSentMessages(); assertEquals(5, messages.length); - assertEquals("{\"type\":3,\"invocationId\":\"2\"}\u001E", messages[4]); + + byte[] thirdMessageExpectedBytes = new byte[] { 0x06, (byte) 0x94, 0x03, (byte) 0x80, (byte) 0xA1, 0x32, 0x02 }; + assertEquals(ByteString.of(thirdMessageExpectedBytes), ByteString.of(messages[4])); } @Test @@ -778,16 +948,16 @@ public void canStartAndStopMultipleStreams() { streamOne.onComplete(); streamTwo.onComplete(); - String[] messages = mockTransport.getSentMessages(); + ByteBuffer[] messages = mockTransport.getSentMessages(); // Handshake message + 2 calls to send + 4 calls to onNext + 2 calls to onComplete = 9 assertEquals(9, messages.length); - assertEquals("{\"type\":2,\"invocationId\":\"1\",\"item\":\"Stream One First Item\"}\u001E", messages[3]); - assertEquals("{\"type\":2,\"invocationId\":\"2\",\"item\":\"Stream Two First Item\"}\u001E", messages[4]); - assertEquals("{\"type\":2,\"invocationId\":\"1\",\"item\":\"Stream One Second Item\"}\u001E", messages[5]); - assertEquals("{\"type\":2,\"invocationId\":\"2\",\"item\":\"Stream Two Second Item\"}\u001E", messages[6]); - assertEquals("{\"type\":3,\"invocationId\":\"1\"}\u001E", messages[7]); - assertEquals("{\"type\":3,\"invocationId\":\"2\"}\u001E", messages[8]); + assertEquals("{\"type\":2,\"invocationId\":\"1\",\"item\":\"Stream One First Item\"}\u001E", TestUtils.byteBufferToString(messages[3])); + assertEquals("{\"type\":2,\"invocationId\":\"2\",\"item\":\"Stream Two First Item\"}\u001E", TestUtils.byteBufferToString(messages[4])); + assertEquals("{\"type\":2,\"invocationId\":\"1\",\"item\":\"Stream One Second Item\"}\u001E", TestUtils.byteBufferToString(messages[5])); + assertEquals("{\"type\":2,\"invocationId\":\"2\",\"item\":\"Stream Two Second Item\"}\u001E", TestUtils.byteBufferToString(messages[6])); + assertEquals("{\"type\":3,\"invocationId\":\"1\"}\u001E", TestUtils.byteBufferToString(messages[7])); + assertEquals("{\"type\":3,\"invocationId\":\"2\"}\u001E", TestUtils.byteBufferToString(messages[8])); } @Test @@ -804,7 +974,8 @@ public void checkStreamSingleItem() { (error) -> {}, () -> completed.set(true)); - assertEquals("{\"type\":4,\"invocationId\":\"1\",\"target\":\"echo\",\"arguments\":[\"message\"]}" + RECORD_SEPARATOR, mockTransport.getSentMessages()[1]); + assertEquals("{\"type\":4,\"invocationId\":\"1\",\"target\":\"echo\",\"arguments\":[\"message\"]}" + RECORD_SEPARATOR, + TestUtils.byteBufferToString(mockTransport.getSentMessages()[1])); assertFalse(completed.get()); assertFalse(onNextCalled.get()); @@ -817,7 +988,39 @@ public void checkStreamSingleItem() { assertEquals("First", result.timeout(1000, TimeUnit.MILLISECONDS).blockingFirst()); } + + @Test + public void checkStreamSingleItemWithMessagePack() { + MockTransport mockTransport = new MockTransport(); + HubConnection hubConnection = TestUtils.createHubConnection("http://example.com", mockTransport, true); + + hubConnection.start().timeout(1, TimeUnit.SECONDS).blockingAwait(); + + AtomicBoolean completed = new AtomicBoolean(); + AtomicBoolean onNextCalled = new AtomicBoolean(); + Observable result = hubConnection.stream(stringType, "echo", "message"); + result.subscribe((item) -> onNextCalled.set(true), + (error) -> {}, + () -> completed.set(true)); + + byte[] firstMessageExpectedBytes = new byte[] { 0x14, (byte) 0x96, 0x04, (byte) 0x80, (byte) 0xA1, 0x31, (byte) 0xA4, 0x65, 0x63, 0x68, 0x6F, + (byte) 0x91, (byte) 0xA7, 0x6D, 0x65, 0x73, 0x73, 0x61, 0x67, 0x65, (byte) 0x90 }; + assertEquals(ByteString.of(firstMessageExpectedBytes), ByteString.of(mockTransport.getSentMessages()[1])); + assertFalse(completed.get()); + assertFalse(onNextCalled.get()); + + byte[] secondMessageExpectedBytes = new byte[] { 0x0B, (byte) 0x94, 0x02, (byte) 0x80, (byte) 0xA1, 0x31, (byte) 0xA5, 0x46, 0x69, 0x72, 0x73, 0x74 }; + mockTransport.receiveMessage(ByteBuffer.wrap(secondMessageExpectedBytes)); + assertTrue(onNextCalled.get()); + + byte[] thirdMessageExpectedBytes = new byte[] { 0x0C, (byte) 0x95, 0x03, (byte) 0x80, (byte) 0xA1, 0x31, 0x03, (byte) 0xA5, 0x68, 0x65, 0x6C, 0x6C, 0x6F }; + mockTransport.receiveMessage(ByteBuffer.wrap(thirdMessageExpectedBytes)); + assertTrue(completed.get()); + + assertEquals("First", result.timeout(1000, TimeUnit.MILLISECONDS).blockingFirst()); + } + @Test public void checkStreamCompletionResult() { MockTransport mockTransport = new MockTransport(); @@ -832,7 +1035,8 @@ public void checkStreamCompletionResult() { (error) -> {}, () -> completed.set(true)); - assertEquals("{\"type\":4,\"invocationId\":\"1\",\"target\":\"echo\",\"arguments\":[\"message\"]}" + RECORD_SEPARATOR, mockTransport.getSentMessages()[1]); + assertEquals("{\"type\":4,\"invocationId\":\"1\",\"target\":\"echo\",\"arguments\":[\"message\"]}" + RECORD_SEPARATOR, + TestUtils.byteBufferToString(mockTransport.getSentMessages()[1])); assertFalse(completed.get()); assertFalse(onNextCalled.get()); @@ -846,6 +1050,40 @@ public void checkStreamCompletionResult() { assertEquals("First", result.timeout(1000, TimeUnit.MILLISECONDS).blockingFirst()); assertEquals("COMPLETED", result.timeout(1000, TimeUnit.MILLISECONDS).blockingLast()); } + + @Test + public void checkStreamCompletionResultWithMessagePack() { + MockTransport mockTransport = new MockTransport(); + HubConnection hubConnection = TestUtils.createHubConnection("http://example.com", mockTransport, true); + + hubConnection.start().timeout(1, TimeUnit.SECONDS).blockingAwait(); + + AtomicBoolean completed = new AtomicBoolean(); + AtomicBoolean onNextCalled = new AtomicBoolean(); + Observable result = hubConnection.stream(stringType, "echo", "message"); + result.subscribe((item) -> onNextCalled.set(true), + (error) -> {}, + () -> completed.set(true)); + + byte[] firstMessageExpectedBytes = new byte[] { 0x14, (byte) 0x96, 0x04, (byte) 0x80, (byte) 0xA1, 0x31, (byte) 0xA4, 0x65, 0x63, 0x68, 0x6F, + (byte) 0x91, (byte) 0xA7, 0x6D, 0x65, 0x73, 0x73, 0x61, 0x67, 0x65, (byte) 0x90 }; + assertEquals(ByteString.of(firstMessageExpectedBytes), ByteString.of(mockTransport.getSentMessages()[1])); + assertFalse(completed.get()); + assertFalse(onNextCalled.get()); + + byte[] secondMessageExpectedBytes = new byte[] { 0x0B, (byte) 0x94, 0x02, (byte) 0x80, (byte) 0xA1, 0x31, (byte) 0xA5, 0x46, 0x69, 0x72, 0x73, 0x74 }; + mockTransport.receiveMessage(ByteBuffer.wrap(secondMessageExpectedBytes)); + + assertTrue(onNextCalled.get()); + + byte[] thirdMessageExpectedBytes = new byte[] { 0x10, (byte) 0x95, 0x03, (byte) 0x80, (byte) 0xA1, 0x31, 0x03, (byte) 0xA9, 0x43, 0x4F, 0x4D, 0x50, + 0x4C, 0x45, 0x54, 0x45, 0x44 }; + mockTransport.receiveMessage(ByteBuffer.wrap(thirdMessageExpectedBytes)); + assertTrue(completed.get()); + + assertEquals("First", result.timeout(1000, TimeUnit.MILLISECONDS).blockingFirst()); + assertEquals("COMPLETED", result.timeout(1000, TimeUnit.MILLISECONDS).blockingLast()); + } @Test public void checkStreamCompletionError() { @@ -861,7 +1099,8 @@ public void checkStreamCompletionError() { (error) -> onErrorCalled.set(true), () -> {}); - assertEquals("{\"type\":4,\"invocationId\":\"1\",\"target\":\"echo\",\"arguments\":[\"message\"]}" + RECORD_SEPARATOR, mockTransport.getSentMessages()[1]); + assertEquals("{\"type\":4,\"invocationId\":\"1\",\"target\":\"echo\",\"arguments\":[\"message\"]}" + RECORD_SEPARATOR, + TestUtils.byteBufferToString(mockTransport.getSentMessages()[1])); assertFalse(onErrorCalled.get()); assertFalse(onNextCalled.get()); @@ -876,6 +1115,40 @@ public void checkStreamCompletionError() { Throwable exception = assertThrows(HubException.class, () -> result.timeout(1000, TimeUnit.MILLISECONDS).blockingLast()); assertEquals("There was an error", exception.getMessage()); } + + @Test + public void checkStreamCompletionErrorWithMessagePack() { + MockTransport mockTransport = new MockTransport(); + HubConnection hubConnection = TestUtils.createHubConnection("http://example.com", mockTransport, true); + + hubConnection.start().timeout(1, TimeUnit.SECONDS).blockingAwait(); + + AtomicBoolean onErrorCalled = new AtomicBoolean(); + AtomicBoolean onNextCalled = new AtomicBoolean(); + Observable result = hubConnection.stream(stringType, "echo", "message"); + result.subscribe((item) -> onNextCalled.set(true), + (error) -> onErrorCalled.set(true), + () -> {}); + + byte[] firstMessageExpectedBytes = new byte[] { 0x14, (byte) 0x96, 0x04, (byte) 0x80, (byte) 0xA1, 0x31, (byte) 0xA4, 0x65, 0x63, 0x68, 0x6F, + (byte) 0x91, (byte) 0xA7, 0x6D, 0x65, 0x73, 0x73, 0x61, 0x67, 0x65, (byte) 0x90 }; + assertEquals(ByteString.of(firstMessageExpectedBytes), ByteString.of(mockTransport.getSentMessages()[1])); + assertFalse(onErrorCalled.get()); + assertFalse(onNextCalled.get()); + + byte[] secondMessageExpectedBytes = new byte[] { 0x0B, (byte) 0x94, 0x02, (byte) 0x80, (byte) 0xA1, 0x31, (byte) 0xA5, 0x46, 0x69, 0x72, 0x73, 0x74 }; + mockTransport.receiveMessage(ByteBuffer.wrap(secondMessageExpectedBytes)); + + assertTrue(onNextCalled.get()); + + byte[] thirdMessageExpectedBytes = new byte[] { 0x0C, (byte) 0x95, 0x03, (byte) 0x80, (byte) 0xA1, 0x31, 0x01, (byte) 0xA5, 0x45, 0x72, 0x72, 0x6F, 0x72 }; + mockTransport.receiveMessage(ByteBuffer.wrap(thirdMessageExpectedBytes)); + assertTrue(onErrorCalled.get()); + + assertEquals("First", result.timeout(1000, TimeUnit.MILLISECONDS).blockingFirst()); + Throwable exception = assertThrows(HubException.class, () -> result.timeout(1000, TimeUnit.MILLISECONDS).blockingLast()); + assertEquals("Error", exception.getMessage()); + } @Test public void checkStreamMultipleItems() { @@ -890,7 +1163,8 @@ public void checkStreamMultipleItems() { (error) -> {/*OnError*/}, () -> {/*OnCompleted*/completed.set(true);}); - assertEquals("{\"type\":4,\"invocationId\":\"1\",\"target\":\"echo\",\"arguments\":[\"message\"]}" + RECORD_SEPARATOR, mockTransport.getSentMessages()[1]); + assertEquals("{\"type\":4,\"invocationId\":\"1\",\"target\":\"echo\",\"arguments\":[\"message\"]}" + RECORD_SEPARATOR, + TestUtils.byteBufferToString(mockTransport.getSentMessages()[1])); assertFalse(completed.get()); mockTransport.receiveMessage("{\"type\":2,\"invocationId\":\"1\",\"item\":\"First\"}" + RECORD_SEPARATOR); @@ -902,6 +1176,39 @@ public void checkStreamMultipleItems() { assertEquals("Second", resultIterator.next()); assertTrue(completed.get()); } + + @Test + public void checkStreamMultipleItemsWithMessagePack() { + MockTransport mockTransport = new MockTransport(); + HubConnection hubConnection = TestUtils.createHubConnection("http://example.com", mockTransport, true); + + hubConnection.start().timeout(1, TimeUnit.SECONDS).blockingAwait(); + + AtomicBoolean completed = new AtomicBoolean(); + Observable result = hubConnection.stream(stringType, "echo", "message"); + result.subscribe((item) -> {/*OnNext*/ }, + (error) -> {/*OnError*/}, + () -> {/*OnCompleted*/completed.set(true);}); + + byte[] firstMessageExpectedBytes = new byte[] { 0x14, (byte) 0x96, 0x04, (byte) 0x80, (byte) 0xA1, 0x31, (byte) 0xA4, 0x65, 0x63, 0x68, 0x6F, + (byte) 0x91, (byte) 0xA7, 0x6D, 0x65, 0x73, 0x73, 0x61, 0x67, 0x65, (byte) 0x90 }; + assertEquals(ByteString.of(firstMessageExpectedBytes), ByteString.of(mockTransport.getSentMessages()[1])); + assertFalse(completed.get()); + + byte[] secondMessageExpectedBytes = new byte[] { 0x0B, (byte) 0x94, 0x02, (byte) 0x80, (byte) 0xA1, 0x31, (byte) 0xA5, 0x46, 0x69, 0x72, 0x73, 0x74 }; + mockTransport.receiveMessage(ByteBuffer.wrap(secondMessageExpectedBytes)); + + byte[] thirdMessageExpectedBytes = new byte[] { 0x0C, (byte) 0x94, 0x02, (byte) 0x80, (byte) 0xA1, 0x31, (byte) 0xA6, 0x53, 0x65, 0x63, 0x6F, 0x6E, 0x64 }; + mockTransport.receiveMessage(ByteBuffer.wrap(thirdMessageExpectedBytes)); + + byte[] fourthMessageExpectedBytes = new byte[] { 0x06, (byte) 0x94, 0x03, (byte) 0x80, (byte) 0xA1, 0x31, 0x02 }; + mockTransport.receiveMessage(ByteBuffer.wrap(fourthMessageExpectedBytes)); + + Iterator resultIterator = result.timeout(1000, TimeUnit.MILLISECONDS).blockingIterable().iterator(); + assertEquals("First", resultIterator.next()); + assertEquals("Second", resultIterator.next()); + assertTrue(completed.get()); + } @Test public void checkCancelIsSentAfterDispose() { @@ -916,11 +1223,35 @@ public void checkCancelIsSentAfterDispose() { (error) -> {/*OnError*/}, () -> {/*OnCompleted*/completed.set(true);}); - assertEquals("{\"type\":4,\"invocationId\":\"1\",\"target\":\"echo\",\"arguments\":[\"message\"]}" + RECORD_SEPARATOR, mockTransport.getSentMessages()[1]); + assertEquals("{\"type\":4,\"invocationId\":\"1\",\"target\":\"echo\",\"arguments\":[\"message\"]}" + RECORD_SEPARATOR, + TestUtils.byteBufferToString(mockTransport.getSentMessages()[1])); + assertFalse(completed.get()); + + subscription.dispose(); + assertEquals("{\"type\":5,\"invocationId\":\"1\"}" + RECORD_SEPARATOR, TestUtils.byteBufferToString(mockTransport.getSentMessages()[2])); + } + + @Test + public void checkCancelIsSentAfterDisposeWithMessagePack() { + MockTransport mockTransport = new MockTransport(); + HubConnection hubConnection = TestUtils.createHubConnection("http://example.com", mockTransport, true); + + hubConnection.start().timeout(1, TimeUnit.SECONDS).blockingAwait(); + + AtomicBoolean completed = new AtomicBoolean(); + Observable result = hubConnection.stream(stringType, "echo", "message"); + Disposable subscription = result.subscribe((item) -> {/*OnNext*/ }, + (error) -> {/*OnError*/}, + () -> {/*OnCompleted*/completed.set(true);}); + + byte[] firstMessageExpectedBytes = new byte[] { 0x14, (byte) 0x96, 0x04, (byte) 0x80, (byte) 0xA1, 0x31, (byte) 0xA4, 0x65, 0x63, 0x68, 0x6F, + (byte) 0x91, (byte) 0xA7, 0x6D, 0x65, 0x73, 0x73, 0x61, 0x67, 0x65, (byte) 0x90 }; + assertEquals(ByteString.of(firstMessageExpectedBytes), ByteString.of(mockTransport.getSentMessages()[1])); assertFalse(completed.get()); subscription.dispose(); - assertEquals("{\"type\":5,\"invocationId\":\"1\"}" + RECORD_SEPARATOR, mockTransport.getSentMessages()[2]); + assertEquals(ByteString.of(new byte[] { 0x05, (byte) 0x93, 0x05, (byte) 0x80, (byte) 0xA1, 0x31 }), + ByteString.of(mockTransport.getSentMessages()[2])); } @Test @@ -942,12 +1273,41 @@ public void checkCancelIsSentAfterAllSubscriptionsAreDisposed() { subscription.dispose(); assertEquals(2, mockTransport.getSentMessages().length); assertEquals("{\"type\":4,\"invocationId\":\"1\",\"target\":\"echo\",\"arguments\":[\"message\"]}" + RECORD_SEPARATOR, - mockTransport.getSentMessages()[mockTransport.getSentMessages().length - 1]); + TestUtils.byteBufferToString(mockTransport.getSentMessages()[mockTransport.getSentMessages().length - 1])); secondSubscription.dispose(); assertEquals(3, mockTransport.getSentMessages().length); assertEquals("{\"type\":5,\"invocationId\":\"1\"}" + RECORD_SEPARATOR, - mockTransport.getSentMessages()[mockTransport.getSentMessages().length - 1]); + TestUtils.byteBufferToString(mockTransport.getSentMessages()[mockTransport.getSentMessages().length - 1])); + } + + @Test + public void checkCancelIsSentAfterAllSubscriptionsAreDisposedWithMessagePack() { + MockTransport mockTransport = new MockTransport(); + HubConnection hubConnection = TestUtils.createHubConnection("http://example.com", mockTransport, true); + + hubConnection.start().timeout(1, TimeUnit.SECONDS).blockingAwait(); + + Observable result = hubConnection.stream(stringType, "echo", "message"); + Disposable subscription = result.subscribe((item) -> {/*OnNext*/ }, + (error) -> {/*OnError*/}, + () -> {/*OnCompleted*/}); + + Disposable secondSubscription = result.subscribe((item) -> {/*OnNext*/ }, + (error) -> {/*OnError*/}, + () -> {/*OnCompleted*/}); + + subscription.dispose(); + assertEquals(2, mockTransport.getSentMessages().length); + + byte[] firstMessageExpectedBytes = new byte[] { 0x14, (byte) 0x96, 0x04, (byte) 0x80, (byte) 0xA1, 0x31, (byte) 0xA4, 0x65, 0x63, 0x68, 0x6F, + (byte) 0x91, (byte) 0xA7, 0x6D, 0x65, 0x73, 0x73, 0x61, 0x67, 0x65, (byte) 0x90 }; + assertEquals(ByteString.of(firstMessageExpectedBytes), ByteString.of(mockTransport.getSentMessages()[mockTransport.getSentMessages().length - 1])); + + secondSubscription.dispose(); + assertEquals(3, mockTransport.getSentMessages().length); + assertEquals(ByteString.of(new byte[] { 0x05, (byte) 0x93, 0x05, (byte) 0x80, (byte) 0xA1, 0x31 }), + ByteString.of(mockTransport.getSentMessages()[mockTransport.getSentMessages().length - 1])); } @Test @@ -962,7 +1322,8 @@ public void checkStreamWithDispose() { (error) -> {/*OnError*/}, () -> {/*OnCompleted*/}); - assertEquals("{\"type\":4,\"invocationId\":\"1\",\"target\":\"echo\",\"arguments\":[\"message\"]}" + RECORD_SEPARATOR, mockTransport.getSentMessages()[1]); + assertEquals("{\"type\":4,\"invocationId\":\"1\",\"target\":\"echo\",\"arguments\":[\"message\"]}" + RECORD_SEPARATOR, + TestUtils.byteBufferToString(mockTransport.getSentMessages()[1])); mockTransport.receiveMessage("{\"type\":2,\"invocationId\":\"1\",\"item\":\"First\"}" + RECORD_SEPARATOR); @@ -971,6 +1332,32 @@ public void checkStreamWithDispose() { assertEquals("First", result.timeout(1000, TimeUnit.MILLISECONDS).blockingLast()); } + + @Test + public void checkStreamWithDisposeWithMessagePack() { + MockTransport mockTransport = new MockTransport(); + HubConnection hubConnection = TestUtils.createHubConnection("http://example.com", mockTransport, true); + + hubConnection.start().timeout(1, TimeUnit.SECONDS).blockingAwait(); + + Observable result = hubConnection.stream(stringType, "echo", "message"); + Disposable subscription = result.subscribe((item) -> {/*OnNext*/}, + (error) -> {/*OnError*/}, + () -> {/*OnCompleted*/}); + + byte[] firstMessageExpectedBytes = new byte[] { 0x14, (byte) 0x96, 0x04, (byte) 0x80, (byte) 0xA1, 0x31, (byte) 0xA4, 0x65, 0x63, 0x68, 0x6F, + (byte) 0x91, (byte) 0xA7, 0x6D, 0x65, 0x73, 0x73, 0x61, 0x67, 0x65, (byte) 0x90 }; + assertEquals(ByteString.of(firstMessageExpectedBytes), ByteString.of(mockTransport.getSentMessages()[1])); + + byte[] secondMessageExpectedBytes = new byte[] { 0x0B, (byte) 0x94, 0x02, (byte) 0x80, (byte) 0xA1, 0x31, (byte) 0xA5, 0x46, 0x69, 0x72, 0x73, 0x74 }; + mockTransport.receiveMessage(ByteBuffer.wrap(secondMessageExpectedBytes)); + + subscription.dispose(); + byte[] thirdMessageExpectedBytes = new byte[] { 0x0C, (byte) 0x94, 0x02, (byte) 0x80, (byte) 0xA1, 0x31, (byte) 0xA6, 0x53, 0x65, 0x63, 0x6F, 0x6E, 0x64 }; + mockTransport.receiveMessage(ByteBuffer.wrap(thirdMessageExpectedBytes)); + + assertEquals("First", result.timeout(1000, TimeUnit.MILLISECONDS).blockingLast()); + } @Test public void checkStreamWithDisposeWithMultipleSubscriptions() { @@ -989,7 +1376,8 @@ public void checkStreamWithDisposeWithMultipleSubscriptions() { (error) -> {/*OnError*/}, () -> {/*OnCompleted*/completed.set(true);}); - assertEquals("{\"type\":4,\"invocationId\":\"1\",\"target\":\"echo\",\"arguments\":[\"message\"]}" + RECORD_SEPARATOR, mockTransport.getSentMessages()[1]); + assertEquals("{\"type\":4,\"invocationId\":\"1\",\"target\":\"echo\",\"arguments\":[\"message\"]}" + RECORD_SEPARATOR, + TestUtils.byteBufferToString(mockTransport.getSentMessages()[1])); assertFalse(completed.get()); mockTransport.receiveMessage("{\"type\":2,\"invocationId\":\"1\",\"item\":\"First\"}" + RECORD_SEPARATOR); @@ -1004,6 +1392,45 @@ public void checkStreamWithDisposeWithMultipleSubscriptions() { subscription2.dispose(); assertEquals("Second", result.timeout(1000, TimeUnit.MILLISECONDS).blockingLast()); } + + @Test + public void checkStreamWithDisposeWithMultipleSubscriptionsWithMessagePack() { + MockTransport mockTransport = new MockTransport(); + HubConnection hubConnection = TestUtils.createHubConnection("http://example.com", mockTransport, true); + + hubConnection.start().timeout(1, TimeUnit.SECONDS).blockingAwait(); + + AtomicBoolean completed = new AtomicBoolean(); + Observable result = hubConnection.stream(stringType, "echo", "message"); + Disposable subscription = result.subscribe((item) -> {/*OnNext*/}, + (error) -> {/*OnError*/}, + () -> {/*OnCompleted*/}); + + Disposable subscription2 = result.subscribe((item) -> {/*OnNext*/}, + (error) -> {/*OnError*/}, + () -> {/*OnCompleted*/completed.set(true);}); + + byte[] firstMessageExpectedBytes = new byte[] { 0x14, (byte) 0x96, 0x04, (byte) 0x80, (byte) 0xA1, 0x31, (byte) 0xA4, 0x65, 0x63, 0x68, 0x6F, + (byte) 0x91, (byte) 0xA7, 0x6D, 0x65, 0x73, 0x73, 0x61, 0x67, 0x65, (byte) 0x90 }; + assertEquals(ByteString.of(firstMessageExpectedBytes), ByteString.of(mockTransport.getSentMessages()[1])); + assertFalse(completed.get()); + + byte[] secondMessageExpectedBytes = new byte[] { 0x0B, (byte) 0x94, 0x02, (byte) 0x80, (byte) 0xA1, 0x31, (byte) 0xA5, 0x46, 0x69, 0x72, 0x73, 0x74 }; + mockTransport.receiveMessage(ByteBuffer.wrap(secondMessageExpectedBytes)); + + subscription.dispose(); + byte[] thirdMessageExpectedBytes = new byte[] { 0x0C, (byte) 0x94, 0x02, (byte) 0x80, (byte) 0xA1, 0x31, (byte) 0xA6, 0x53, 0x65, 0x63, 0x6F, 0x6E, 0x64 }; + mockTransport.receiveMessage(ByteBuffer.wrap(thirdMessageExpectedBytes)); + + byte[] fourthMessageExpectedBytes = new byte[] { 0x06, (byte) 0x94, 0x03, (byte) 0x80, (byte) 0xA1, 0x31, 0x02 }; + mockTransport.receiveMessage(ByteBuffer.wrap(fourthMessageExpectedBytes)); + + assertTrue(completed.get()); + assertEquals("First", result.timeout(1000, TimeUnit.MILLISECONDS).blockingFirst()); + + subscription2.dispose(); + assertEquals("Second", result.timeout(1000, TimeUnit.MILLISECONDS).blockingLast()); + } @Test public void invokeWaitsForCompletionMessage() { @@ -1015,7 +1442,8 @@ public void invokeWaitsForCompletionMessage() { AtomicBoolean done = new AtomicBoolean(); Single result = hubConnection.invoke(Integer.class, "echo", "message"); result.doOnSuccess(value -> done.set(true)).subscribe(); - assertEquals("{\"type\":1,\"invocationId\":\"1\",\"target\":\"echo\",\"arguments\":[\"message\"]}" + RECORD_SEPARATOR, mockTransport.getSentMessages()[1]); + assertEquals("{\"type\":1,\"invocationId\":\"1\",\"target\":\"echo\",\"arguments\":[\"message\"]}" + RECORD_SEPARATOR, + TestUtils.byteBufferToString(mockTransport.getSentMessages()[1])); assertFalse(done.get()); mockTransport.receiveMessage("{\"type\":3,\"invocationId\":\"1\",\"result\":42}" + RECORD_SEPARATOR); @@ -1023,6 +1451,28 @@ public void invokeWaitsForCompletionMessage() { assertEquals(Integer.valueOf(42), result.timeout(1000, TimeUnit.MILLISECONDS).blockingGet()); assertTrue(done.get()); } + + @Test + public void invokeWaitsForCompletionMessageWithMessagePack() { + MockTransport mockTransport = new MockTransport(); + HubConnection hubConnection = TestUtils.createHubConnection("http://example.com", mockTransport, true); + + hubConnection.start().timeout(1, TimeUnit.SECONDS).blockingAwait(); + + AtomicBoolean done = new AtomicBoolean(); + Single result = hubConnection.invoke(integerType, "echo", "message"); + result.doOnSuccess(value -> done.set(true)).subscribe(); + + byte[] firstMessageExpectedBytes = new byte[] { 0x14, (byte) 0x96, 0x01, (byte) 0x80, (byte) 0xA1, 0x31, (byte) 0xA4, 0x65, 0x63, 0x68, 0x6F, + (byte) 0x91, (byte) 0xA7, 0x6D, 0x65, 0x73, 0x73, 0x61, 0x67, 0x65, (byte) 0x90 }; + assertEquals(ByteString.of(firstMessageExpectedBytes), ByteString.of(mockTransport.getSentMessages()[1])); + assertFalse(done.get()); + + mockTransport.receiveMessage(ByteBuffer.wrap(new byte[] { 0x07, (byte) 0x95, 0x03, (byte) 0x80, (byte) 0xA1, 0x31, 0x03, 0x2A })); + + assertEquals(Integer.valueOf(42), result.timeout(1000, TimeUnit.MILLISECONDS).blockingGet()); + assertTrue(done.get()); + } @Test public void invokeNoReturnValueWaitsForCompletion() { @@ -1035,7 +1485,8 @@ public void invokeNoReturnValueWaitsForCompletion() { Completable result = hubConnection.invoke("test", "message"); result.doOnComplete(() -> done.set(true)).subscribe(); - assertEquals("{\"type\":1,\"invocationId\":\"1\",\"target\":\"test\",\"arguments\":[\"message\"]}" + RECORD_SEPARATOR, mockTransport.getSentMessages()[1]); + assertEquals("{\"type\":1,\"invocationId\":\"1\",\"target\":\"test\",\"arguments\":[\"message\"]}" + RECORD_SEPARATOR, + TestUtils.byteBufferToString(mockTransport.getSentMessages()[1])); assertFalse(done.get()); mockTransport.receiveMessage("{\"type\":3,\"invocationId\":\"1\"}" + RECORD_SEPARATOR); @@ -1043,6 +1494,28 @@ public void invokeNoReturnValueWaitsForCompletion() { assertNull(result.timeout(1000, TimeUnit.MILLISECONDS).blockingGet()); assertTrue(done.get()); } + + @Test + public void invokeNoReturnValueWaitsForCompletionWithMessagePack() { + MockTransport mockTransport = new MockTransport(); + HubConnection hubConnection = TestUtils.createHubConnection("http://example.com", mockTransport, true); + + hubConnection.start().timeout(1, TimeUnit.SECONDS).blockingAwait(); + + AtomicBoolean done = new AtomicBoolean(); + Completable result = hubConnection.invoke("test", "message"); + result.doOnComplete(() -> done.set(true)).subscribe(); + + byte[] firstMessageExpectedBytes = new byte[] { 0x14, (byte) 0x96, 0x01, (byte) 0x80, (byte) 0xA1, 0x31, (byte) 0xA4, 0x74, 0x65, 0x73, 0x74, + (byte) 0x91, (byte) 0xA7, 0x6D, 0x65, 0x73, 0x73, 0x61, 0x67, 0x65, (byte) 0x90 }; + assertEquals(ByteString.of(firstMessageExpectedBytes), ByteString.of(mockTransport.getSentMessages()[1])); + assertFalse(done.get()); + + mockTransport.receiveMessage(ByteBuffer.wrap(new byte[] { 0x06, (byte) 0x94, 0x03, (byte) 0x80, (byte) 0xA1, 0x31, 0x02 })); + + assertNull(result.timeout(1000, TimeUnit.MILLISECONDS).blockingGet()); + assertTrue(done.get()); + } @Test public void invokeCompletedByCompletionMessageWithResult() { @@ -1055,7 +1528,8 @@ public void invokeCompletedByCompletionMessageWithResult() { Completable result = hubConnection.invoke("test", "message"); result.doOnComplete(() -> done.set(true)).subscribe(); - assertEquals("{\"type\":1,\"invocationId\":\"1\",\"target\":\"test\",\"arguments\":[\"message\"]}" + RECORD_SEPARATOR, mockTransport.getSentMessages()[1]); + assertEquals("{\"type\":1,\"invocationId\":\"1\",\"target\":\"test\",\"arguments\":[\"message\"]}" + RECORD_SEPARATOR, + TestUtils.byteBufferToString(mockTransport.getSentMessages()[1])); assertFalse(done.get()); mockTransport.receiveMessage("{\"type\":3,\"invocationId\":\"1\",\"result\":42}" + RECORD_SEPARATOR); @@ -1065,25 +1539,29 @@ public void invokeCompletedByCompletionMessageWithResult() { } @Test - public void completionWithResultAndErrorHandlesError() { + public void invokeCompletedByCompletionMessageWithResultWithMessagePack() { MockTransport mockTransport = new MockTransport(); - HubConnection hubConnection = TestUtils.createHubConnection("http://example.com", mockTransport); + HubConnection hubConnection = TestUtils.createHubConnection("http://example.com", mockTransport, true); hubConnection.start().timeout(1, TimeUnit.SECONDS).blockingAwait(); AtomicBoolean done = new AtomicBoolean(); Completable result = hubConnection.invoke("test", "message"); - result.doOnComplete(() -> done.set(true)).subscribe(() -> {}, (error) -> {}); + result.doOnComplete(() -> done.set(true)).subscribe(); - assertEquals("{\"type\":1,\"invocationId\":\"1\",\"target\":\"test\",\"arguments\":[\"message\"]}" + RECORD_SEPARATOR, mockTransport.getSentMessages()[1]); + byte[] firstMessageExpectedBytes = new byte[] { 0x14, (byte) 0x96, 0x01, (byte) 0x80, (byte) 0xA1, 0x31, (byte) 0xA4, 0x74, 0x65, 0x73, 0x74, + (byte) 0x91, (byte) 0xA7, 0x6D, 0x65, 0x73, 0x73, 0x61, 0x67, 0x65, (byte) 0x90 }; + assertEquals(ByteString.of(firstMessageExpectedBytes), ByteString.of(mockTransport.getSentMessages()[1])); assertFalse(done.get()); - Throwable exception = assertThrows(IllegalArgumentException.class, () -> mockTransport.receiveMessage("{\"type\":3,\"invocationId\":\"1\",\"result\":42,\"error\":\"There was an error\"}" + RECORD_SEPARATOR)); - assertEquals("Expected either 'error' or 'result' to be provided, but not both.", exception.getMessage()); + mockTransport.receiveMessage(ByteBuffer.wrap(new byte[] { 0x07, (byte) 0x95, 0x03, (byte) 0x80, (byte) 0xA1, 0x31, 0x03, 0x2A })); + + assertNull(result.timeout(1000, TimeUnit.MILLISECONDS).blockingGet()); + assertTrue(done.get()); } @Test - public void invokeNoReturnValueHandlesError() { + public void completionWithResultAndErrorHandlesError() { MockTransport mockTransport = new MockTransport(); HubConnection hubConnection = TestUtils.createHubConnection("http://example.com", mockTransport); @@ -1093,10 +1571,61 @@ public void invokeNoReturnValueHandlesError() { Completable result = hubConnection.invoke("test", "message"); result.doOnComplete(() -> done.set(true)).subscribe(() -> {}, (error) -> {}); - assertEquals("{\"type\":1,\"invocationId\":\"1\",\"target\":\"test\",\"arguments\":[\"message\"]}" + RECORD_SEPARATOR, mockTransport.getSentMessages()[1]); + assertEquals("{\"type\":1,\"invocationId\":\"1\",\"target\":\"test\",\"arguments\":[\"message\"]}" + RECORD_SEPARATOR, + TestUtils.byteBufferToString(mockTransport.getSentMessages()[1])); assertFalse(done.get()); - mockTransport.receiveMessage("{\"type\":3,\"invocationId\":\"1\",\"error\":\"There was an error\"}" + RECORD_SEPARATOR); + Throwable exception = assertThrows(IllegalArgumentException.class, () -> + mockTransport.receiveMessage("{\"type\":3,\"invocationId\":\"1\",\"result\":42,\"error\":\"There was an error\"}" + RECORD_SEPARATOR)); + assertEquals("Expected either 'error' or 'result' to be provided, but not both.", exception.getMessage()); + } + + @Test + public void invokeNoReturnValueHandlesError() { + MockTransport mockTransport = new MockTransport(); + HubConnection hubConnection = TestUtils.createHubConnection("http://example.com", mockTransport); + + hubConnection.start().timeout(1, TimeUnit.SECONDS).blockingAwait(); + + AtomicBoolean done = new AtomicBoolean(); + Completable result = hubConnection.invoke("test", "message"); + result.doOnComplete(() -> done.set(true)).subscribe(() -> {}, (error) -> {}); + + assertEquals("{\"type\":1,\"invocationId\":\"1\",\"target\":\"test\",\"arguments\":[\"message\"]}" + RECORD_SEPARATOR, + TestUtils.byteBufferToString(mockTransport.getSentMessages()[1])); + assertFalse(done.get()); + + mockTransport.receiveMessage("{\"type\":3,\"invocationId\":\"1\",\"error\":\"There was an error\"}" + RECORD_SEPARATOR); + + result.timeout(1000, TimeUnit.MILLISECONDS).blockingGet(); + + AtomicReference errorMessage = new AtomicReference<>(); + result.doOnError(error -> { + errorMessage.set(error.getMessage()); + }).subscribe(() -> {}, (error) -> {}); + + assertEquals("There was an error", errorMessage.get()); + } + + @Test + public void invokeNoReturnValueHandlesErrorWithMessagePack() { + MockTransport mockTransport = new MockTransport(); + HubConnection hubConnection = TestUtils.createHubConnection("http://example.com", mockTransport, true); + + hubConnection.start().timeout(1, TimeUnit.SECONDS).blockingAwait(); + + AtomicBoolean done = new AtomicBoolean(); + Completable result = hubConnection.invoke("test", "message"); + result.doOnComplete(() -> done.set(true)).subscribe(() -> {}, (error) -> {}); + + byte[] firstMessageExpectedBytes = new byte[] { 0x14, (byte) 0x96, 0x01, (byte) 0x80, (byte) 0xA1, 0x31, (byte) 0xA4, 0x74, 0x65, 0x73, 0x74, + (byte) 0x91, (byte) 0xA7, 0x6D, 0x65, 0x73, 0x73, 0x61, 0x67, 0x65, (byte) 0x90 }; + assertEquals(ByteString.of(firstMessageExpectedBytes), ByteString.of(mockTransport.getSentMessages()[1])); + assertFalse(done.get()); + + byte[] completionMessageErrorBytes = new byte[] { 0x19, (byte) 0x95, 0x03, (byte) 0x80, (byte) 0xA1, 0x31, 0x01, (byte) 0xB2, 0x54, 0x68, 0x65, + 0x72, 0x65, 0x20, 0x77, 0x61, 0x73, 0x20, 0x61, 0x6E, 0x20, 0x65, 0x72, 0x72, 0x6F, 0x72 }; + mockTransport.receiveMessage(ByteBuffer.wrap(completionMessageErrorBytes)); result.timeout(1000, TimeUnit.MILLISECONDS).blockingGet(); @@ -1118,7 +1647,8 @@ public void canSendNullArgInInvocation() { AtomicBoolean done = new AtomicBoolean(); Single result = hubConnection.invoke(String.class, "fixedMessage", (Object)null); result.doOnSuccess(value -> done.set(true)).subscribe(); - assertEquals("{\"type\":1,\"invocationId\":\"1\",\"target\":\"fixedMessage\",\"arguments\":[null]}" + RECORD_SEPARATOR, mockTransport.getSentMessages()[1]); + assertEquals("{\"type\":1,\"invocationId\":\"1\",\"target\":\"fixedMessage\",\"arguments\":[null]}" + RECORD_SEPARATOR, + TestUtils.byteBufferToString(mockTransport.getSentMessages()[1])); assertFalse(done.get()); mockTransport.receiveMessage("{\"type\":3,\"invocationId\":\"1\",\"result\":\"Hello World\"}" + RECORD_SEPARATOR); @@ -1126,6 +1656,30 @@ public void canSendNullArgInInvocation() { assertEquals("Hello World", result.timeout(1000, TimeUnit.MILLISECONDS).blockingGet()); assertTrue(done.get()); } + + @Test + public void canSendNullArgInInvocationWithMessagePack() { + MockTransport mockTransport = new MockTransport(); + HubConnection hubConnection = TestUtils.createHubConnection("http://example.com", mockTransport, true); + + hubConnection.start().timeout(1, TimeUnit.SECONDS).blockingAwait(); + + AtomicBoolean done = new AtomicBoolean(); + Single result = hubConnection.invoke(stringType, "fixedMessage", (Object)null); + result.doOnSuccess(value -> done.set(true)).subscribe(); + + byte[] firstMessageExpectedBytes = new byte[] { 0x15, (byte) 0x96, 0x01, (byte) 0x80, (byte) 0xA1, 0x31, (byte) 0xAC, 0x66, 0x69, 0x78, 0x65, 0x64, + 0x4D, 0x65, 0x73, 0x73, 0x61, 0x67, 0x65, (byte) 0x91, (byte) 0xC0, (byte) 0x90 }; + assertEquals(ByteString.of(firstMessageExpectedBytes), ByteString.of(mockTransport.getSentMessages()[1])); + assertFalse(done.get()); + + byte[] completionMessageBytes = new byte[] { 0x12, (byte) 0x95, 0x03, (byte) 0x80, (byte) 0xA1, 0x31, 0x03, (byte) 0xAB, 0x48, 0x65, 0x6C, 0x6C, + 0x6F, 0x20, 0x57, 0x6F, 0x72, 0x6C, 0x64 }; + mockTransport.receiveMessage(ByteBuffer.wrap(completionMessageBytes)); + + assertEquals("Hello World", result.timeout(1000, TimeUnit.MILLISECONDS).blockingGet()); + assertTrue(done.get()); + } @Test public void canSendMultipleNullArgsInInvocation() { @@ -1137,7 +1691,8 @@ public void canSendMultipleNullArgsInInvocation() { AtomicBoolean done = new AtomicBoolean(); Single result = hubConnection.invoke(String.class, "fixedMessage", null, null); result.doOnSuccess(value -> done.set(true)).subscribe(); - assertEquals("{\"type\":1,\"invocationId\":\"1\",\"target\":\"fixedMessage\",\"arguments\":[null,null]}"+ RECORD_SEPARATOR, mockTransport.getSentMessages()[1]); + assertEquals("{\"type\":1,\"invocationId\":\"1\",\"target\":\"fixedMessage\",\"arguments\":[null,null]}"+ RECORD_SEPARATOR, + TestUtils.byteBufferToString(mockTransport.getSentMessages()[1])); assertFalse(done.get()); mockTransport.receiveMessage("{\"type\":3,\"invocationId\":\"1\",\"result\":\"Hello World\"}" + RECORD_SEPARATOR); @@ -1145,6 +1700,30 @@ public void canSendMultipleNullArgsInInvocation() { assertEquals("Hello World", result.timeout(1000, TimeUnit.MILLISECONDS).blockingGet()); assertTrue(done.get()); } + + @Test + public void canSendMultipleNullArgsInInvocationWithMessagePack() { + MockTransport mockTransport = new MockTransport(); + HubConnection hubConnection = TestUtils.createHubConnection("http://example.com", mockTransport, true); + + hubConnection.start().timeout(1, TimeUnit.SECONDS).blockingAwait(); + + AtomicBoolean done = new AtomicBoolean(); + Single result = hubConnection.invoke(String.class, "fixedMessage", null, null); + result.doOnSuccess(value -> done.set(true)).subscribe(); + + byte[] firstMessageExpectedBytes = new byte[] { 0x16, (byte) 0x96, 0x01, (byte) 0x80, (byte) 0xA1, 0x31, (byte) 0xAC, 0x66, 0x69, 0x78, 0x65, 0x64, 0x4D, + 0x65, 0x73, 0x73, 0x61, 0x67, 0x65, (byte) 0x92, (byte) 0xC0, (byte) 0xC0, (byte) 0x90 }; + assertEquals(ByteString.of(firstMessageExpectedBytes), ByteString.of(mockTransport.getSentMessages()[1])); + assertFalse(done.get()); + + byte[] completionMessageBytes = new byte[] { 0x12, (byte) 0x95, 0x03, (byte) 0x80, (byte) 0xA1, 0x31, 0x03, (byte) 0xAB, 0x48, 0x65, 0x6C, 0x6C, + 0x6F, 0x20, 0x57, 0x6F, 0x72, 0x6C, 0x64 }; + mockTransport.receiveMessage(ByteBuffer.wrap(completionMessageBytes)); + + assertEquals("Hello World", result.timeout(1000, TimeUnit.MILLISECONDS).blockingGet()); + assertTrue(done.get()); + } @Test public void multipleInvokesWaitForOwnCompletionMessage() { @@ -1159,8 +1738,10 @@ public void multipleInvokesWaitForOwnCompletionMessage() { Single result2 = hubConnection.invoke(String.class, "echo", "message"); result.doOnSuccess(value -> doneFirst.set(true)).subscribe(); result2.doOnSuccess(value -> doneSecond.set(true)).subscribe(); - assertEquals("{\"type\":1,\"invocationId\":\"1\",\"target\":\"echo\",\"arguments\":[\"message\"]}" + RECORD_SEPARATOR, mockTransport.getSentMessages()[1]); - assertEquals("{\"type\":1,\"invocationId\":\"2\",\"target\":\"echo\",\"arguments\":[\"message\"]}" + RECORD_SEPARATOR, mockTransport.getSentMessages()[2]); + assertEquals("{\"type\":1,\"invocationId\":\"1\",\"target\":\"echo\",\"arguments\":[\"message\"]}" + RECORD_SEPARATOR, + TestUtils.byteBufferToString(mockTransport.getSentMessages()[1])); + assertEquals("{\"type\":1,\"invocationId\":\"2\",\"target\":\"echo\",\"arguments\":[\"message\"]}" + RECORD_SEPARATOR, + TestUtils.byteBufferToString(mockTransport.getSentMessages()[2])); assertFalse(doneFirst.get()); assertFalse(doneSecond.get()); @@ -1173,6 +1754,43 @@ public void multipleInvokesWaitForOwnCompletionMessage() { assertEquals(Integer.valueOf(42), result.timeout(1000, TimeUnit.MILLISECONDS).blockingGet()); assertTrue(doneFirst.get()); } + + @Test + public void multipleInvokesWaitForOwnCompletionMessageWithMessagePack() { + MockTransport mockTransport = new MockTransport(); + HubConnection hubConnection = TestUtils.createHubConnection("http://example.com", mockTransport, true); + + hubConnection.start().timeout(1, TimeUnit.SECONDS).blockingAwait(); + + AtomicBoolean doneFirst = new AtomicBoolean(); + AtomicBoolean doneSecond = new AtomicBoolean(); + Single result = hubConnection.invoke(integerType, "echo", "message"); + Single result2 = hubConnection.invoke(stringType, "echo", "message"); + result.doOnSuccess(value -> doneFirst.set(true)).subscribe(); + result2.doOnSuccess(value -> doneSecond.set(true)).subscribe(); + + byte[] firstMessageExpectedBytes = new byte[] { 0x14, (byte) 0x96, 0x01, (byte) 0x80, (byte) 0xA1, 0x31, (byte) 0xA4, 0x65, 0x63, 0x68, 0x6F, + (byte) 0x91, (byte) 0xA7, 0x6D, 0x65, 0x73, 0x73, 0x61, 0x67, 0x65, (byte) 0x90 }; + assertEquals(ByteString.of(firstMessageExpectedBytes), ByteString.of(mockTransport.getSentMessages()[1])); + + byte[] secondMessageExpectedBytes = new byte[] { 0x14, (byte) 0x96, 0x01, (byte) 0x80, (byte) 0xA1, 0x32, (byte) 0xA4, 0x65, 0x63, 0x68, 0x6F, + (byte) 0x91, (byte) 0xA7, 0x6D, 0x65, 0x73, 0x73, 0x61, 0x67, 0x65, (byte) 0x90 }; + assertEquals(ByteString.of(secondMessageExpectedBytes), ByteString.of(mockTransport.getSentMessages()[2])); + assertFalse(doneFirst.get()); + assertFalse(doneSecond.get()); + + byte[] firstCompletionMessageBytes = new byte[] { 0x0E, (byte) 0x95, 0x03, (byte) 0x80, (byte) 0xA1, 0x32, 0x03, (byte) 0xA7, 0x6D, 0x65, 0x73, + 0x73, 0x61, 0x67, 0x65 }; + mockTransport.receiveMessage(ByteBuffer.wrap(firstCompletionMessageBytes)); + assertEquals("message", result2.timeout(1000, TimeUnit.MILLISECONDS).blockingGet()); + assertFalse(doneFirst.get()); + assertTrue(doneSecond.get()); + + byte[] secondCompletionMessageBytes = new byte[] { 0x07, (byte) 0x95, 0x03, (byte) 0x80, (byte) 0xA1, 0x31, 0x03, 0x2A }; + mockTransport.receiveMessage(ByteBuffer.wrap(secondCompletionMessageBytes)); + assertEquals(Integer.valueOf(42), result.timeout(1000, TimeUnit.MILLISECONDS).blockingGet()); + assertTrue(doneFirst.get()); + } @Test public void invokeWorksForPrimitiveTypes() { @@ -1193,6 +1811,26 @@ public void invokeWorksForPrimitiveTypes() { assertEquals(Integer.valueOf(42), result.timeout(1000, TimeUnit.MILLISECONDS).blockingGet()); assertTrue(done.get()); } + + @Test + public void invokeWorksForPrimitiveTypesWithMessagePack() { + MockTransport mockTransport = new MockTransport(); + HubConnection hubConnection = TestUtils.createHubConnection("http://example.com", mockTransport, true); + + hubConnection.start().timeout(1, TimeUnit.SECONDS).blockingAwait(); + + AtomicBoolean done = new AtomicBoolean(); + // int.class is a primitive type and since we use Class.cast to cast an Object to the expected return type + // which does not work for primitives we have to write special logic for that case. + Single result = hubConnection.invoke(int.class, "echo", "message"); + result.doOnSuccess(value -> done.set(true)).subscribe(); + assertFalse(done.get()); + + mockTransport.receiveMessage(ByteBuffer.wrap(new byte[] { 0x07, (byte) 0x95, 0x03, (byte) 0x80, (byte) 0xA1, 0x31, 0x03, 0x2A })); + + assertEquals(Integer.valueOf(42), result.timeout(1000, TimeUnit.MILLISECONDS).blockingGet()); + assertTrue(done.get()); + } @Test public void completionMessageCanHaveError() { @@ -1218,6 +1856,33 @@ public void completionMessageCanHaveError() { assertEquals("There was an error", exceptionMessage); } + + @Test + public void completionMessageCanHaveErrorWithMessagePack() { + MockTransport mockTransport = new MockTransport(); + HubConnection hubConnection = TestUtils.createHubConnection("http://example.com", mockTransport, true); + + hubConnection.start().timeout(1, TimeUnit.SECONDS).blockingAwait(); + + AtomicBoolean done = new AtomicBoolean(); + Single result = hubConnection.invoke(int.class, "echo", "message"); + result.doOnSuccess(value -> done.set(true)); + assertFalse(done.get()); + + byte[] completionMessageErrorBytes = new byte[] { 0x19, (byte) 0x95, 0x03, (byte) 0x80, (byte) 0xA1, 0x31, 0x01, (byte) 0xB2, 0x54, 0x68, 0x65, + 0x72, 0x65, 0x20, 0x77, 0x61, 0x73, 0x20, 0x61, 0x6E, 0x20, 0x65, 0x72, 0x72, 0x6F, 0x72 }; + mockTransport.receiveMessage(ByteBuffer.wrap(completionMessageErrorBytes)); + + String exceptionMessage = null; + try { + result.timeout(1000, TimeUnit.MILLISECONDS).blockingGet(); + assertFalse(true); + } catch (HubException ex) { + exceptionMessage = ex.getMessage(); + } + + assertEquals("There was an error", exceptionMessage); + } @Test public void stopCancelsActiveInvokes() { @@ -1533,6 +2198,315 @@ public void sendWithEightParamsTriggersOnHandler() { assertEquals("E", value7.get()); assertEquals("F", value8.get()); } + + @Test + public void sendWithNoParamsTriggersOnHandlerWithMessagePack() { + AtomicReference value = new AtomicReference<>(0); + MockTransport mockTransport = new MockTransport(); + HubConnection hubConnection = TestUtils.createHubConnection("http://example.com", mockTransport, true); + + hubConnection.on("inc", () ->{ + assertEquals(Integer.valueOf(0), value.get()); + value.getAndUpdate((val) -> val + 1); + }); + + hubConnection.start().timeout(1, TimeUnit.SECONDS).blockingAwait(); + byte[] messageBytes = new byte[] { 0x0A, (byte) 0x96, 0x01, (byte) 0x80, (byte) 0xC0, (byte) 0xA3, 0x69, 0x6E, 0x63, (byte) 0x90, (byte) 0x90 }; + mockTransport.receiveMessage(ByteBuffer.wrap(messageBytes)); + + // Confirming that our handler was called and that the counter property was incremented. + assertEquals(Integer.valueOf(1), value.get()); + } + + @Test + public void sendWithParamTriggersOnHandlerWithMessagePack() { + AtomicReference value = new AtomicReference<>(); + MockTransport mockTransport = new MockTransport(); + HubConnection hubConnection = TestUtils.createHubConnection("http://example.com", mockTransport, true); + + hubConnection.on("inc", (param) ->{ + assertNull(value.get()); + value.set(param); + }, stringType); + + hubConnection.start().timeout(1, TimeUnit.SECONDS).blockingAwait(); + byte[] messageBytes = new byte[] { 0x0C, (byte) 0x96, 0x01, (byte) 0x80, (byte) 0xC0, (byte) 0xA3, 0x69, 0x6E, 0x63, (byte) 0x91, (byte) 0xA1, + 0x41, (byte) 0x90 }; + mockTransport.receiveMessage(ByteBuffer.wrap(messageBytes)); + hubConnection.send("inc", "A"); + + // Confirming that our handler was called and the correct message was passed in. + assertEquals("A", value.get()); + } + + @Test + public void sendWithTwoParamsTriggersOnHandlerWithMessagePack() { + AtomicReference value1 = new AtomicReference<>(); + AtomicReference value2 = new AtomicReference<>(); + + MockTransport mockTransport = new MockTransport(); + HubConnection hubConnection = TestUtils.createHubConnection("http://example.com", mockTransport, true); + + hubConnection.on("inc", (param1, param2) ->{ + assertNull(value1.get()); + assertNull((value2.get())); + + value1.set(param1); + value2.set(param2); + }, stringType, doubleType); + + hubConnection.start().timeout(1, TimeUnit.SECONDS).blockingAwait(); + byte[] messageBytes = new byte[] { 0x15, (byte) 0x96, 0x01, (byte) 0x80, (byte) 0xC0, (byte) 0xA3, 0x69, 0x6E, 0x63, (byte) 0x92, (byte) 0xA1, 0x41, + (byte) 0xCB, 0x40, 0x28, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, (byte) 0x90 }; + mockTransport.receiveMessage(ByteBuffer.wrap(messageBytes)); + hubConnection.send("inc", "A", 12); + + // Confirming that our handler was called and the correct message was passed in. + assertEquals("A", value1.get()); + assertEquals(Double.valueOf(12), value2.get()); + } + + @Test + public void sendWithThreeParamsTriggersOnHandlerWithMessagePack() { + AtomicReference value1 = new AtomicReference<>(); + AtomicReference value2 = new AtomicReference<>(); + AtomicReference value3 = new AtomicReference<>(); + + MockTransport mockTransport = new MockTransport(); + HubConnection hubConnection = TestUtils.createHubConnection("http://example.com", mockTransport, true); + + hubConnection.on("inc", (param1, param2, param3) ->{ + assertNull(value1.get()); + assertNull(value2.get()); + assertNull(value3.get()); + + value1.set(param1); + value2.set(param2); + value3.set(param3); + }, stringType, stringType, stringType); + + hubConnection.start().timeout(1, TimeUnit.SECONDS).blockingAwait(); + byte[] messageBytes = new byte[] { 0x10, (byte) 0x96, 0x01, (byte) 0x80, (byte) 0xC0, (byte) 0xA3, 0x69, 0x6E, 0x63, (byte) 0x93, (byte) 0xA1, 0x41, + (byte) 0xA1, 0x42, (byte) 0xA1, 0x43, (byte) 0x90 }; + mockTransport.receiveMessage(ByteBuffer.wrap(messageBytes)); + hubConnection.send("inc", "A", "B", "C"); + + // Confirming that our handler was called and the correct message was passed in. + assertEquals("A", value1.get()); + assertEquals("B", value2.get()); + assertEquals("C", value3.get()); + } + + @Test + public void sendWithFourParamsTriggersOnHandlerWithMessagePack() { + AtomicReference value1 = new AtomicReference<>(); + AtomicReference value2 = new AtomicReference<>(); + AtomicReference value3 = new AtomicReference<>(); + AtomicReference value4 = new AtomicReference<>(); + + MockTransport mockTransport = new MockTransport(); + HubConnection hubConnection = TestUtils.createHubConnection("http://example.com", mockTransport, true); + + hubConnection.on("inc", (param1, param2, param3, param4) ->{ + assertNull(value1.get()); + assertNull(value2.get()); + assertNull(value3.get()); + assertNull(value4.get()); + + value1.set(param1); + value2.set(param2); + value3.set(param3); + value4.set(param4); + }, stringType, stringType, stringType, stringType); + + hubConnection.start().timeout(1, TimeUnit.SECONDS).blockingAwait(); + byte[] messageBytes = new byte[] { 0x12, (byte) 0x96, 0x01, (byte) 0x80, (byte) 0xC0, (byte) 0xA3, 0x69, 0x6E, 0x63, (byte) 0x94, (byte) 0xA1, 0x41, + (byte) 0xA1, 0x42, (byte) 0xA1, 0x43, (byte) 0xA1, 0x44, (byte) 0x90 }; + mockTransport.receiveMessage(ByteBuffer.wrap(messageBytes)); + + // Confirming that our handler was called and the correct message was passed in. + assertEquals("A", value1.get()); + assertEquals("B", value2.get()); + assertEquals("C", value3.get()); + assertEquals("D", value4.get()); + } + + @Test + public void sendWithFiveParamsTriggersOnHandlerWithMessagePack() { + AtomicReference value1 = new AtomicReference<>(); + AtomicReference value2 = new AtomicReference<>(); + AtomicReference value3 = new AtomicReference<>(); + AtomicReference value4 = new AtomicReference<>(); + AtomicReference value5 = new AtomicReference<>(); + + MockTransport mockTransport = new MockTransport(); + HubConnection hubConnection = TestUtils.createHubConnection("http://example.com", mockTransport, true); + + hubConnection.on("inc", (param1, param2, param3, param4, param5) ->{ + assertNull(value1.get()); + assertNull(value2.get()); + assertNull(value3.get()); + assertNull(value4.get()); + assertNull(value5.get()); + + value1.set(param1); + value2.set(param2); + value3.set(param3); + value4.set(param4); + value5.set(param5); + }, stringType, stringType, stringType, booleanType, doubleType); + + hubConnection.start().timeout(1, TimeUnit.SECONDS).blockingAwait(); + byte[] messageBytes = new byte[] { 0x1A, (byte) 0x96, 0x01, (byte) 0x80, (byte) 0xC0, (byte) 0xA3, 0x69, 0x6E, 0x63, (byte) 0x95, (byte) 0xA1, 0x41, + (byte) 0xA1, 0x42, (byte) 0xA1, 0x43, (byte) 0xC3, (byte) 0xCB, 0x40, 0x28, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, (byte) 0x90 }; + mockTransport.receiveMessage(ByteBuffer.wrap(messageBytes)); + + // Confirming that our handler was called and the correct message was passed in. + assertEquals("A", value1.get()); + assertEquals("B", value2.get()); + assertEquals("C", value3.get()); + assertTrue(value4.get()); + assertEquals(Double.valueOf(12), value5.get()); + } + + @Test + public void sendWithSixParamsTriggersOnHandlerWithMessagePack() { + AtomicReference value1 = new AtomicReference<>(); + AtomicReference value2 = new AtomicReference<>(); + AtomicReference value3 = new AtomicReference<>(); + AtomicReference value4 = new AtomicReference<>(); + AtomicReference value5 = new AtomicReference<>(); + AtomicReference value6 = new AtomicReference<>(); + + MockTransport mockTransport = new MockTransport(); + HubConnection hubConnection = TestUtils.createHubConnection("http://example.com", mockTransport, true); + + hubConnection.on("inc", (param1, param2, param3, param4, param5, param6) -> { + assertNull(value1.get()); + assertNull(value2.get()); + assertNull(value3.get()); + assertNull(value4.get()); + assertNull(value5.get()); + assertNull(value6.get()); + + value1.set(param1); + value2.set(param2); + value3.set(param3); + value4.set(param4); + value5.set(param5); + value6.set(param6); + }, stringType, stringType, stringType, booleanType, doubleType, stringType); + + hubConnection.start().timeout(1, TimeUnit.SECONDS).blockingAwait(); + byte[] messageBytes = new byte[] { 0x1C, (byte) 0x96, 0x01, (byte) 0x80, (byte) 0xC0, (byte) 0xA3, 0x69, 0x6E, 0x63, (byte) 0x96, (byte) 0xA1, 0x41, + (byte) 0xA1, 0x42, (byte) 0xA1, 0x43, (byte) 0xC3, (byte) 0xCB, 0x40, 0x28, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, (byte) 0xA1, 0x44, (byte) 0x90 }; + mockTransport.receiveMessage(ByteBuffer.wrap(messageBytes)); + + // Confirming that our handler was called and the correct message was passed in. + assertEquals("A", value1.get()); + assertEquals("B", value2.get()); + assertEquals("C", value3.get()); + assertTrue(value4.get()); + assertEquals(Double.valueOf(12), value5.get()); + assertEquals("D", value6.get()); + } + + @Test + public void sendWithSevenParamsTriggersOnHandlerWithMessagePack() { + AtomicReference value1 = new AtomicReference<>(); + AtomicReference value2 = new AtomicReference<>(); + AtomicReference value3 = new AtomicReference<>(); + AtomicReference value4 = new AtomicReference<>(); + AtomicReference value5 = new AtomicReference<>(); + AtomicReference value6 = new AtomicReference<>(); + AtomicReference value7 = new AtomicReference<>(); + + MockTransport mockTransport = new MockTransport(); + HubConnection hubConnection = TestUtils.createHubConnection("http://example.com", mockTransport, true); + + hubConnection.on("inc", (param1, param2, param3, param4, param5, param6, param7) -> { + assertNull(value1.get()); + assertNull(value2.get()); + assertNull(value3.get()); + assertNull(value4.get()); + assertNull(value5.get()); + assertNull(value6.get()); + assertNull(value7.get()); + + value1.set(param1); + value2.set(param2); + value3.set(param3); + value4.set(param4); + value5.set(param5); + value6.set(param6); + value7.set(param7); + }, stringType, stringType, stringType, booleanType, doubleType, stringType, stringType); + + hubConnection.start().timeout(1, TimeUnit.SECONDS).blockingAwait(); + byte[] messageBytes = new byte[] { 0x1E, (byte) 0x96, 0x01, (byte) 0x80, (byte) 0xC0, (byte) 0xA3, 0x69, 0x6E, 0x63, (byte) 0x97, (byte) 0xA1, 0x41, + (byte) 0xA1, 0x42, (byte) 0xA1, 0x43, (byte) 0xC3, (byte) 0xCB, 0x40, 0x28, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, (byte) 0xA1, 0x44, (byte) 0xA1, + 0x45, (byte) 0x90 }; + mockTransport.receiveMessage(ByteBuffer.wrap(messageBytes)); + + // Confirming that our handler was called and the correct message was passed in. + assertEquals("A", value1.get()); + assertEquals("B", value2.get()); + assertEquals("C", value3.get()); + assertTrue(value4.get()); + assertEquals(Double.valueOf(12), value5.get()); + assertEquals("D", value6.get()); + assertEquals("E", value7.get()); + } + + @Test + public void sendWithEightParamsTriggersOnHandlerWithMessagePack() { + AtomicReference value1 = new AtomicReference<>(); + AtomicReference value2 = new AtomicReference<>(); + AtomicReference value3 = new AtomicReference<>(); + AtomicReference value4 = new AtomicReference<>(); + AtomicReference value5 = new AtomicReference<>(); + AtomicReference value6 = new AtomicReference<>(); + AtomicReference value7 = new AtomicReference<>(); + AtomicReference value8 = new AtomicReference<>(); + + MockTransport mockTransport = new MockTransport(); + HubConnection hubConnection = TestUtils.createHubConnection("http://example.com", mockTransport, true); + + hubConnection.on("inc", (param1, param2, param3, param4, param5, param6, param7, param8) -> { + assertNull(value1.get()); + assertNull(value2.get()); + assertNull(value3.get()); + assertNull(value4.get()); + assertNull(value5.get()); + assertNull(value6.get()); + assertNull(value7.get()); + assertNull(value8.get()); + + value1.set(param1); + value2.set(param2); + value3.set(param3); + value4.set(param4); + value5.set(param5); + value6.set(param6); + value7.set(param7); + value8.set(param8); + }, stringType, stringType, stringType, booleanType, doubleType, stringType, stringType, stringType); + + hubConnection.start().timeout(1, TimeUnit.SECONDS).blockingAwait(); + byte[] messageBytes = new byte[] { 0x20, (byte) 0x96, 0x01, (byte) 0x80, (byte) 0xC0, (byte) 0xA3, 0x69, 0x6E, 0x63, (byte) 0x98, (byte) 0xA1, 0x41, + (byte) 0xA1, 0x42, (byte) 0xA1, 0x43, (byte) 0xC3, (byte) 0xCB, 0x40, 0x28, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, (byte) 0xA1, 0x44, (byte) 0xA1, + 0x45, (byte) 0xA1, 0x46, (byte) 0x90 }; + mockTransport.receiveMessage(ByteBuffer.wrap(messageBytes)); + // Confirming that our handler was called and the correct message was passed in. + assertEquals("A", value1.get()); + assertEquals("B", value2.get()); + assertEquals("C", value3.get()); + assertTrue(value4.get()); + assertEquals(Double.valueOf(12), value5.get()); + assertEquals("D", value6.get()); + assertEquals("E", value7.get()); + assertEquals("F", value8.get()); + } private class Custom { public int number; @@ -1564,6 +2538,33 @@ public void sendWithCustomObjectTriggersOnHandler() { assertEquals(true, custom.bools[0]); assertEquals(false, custom.bools[1]); } + + @Test + public void sendWithCustomObjectTriggersOnHandlerWithMessagePack() { + AtomicReference> value1 = new AtomicReference<>(); + + MockTransport mockTransport = new MockTransport(); + HubConnection hubConnection = TestUtils.createHubConnection("http://example.com", mockTransport, true); + + hubConnection.>on("inc", (param1) -> { + assertNull(value1.get()); + + value1.set(param1); + }, (new TypeReference>() { }).getType()); + + hubConnection.start().timeout(1, TimeUnit.SECONDS).blockingAwait(); + byte[] messageBytes = new byte[] { 0x2F, (byte) 0x96, 0x01, (byte) 0x80, (byte) 0xC0, (byte) 0xA3, 0x69, 0x6E, 0x63, (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, 0x05, (byte) 0x90 }; + mockTransport.receiveMessage(ByteBuffer.wrap(messageBytes)); + + // Confirming that our handler was called and the correct message was passed in. + PersonPojo person = value1.get(); + assertEquals("John", person.getFirstName()); + assertEquals("Doe", person.getLastName()); + assertEquals(30, person.getAge()); + assertEquals((short) 5, (short) person.getT()); + } @Test public void receiveHandshakeResponseAndMessage() { @@ -1580,7 +2581,7 @@ public void receiveHandshakeResponseAndMessage() { hubConnection.start(); mockTransport.getStartTask().timeout(1, TimeUnit.SECONDS).blockingAwait(); String expectedSentMessage = "{\"protocol\":\"json\",\"version\":1}" + RECORD_SEPARATOR; - assertEquals(expectedSentMessage, mockTransport.getSentMessages()[0]); + assertEquals(expectedSentMessage, TestUtils.byteBufferToString(mockTransport.getSentMessages()[0])); mockTransport.receiveMessage("{}" + RECORD_SEPARATOR + "{\"type\":1,\"target\":\"inc\",\"arguments\":[]}" + RECORD_SEPARATOR); @@ -1702,7 +2703,7 @@ public void doesNotErrorWhenReceivingInvokeWithIncorrectArgumentLength() { @Test public void negotiateSentOnStart() { TestHttpClient client = new TestHttpClient() - .on("POST", (req) -> Single.just(new HttpResponse(404, "", ""))); + .on("POST", (req) -> Single.just(new HttpResponse(404, "", TestUtils.emptyByteBuffer))); HubConnection hubConnection = HubConnectionBuilder .create("http://example.com") @@ -1720,7 +2721,7 @@ public void negotiateSentOnStart() { @Test public void negotiateThatRedirectsForeverFailsAfter100Tries() { TestHttpClient client = new TestHttpClient().on("POST", "http://example.com/negotiate?negotiateVersion=1", - (req) -> Single.just(new HttpResponse(200, "", "{\"url\":\"http://example.com\"}"))); + (req) -> Single.just(new HttpResponse(200, "", TestUtils.stringToByteBuffer("{\"url\":\"http://example.com\"}")))); HubConnection hubConnection = HubConnectionBuilder .create("http://example.com") @@ -1754,8 +2755,8 @@ public void noConnectionIdWhenSkippingNegotiate() { public void connectionIdIsAvailableAfterStart() { TestHttpClient client = new TestHttpClient().on("POST", "http://example.com/negotiate?negotiateVersion=1", (req) -> Single.just(new HttpResponse(200, "", - "{\"connectionId\":\"bVOiRPG8-6YiJ6d7ZcTOVQ\",\"" - + "availableTransports\":[{\"transport\":\"WebSockets\",\"transferFormats\":[\"Text\",\"Binary\"]}]}"))); + TestUtils.stringToByteBuffer("{\"connectionId\":\"bVOiRPG8-6YiJ6d7ZcTOVQ\",\"" + + "availableTransports\":[{\"transport\":\"WebSockets\",\"transferFormats\":[\"Text\",\"Binary\"]}]}")))); MockTransport transport = new MockTransport(true); HubConnection hubConnection = HubConnectionBuilder @@ -1779,10 +2780,10 @@ public void connectionIdIsAvailableAfterStart() { public void connectionTokenAppearsInQSConnectionIdIsOnConnectionInstance() { TestHttpClient client = new TestHttpClient().on("POST", "http://example.com/negotiate?negotiateVersion=1", (req) -> Single.just(new HttpResponse(200, "", - "{\"connectionId\":\"bVOiRPG8-6YiJ6d7ZcTOVQ\"," + + TestUtils.stringToByteBuffer("{\"connectionId\":\"bVOiRPG8-6YiJ6d7ZcTOVQ\"," + "\"negotiateVersion\": 1," + "\"connectionToken\":\"connection-token-value\"," + - "\"availableTransports\":[{\"transport\":\"WebSockets\",\"transferFormats\":[\"Text\",\"Binary\"]}]}"))); + "\"availableTransports\":[{\"transport\":\"WebSockets\",\"transferFormats\":[\"Text\",\"Binary\"]}]}")))); MockTransport transport = new MockTransport(true); HubConnection hubConnection = HubConnectionBuilder @@ -1806,9 +2807,9 @@ public void connectionTokenAppearsInQSConnectionIdIsOnConnectionInstance() { public void connectionTokenIsIgnoredIfNegotiateVersionIsNotPresentInNegotiateResponse() { TestHttpClient client = new TestHttpClient().on("POST", "http://example.com/negotiate?negotiateVersion=1", (req) -> Single.just(new HttpResponse(200, "", - "{\"connectionId\":\"bVOiRPG8-6YiJ6d7ZcTOVQ\"," + + TestUtils.stringToByteBuffer("{\"connectionId\":\"bVOiRPG8-6YiJ6d7ZcTOVQ\"," + "\"connectionToken\":\"connection-token-value\"," + - "\"availableTransports\":[{\"transport\":\"WebSockets\",\"transferFormats\":[\"Text\",\"Binary\"]}]}"))); + "\"availableTransports\":[{\"transport\":\"WebSockets\",\"transferFormats\":[\"Text\",\"Binary\"]}]}")))); MockTransport transport = new MockTransport(true); HubConnection hubConnection = HubConnectionBuilder @@ -1832,9 +2833,9 @@ public void connectionTokenIsIgnoredIfNegotiateVersionIsNotPresentInNegotiateRes public void negotiateVersionIsNotAddedIfAlreadyPresent() { TestHttpClient client = new TestHttpClient().on("POST", "http://example.com/negotiate?negotiateVersion=42", (req) -> Single.just(new HttpResponse(200, "", - "{\"connectionId\":\"bVOiRPG8-6YiJ6d7ZcTOVQ\"," + + TestUtils.stringToByteBuffer("{\"connectionId\":\"bVOiRPG8-6YiJ6d7ZcTOVQ\"," + "\"connectionToken\":\"connection-token-value\"," + - "\"availableTransports\":[{\"transport\":\"WebSockets\",\"transferFormats\":[\"Text\",\"Binary\"]}]}"))); + "\"availableTransports\":[{\"transport\":\"WebSockets\",\"transferFormats\":[\"Text\",\"Binary\"]}]}")))); MockTransport transport = new MockTransport(true); HubConnection hubConnection = HubConnectionBuilder @@ -1858,8 +2859,8 @@ public void negotiateVersionIsNotAddedIfAlreadyPresent() { public void afterSuccessfulNegotiateConnectsWithWebsocketsTransport() { TestHttpClient client = new TestHttpClient().on("POST", "http://example.com/negotiate?negotiateVersion=1", (req) -> Single.just(new HttpResponse(200, "", - "{\"connectionId\":\"bVOiRPG8-6YiJ6d7ZcTOVQ\",\"" - + "availableTransports\":[{\"transport\":\"WebSockets\",\"transferFormats\":[\"Text\",\"Binary\"]}]}"))); + TestUtils.stringToByteBuffer("{\"connectionId\":\"bVOiRPG8-6YiJ6d7ZcTOVQ\",\"" + + "availableTransports\":[{\"transport\":\"WebSockets\",\"transferFormats\":[\"Text\",\"Binary\"]}]}")))); MockTransport transport = new MockTransport(true); HubConnection hubConnection = HubConnectionBuilder @@ -1870,17 +2871,17 @@ public void afterSuccessfulNegotiateConnectsWithWebsocketsTransport() { hubConnection.start().timeout(1, TimeUnit.SECONDS).blockingAwait(); - String[] sentMessages = transport.getSentMessages(); + ByteBuffer[] sentMessages = transport.getSentMessages(); assertEquals(1, sentMessages.length); - assertEquals("{\"protocol\":\"json\",\"version\":1}" + RECORD_SEPARATOR, sentMessages[0]); + assertEquals("{\"protocol\":\"json\",\"version\":1}" + RECORD_SEPARATOR, TestUtils.byteBufferToString(sentMessages[0])); } @Test public void afterSuccessfulNegotiateConnectsWithLongPollingTransport() { TestHttpClient client = new TestHttpClient().on("POST", "http://example.com/negotiate?negotiateVersion=1", (req) -> Single.just(new HttpResponse(200, "", - "{\"connectionId\":\"bVOiRPG8-6YiJ6d7ZcTOVQ\",\"" - + "availableTransports\":[{\"transport\":\"LongPolling\",\"transferFormats\":[\"Text\",\"Binary\"]}]}"))); + TestUtils.stringToByteBuffer("{\"connectionId\":\"bVOiRPG8-6YiJ6d7ZcTOVQ\",\"" + + "availableTransports\":[{\"transport\":\"LongPolling\",\"transferFormats\":[\"Text\",\"Binary\"]}]}")))); MockTransport transport = new MockTransport(true); HubConnection hubConnection = HubConnectionBuilder @@ -1891,9 +2892,9 @@ public void afterSuccessfulNegotiateConnectsWithLongPollingTransport() { hubConnection.start().timeout(1, TimeUnit.SECONDS).blockingAwait(); - String[] sentMessages = transport.getSentMessages(); + ByteBuffer[] sentMessages = transport.getSentMessages(); assertEquals(1, sentMessages.length); - assertEquals("{\"protocol\":\"json\",\"version\":1}" + RECORD_SEPARATOR, sentMessages[0]); + assertEquals("{\"protocol\":\"json\",\"version\":1}" + RECORD_SEPARATOR, TestUtils.byteBufferToString(sentMessages[0])); } @Test @@ -1903,15 +2904,15 @@ public void TransportAllUsesLongPollingWhenServerOnlySupportLongPolling() { TestHttpClient client = new TestHttpClient() .on("POST", (req) -> Single.just(new HttpResponse(200, "", - "{\"connectionId\":\"bVOiRPG8-6YiJ6d7ZcTOVQ\",\"" - + "availableTransports\":[{\"transport\":\"LongPolling\",\"transferFormats\":[\"Text\",\"Binary\"]}]}"))) + TestUtils.stringToByteBuffer("{\"connectionId\":\"bVOiRPG8-6YiJ6d7ZcTOVQ\",\"" + + "availableTransports\":[{\"transport\":\"LongPolling\",\"transferFormats\":[\"Text\",\"Binary\"]}]}")))) .on("GET", (req) -> { if (requestCount.get() < 2) { requestCount.incrementAndGet(); - return Single.just(new HttpResponse(200, "", "{}" + RECORD_SEPARATOR)); + return Single.just(new HttpResponse(200, "", TestUtils.stringToByteBuffer("{}" + RECORD_SEPARATOR))); } assertTrue(close.blockingAwait(5, TimeUnit.SECONDS)); - return Single.just(new HttpResponse(204, "", "")); + return Single.just(new HttpResponse(204, "", TestUtils.emptyByteBuffer)); }); HubConnection hubConnection = HubConnectionBuilder @@ -1930,10 +2931,10 @@ public void TransportAllUsesLongPollingWhenServerOnlySupportLongPolling() { public void ClientThatSelectsWebsocketsThrowsWhenWebsocketsAreNotAvailable() { TestHttpClient client = new TestHttpClient().on("POST", (req) -> Single.just(new HttpResponse(200, "", - "{\"connectionId\":\"bVOiRPG8-6YiJ6d7ZcTOVQ\",\"" - + "availableTransports\":[{\"transport\":\"LongPolling\",\"transferFormats\":[\"Text\",\"Binary\"]}]}"))) + TestUtils.stringToByteBuffer("{\"connectionId\":\"bVOiRPG8-6YiJ6d7ZcTOVQ\",\"" + + "availableTransports\":[{\"transport\":\"LongPolling\",\"transferFormats\":[\"Text\",\"Binary\"]}]}")))) .on("GET", (req) -> { - return Single.just(new HttpResponse(204, "", "")); + return Single.just(new HttpResponse(204, "", TestUtils.emptyByteBuffer)); }); HubConnection hubConnection = HubConnectionBuilder @@ -1952,8 +2953,8 @@ public void ClientThatSelectsWebsocketsThrowsWhenWebsocketsAreNotAvailable() { @Test public void ClientThatSelectsLongPollingThrowsWhenLongPollingIsNotAvailable() { TestHttpClient client = new TestHttpClient().on("POST", - (req) -> Single.just(new HttpResponse(200, "", "{\"connectionId\":\"bVOiRPG8-6YiJ6d7ZcTOVQ\",\"" - + "availableTransports\":[{\"transport\":\"WebSockets\",\"transferFormats\":[\"Text\",\"Binary\"]}]}"))); + (req) -> Single.just(new HttpResponse(200, "", TestUtils.stringToByteBuffer("{\"connectionId\":\"bVOiRPG8-6YiJ6d7ZcTOVQ\",\"" + + "availableTransports\":[{\"transport\":\"WebSockets\",\"transferFormats\":[\"Text\",\"Binary\"]}]}")))); HubConnection hubConnection = HubConnectionBuilder .create("http://example.com") @@ -1972,8 +2973,8 @@ public void ClientThatSelectsLongPollingThrowsWhenLongPollingIsNotAvailable() { public void receivingServerSentEventsTransportFromNegotiateFails() { TestHttpClient client = new TestHttpClient().on("POST", "http://example.com/negotiate?negotiateVersion=1", (req) -> Single.just(new HttpResponse(200, "", - "{\"connectionId\":\"bVOiRPG8-6YiJ6d7ZcTOVQ\",\"" - + "availableTransports\":[{\"transport\":\"ServerSentEvents\",\"transferFormats\":[\"Text\"]}]}"))); + TestUtils.stringToByteBuffer("{\"connectionId\":\"bVOiRPG8-6YiJ6d7ZcTOVQ\",\"" + + "availableTransports\":[{\"transport\":\"ServerSentEvents\",\"transferFormats\":[\"Text\"]}]}")))); MockTransport transport = new MockTransport(true); HubConnection hubConnection = HubConnectionBuilder @@ -1991,7 +2992,7 @@ public void receivingServerSentEventsTransportFromNegotiateFails() { @Test public void negotiateThatReturnsErrorThrowsFromStart() { TestHttpClient client = new TestHttpClient().on("POST", "http://example.com/negotiate?negotiateVersion=1", - (req) -> Single.just(new HttpResponse(200, "", "{\"error\":\"Test error.\"}"))); + (req) -> Single.just(new HttpResponse(200, "", TestUtils.stringToByteBuffer("{\"error\":\"Test error.\"}")))); MockTransport transport = new MockTransport(true); HubConnection hubConnection = HubConnectionBuilder @@ -2008,7 +3009,7 @@ public void negotiateThatReturnsErrorThrowsFromStart() { @Test public void DetectWhenTryingToConnectToClassicSignalRServer() { TestHttpClient client = new TestHttpClient().on("POST", "http://example.com/negotiate?negotiateVersion=1", - (req) -> Single.just(new HttpResponse(200, "", "{\"Url\":\"/signalr\"," + + (req) -> Single.just(new HttpResponse(200, "", TestUtils.stringToByteBuffer("{\"Url\":\"/signalr\"," + "\"ConnectionToken\":\"X97dw3uxW4NPPggQsYVcNcyQcuz4w2\"," + "\"ConnectionId\":\"05265228-1e2c-46c5-82a1-6a5bcc3f0143\"," + "\"KeepAliveTimeout\":10.0," + @@ -2016,7 +3017,7 @@ public void DetectWhenTryingToConnectToClassicSignalRServer() { "\"TryWebSockets\":true," + "\"ProtocolVersion\":\"1.5\"," + "\"TransportConnectTimeout\":30.0," + - "\"LongPollDelay\":0.0}"))); + "\"LongPollDelay\":0.0}")))); MockTransport transport = new MockTransport(true); HubConnection hubConnection = HubConnectionBuilder @@ -2034,10 +3035,10 @@ public void DetectWhenTryingToConnectToClassicSignalRServer() { @Test public void negotiateRedirectIsFollowed() { TestHttpClient client = new TestHttpClient().on("POST", "http://example.com/negotiate?negotiateVersion=1", - (req) -> Single.just(new HttpResponse(200, "", "{\"url\":\"http://testexample.com/\"}"))) + (req) -> Single.just(new HttpResponse(200, "", TestUtils.stringToByteBuffer("{\"url\":\"http://testexample.com/\"}")))) .on("POST", "http://testexample.com/negotiate?negotiateVersion=1", - (req) -> Single.just(new HttpResponse(200, "", "{\"connectionId\":\"bVOiRPG8-6YiJ6d7ZcTOVQ\",\"" - + "availableTransports\":[{\"transport\":\"WebSockets\",\"transferFormats\":[\"Text\",\"Binary\"]}]}"))); + (req) -> Single.just(new HttpResponse(200, "", TestUtils.stringToByteBuffer("{\"connectionId\":\"bVOiRPG8-6YiJ6d7ZcTOVQ\",\"" + + "availableTransports\":[{\"transport\":\"WebSockets\",\"transferFormats\":[\"Text\",\"Binary\"]}]}")))); MockTransport transport = new MockTransport(true); HubConnection hubConnection = HubConnectionBuilder @@ -2059,12 +3060,12 @@ public void accessTokenProviderReferenceIsKeptAfterNegotiateRedirect() { TestHttpClient client = new TestHttpClient() .on("POST", "http://example.com/negotiate?negotiateVersion=1", (req) -> { beforeRedirectToken.set(req.getHeaders().get("Authorization")); - return Single.just(new HttpResponse(200, "", "{\"url\":\"http://testexample.com/\",\"accessToken\":\"newToken\"}")); + return Single.just(new HttpResponse(200, "", TestUtils.stringToByteBuffer("{\"url\":\"http://testexample.com/\",\"accessToken\":\"newToken\"}"))); }) .on("POST", "http://testexample.com/negotiate?negotiateVersion=1", (req) -> { token.set(req.getHeaders().get("Authorization")); - return Single.just(new HttpResponse(200, "", "{\"connectionId\":\"bVOiRPG8-6YiJ6d7ZcTOVQ\",\"" - + "availableTransports\":[{\"transport\":\"WebSockets\",\"transferFormats\":[\"Text\",\"Binary\"]}]}")); + return Single.just(new HttpResponse(200, "", TestUtils.stringToByteBuffer("{\"connectionId\":\"bVOiRPG8-6YiJ6d7ZcTOVQ\",\"" + + "availableTransports\":[{\"transport\":\"WebSockets\",\"transferFormats\":[\"Text\",\"Binary\"]}]}"))); }); MockTransport transport = new MockTransport(true); @@ -2100,8 +3101,8 @@ public void accessTokenProviderIsUsedForNegotiate() { .on("POST", "http://example.com/negotiate?negotiateVersion=1", (req) -> { token.set(req.getHeaders().get("Authorization")); - return Single.just(new HttpResponse(200, "", "{\"connectionId\":\"bVOiRPG8-6YiJ6d7ZcTOVQ\",\"" - + "availableTransports\":[{\"transport\":\"WebSockets\",\"transferFormats\":[\"Text\",\"Binary\"]}]}")); + return Single.just(new HttpResponse(200, "", TestUtils.stringToByteBuffer("{\"connectionId\":\"bVOiRPG8-6YiJ6d7ZcTOVQ\",\"" + + "availableTransports\":[{\"transport\":\"WebSockets\",\"transferFormats\":[\"Text\",\"Binary\"]}]}"))); }); MockTransport transport = new MockTransport(true); @@ -2125,8 +3126,8 @@ public void AccessTokenProviderCanProvideDifferentValues() { .on("POST", "http://example.com/negotiate?negotiateVersion=1", (req) -> { token.set(req.getHeaders().get("Authorization")); - return Single.just(new HttpResponse(200, "", "{\"connectionId\":\"bVOiRPG8-6YiJ6d7ZcTOVQ\",\"" - + "availableTransports\":[{\"transport\":\"WebSockets\",\"transferFormats\":[\"Text\",\"Binary\"]}]}")); + return Single.just(new HttpResponse(200, "", TestUtils.stringToByteBuffer("{\"connectionId\":\"bVOiRPG8-6YiJ6d7ZcTOVQ\",\"" + + "availableTransports\":[{\"transport\":\"WebSockets\",\"transferFormats\":[\"Text\",\"Binary\"]}]}"))); }); AtomicInteger i = new AtomicInteger(0); @@ -2153,13 +3154,14 @@ public void AccessTokenProviderCanProvideDifferentValues() { public void accessTokenProviderIsOverriddenFromRedirectNegotiate() { AtomicReference token = new AtomicReference<>(); TestHttpClient client = new TestHttpClient() - .on("POST", "http://example.com/negotiate?negotiateVersion=1", (req) -> Single.just(new HttpResponse(200, "", "{\"url\":\"http://testexample.com/\",\"accessToken\":\"newToken\"}"))) + .on("POST", "http://example.com/negotiate?negotiateVersion=1", (req) -> Single.just(new HttpResponse(200, "", + TestUtils.stringToByteBuffer("{\"url\":\"http://testexample.com/\",\"accessToken\":\"newToken\"}")))) .on("POST", "http://testexample.com/negotiate?negotiateVersion=1", (req) -> { token.set(req.getHeaders().get("Authorization")); - return Single.just(new HttpResponse(200, "", "{\"connectionId\":\"bVOiRPG8-6YiJ6d7ZcTOVQ\"," + return Single.just(new HttpResponse(200, "", TestUtils.stringToByteBuffer("{\"connectionId\":\"bVOiRPG8-6YiJ6d7ZcTOVQ\"," + "\"connectionToken\":\"connection-token-value\"," + "\"negotiateVersion\":1," - + "\"availableTransports\":[{\"transport\":\"WebSockets\",\"transferFormats\":[\"Text\",\"Binary\"]}]}")); + + "\"availableTransports\":[{\"transport\":\"WebSockets\",\"transferFormats\":[\"Text\",\"Binary\"]}]}"))); }); MockTransport transport = new MockTransport(true); @@ -2185,12 +3187,12 @@ public void authorizationHeaderFromNegotiateGetsClearedAfterStopping() { TestHttpClient client = new TestHttpClient() .on("POST", "http://example.com/negotiate?negotiateVersion=1", (req) -> { beforeRedirectToken.set(req.getHeaders().get("Authorization")); - return Single.just(new HttpResponse(200, "", "{\"url\":\"http://testexample.com/\",\"accessToken\":\"newToken\"}")); + return Single.just(new HttpResponse(200, "", TestUtils.stringToByteBuffer("{\"url\":\"http://testexample.com/\",\"accessToken\":\"newToken\"}"))); }) .on("POST", "http://testexample.com/negotiate?negotiateVersion=1", (req) -> { token.set(req.getHeaders().get("Authorization")); - return Single.just(new HttpResponse(200, "", "{\"connectionId\":\"bVOiRPG8-6YiJ6d7ZcTOVQ\"," - + "\"availableTransports\":[{\"transport\":\"WebSockets\",\"transferFormats\":[\"Text\",\"Binary\"]}]}")); + return Single.just(new HttpResponse(200, "", TestUtils.stringToByteBuffer("{\"connectionId\":\"bVOiRPG8-6YiJ6d7ZcTOVQ\"," + + "\"availableTransports\":[{\"transport\":\"WebSockets\",\"transferFormats\":[\"Text\",\"Binary\"]}]}"))); }); MockTransport transport = new MockTransport(true); @@ -2228,16 +3230,16 @@ public void authorizationHeaderFromNegotiateGetsSetToNewValue() { if (redirectCount.get() == 0) { redirectCount.incrementAndGet(); redirectToken.set(req.getHeaders().get("Authorization")); - return Single.just(new HttpResponse(200, "", "{\"url\":\"http://testexample.com/\",\"accessToken\":\"firstRedirectToken\"}")); + return Single.just(new HttpResponse(200, "", TestUtils.stringToByteBuffer("{\"url\":\"http://testexample.com/\",\"accessToken\":\"firstRedirectToken\"}"))); } else { redirectToken.set(req.getHeaders().get("Authorization")); - return Single.just(new HttpResponse(200, "", "{\"url\":\"http://testexample.com/\",\"accessToken\":\"secondRedirectToken\"}")); + return Single.just(new HttpResponse(200, "", TestUtils.stringToByteBuffer("{\"url\":\"http://testexample.com/\",\"accessToken\":\"secondRedirectToken\"}"))); } }) .on("POST", "http://testexample.com/negotiate?negotiateVersion=1", (req) -> { token.set(req.getHeaders().get("Authorization")); - return Single.just(new HttpResponse(200, "", "{\"connectionId\":\"bVOiRPG8-6YiJ6d7ZcTOVQ\",\"" - + "availableTransports\":[{\"transport\":\"WebSockets\",\"transferFormats\":[\"Text\",\"Binary\"]}]}")); + return Single.just(new HttpResponse(200, "", TestUtils.stringToByteBuffer("{\"connectionId\":\"bVOiRPG8-6YiJ6d7ZcTOVQ\",\"" + + "availableTransports\":[{\"transport\":\"WebSockets\",\"transferFormats\":[\"Text\",\"Binary\"]}]}"))); }); MockTransport transport = new MockTransport(true); @@ -2287,9 +3289,9 @@ public void connectionSendsPingsRegularly() throws InterruptedException { hubConnection.start().timeout(1, TimeUnit.SECONDS).blockingAwait(); - String message = mockTransport.getNextSentMessage().timeout(1, TimeUnit.SECONDS).blockingGet(); + String message = TestUtils.byteBufferToString(mockTransport.getNextSentMessage().timeout(1, TimeUnit.SECONDS).blockingGet()); assertEquals("{\"type\":6}" + RECORD_SEPARATOR, message); - message = mockTransport.getNextSentMessage().timeout(1, TimeUnit.SECONDS).blockingGet(); + message = TestUtils.byteBufferToString(mockTransport.getNextSentMessage().timeout(1, TimeUnit.SECONDS).blockingGet()); assertEquals("{\"type\":6}" + RECORD_SEPARATOR, message); hubConnection.stop().timeout(1, TimeUnit.SECONDS).blockingAwait(); @@ -2302,8 +3304,8 @@ public void userAgentHeaderIsSet() { .on("POST", "http://example.com/negotiate?negotiateVersion=1", (req) -> { header.set(req.getHeaders().get("User-Agent")); - return Single.just(new HttpResponse(200, "", "{\"connectionId\":\"bVOiRPG8-6YiJ6d7ZcTOVQ\",\"" - + "availableTransports\":[{\"transport\":\"WebSockets\",\"transferFormats\":[\"Text\",\"Binary\"]}]}")); + return Single.just(new HttpResponse(200, "", TestUtils.stringToByteBuffer("{\"connectionId\":\"bVOiRPG8-6YiJ6d7ZcTOVQ\",\"" + + "availableTransports\":[{\"transport\":\"WebSockets\",\"transferFormats\":[\"Text\",\"Binary\"]}]}"))); }); MockTransport transport = new MockTransport(); @@ -2326,8 +3328,8 @@ public void userAgentHeaderCanBeOverwritten() { .on("POST", "http://example.com/negotiate?negotiateVersion=1", (req) -> { header.set(req.getHeaders().get("User-Agent")); - return Single.just(new HttpResponse(200, "", "{\"connectionId\":\"bVOiRPG8-6YiJ6d7ZcTOVQ\",\"" - + "availableTransports\":[{\"transport\":\"WebSockets\",\"transferFormats\":[\"Text\",\"Binary\"]}]}")); + return Single.just(new HttpResponse(200, "", TestUtils.stringToByteBuffer("{\"connectionId\":\"bVOiRPG8-6YiJ6d7ZcTOVQ\",\"" + + "availableTransports\":[{\"transport\":\"WebSockets\",\"transferFormats\":[\"Text\",\"Binary\"]}]}"))); }); MockTransport transport = new MockTransport(); @@ -2350,8 +3352,8 @@ public void userAgentCanBeCleared() { .on("POST", "http://example.com/negotiate?negotiateVersion=1", (req) -> { header.set(req.getHeaders().get("User-Agent")); - return Single.just(new HttpResponse(200, "", "{\"connectionId\":\"bVOiRPG8-6YiJ6d7ZcTOVQ\",\"" - + "availableTransports\":[{\"transport\":\"WebSockets\",\"transferFormats\":[\"Text\",\"Binary\"]}]}")); + return Single.just(new HttpResponse(200, "", TestUtils.stringToByteBuffer("{\"connectionId\":\"bVOiRPG8-6YiJ6d7ZcTOVQ\",\"" + + "availableTransports\":[{\"transport\":\"WebSockets\",\"transferFormats\":[\"Text\",\"Binary\"]}]}"))); }); MockTransport transport = new MockTransport(); @@ -2373,8 +3375,8 @@ public void headersAreSetAndSentThroughBuilder() { .on("POST", "http://example.com/negotiate?negotiateVersion=1", (req) -> { header.set(req.getHeaders().get("ExampleHeader")); - return Single.just(new HttpResponse(200, "", "{\"connectionId\":\"bVOiRPG8-6YiJ6d7ZcTOVQ\",\"" - + "availableTransports\":[{\"transport\":\"WebSockets\",\"transferFormats\":[\"Text\",\"Binary\"]}]}")); + return Single.just(new HttpResponse(200, "", TestUtils.stringToByteBuffer("{\"connectionId\":\"bVOiRPG8-6YiJ6d7ZcTOVQ\",\"" + + "availableTransports\":[{\"transport\":\"WebSockets\",\"transferFormats\":[\"Text\",\"Binary\"]}]}"))); }); @@ -2398,8 +3400,8 @@ public void headersAreNotClearedWhenConnectionIsRestarted() { .on("POST", "http://example.com/negotiate?negotiateVersion=1", (req) -> { header.set(req.getHeaders().get("Authorization")); - return Single.just(new HttpResponse(200, "", "{\"connectionId\":\"bVOiRPG8-6YiJ6d7ZcTOVQ\",\"" - + "availableTransports\":[{\"transport\":\"WebSockets\",\"transferFormats\":[\"Text\",\"Binary\"]}]}")); + return Single.just(new HttpResponse(200, "", TestUtils.stringToByteBuffer("{\"connectionId\":\"bVOiRPG8-6YiJ6d7ZcTOVQ\",\"" + + "availableTransports\":[{\"transport\":\"WebSockets\",\"transferFormats\":[\"Text\",\"Binary\"]}]}"))); }); MockTransport transport = new MockTransport(); @@ -2428,13 +3430,13 @@ public void userSetAuthHeaderIsNotClearedAfterRedirect() { .on("POST", "http://example.com/negotiate?negotiateVersion=1", (req) -> { beforeRedirectHeader.set(req.getHeaders().get("Authorization")); - return Single.just(new HttpResponse(200, "", "{\"url\":\"http://testexample.com/\",\"accessToken\":\"redirectToken\"}\"}")); + return Single.just(new HttpResponse(200, "", TestUtils.stringToByteBuffer("{\"url\":\"http://testexample.com/\",\"accessToken\":\"redirectToken\"}\"}"))); }) .on("POST", "http://testexample.com/negotiate?negotiateVersion=1", (req) -> { afterRedirectHeader.set(req.getHeaders().get("Authorization")); - return Single.just(new HttpResponse(200, "", "{\"connectionId\":\"bVOiRPG8-6YiJ6d7ZcTOVQ\",\"" - + "availableTransports\":[{\"transport\":\"WebSockets\",\"transferFormats\":[\"Text\",\"Binary\"]}]}")); + return Single.just(new HttpResponse(200, "", TestUtils.stringToByteBuffer("{\"connectionId\":\"bVOiRPG8-6YiJ6d7ZcTOVQ\",\"" + + "availableTransports\":[{\"transport\":\"WebSockets\",\"transferFormats\":[\"Text\",\"Binary\"]}]}"))); }); MockTransport transport = new MockTransport(); @@ -2471,8 +3473,8 @@ public void sameHeaderSetTwiceGetsOverwritten() { .on("POST", "http://example.com/negotiate?negotiateVersion=1", (req) -> { header.set(req.getHeaders().get("ExampleHeader")); - return Single.just(new HttpResponse(200, "", "{\"connectionId\":\"bVOiRPG8-6YiJ6d7ZcTOVQ\",\"" - + "availableTransports\":[{\"transport\":\"WebSockets\",\"transferFormats\":[\"Text\",\"Binary\"]}]}")); + return Single.just(new HttpResponse(200, "", TestUtils.stringToByteBuffer("{\"connectionId\":\"bVOiRPG8-6YiJ6d7ZcTOVQ\",\"" + + "availableTransports\":[{\"transport\":\"WebSockets\",\"transferFormats\":[\"Text\",\"Binary\"]}]}"))); }); @@ -2513,9 +3515,9 @@ public void hubConnectionCanBeStartedAfterBeingStopped() { public void hubConnectionCanBeStartedAfterBeingStoppedAndRedirected() { MockTransport mockTransport = new MockTransport(); TestHttpClient client = new TestHttpClient() - .on("POST", "http://example.com/negotiate?negotiateVersion=1", (req) -> Single.just(new HttpResponse(200, "", "{\"url\":\"http://testexample.com/\"}"))) - .on("POST", "http://testexample.com/negotiate?negotiateVersion=1", (req) -> Single.just(new HttpResponse(200, "", "{\"connectionId\":\"bVOiRPG8-6YiJ6d7ZcTOVQ\",\"" - + "availableTransports\":[{\"transport\":\"WebSockets\",\"transferFormats\":[\"Text\",\"Binary\"]}]}"))); + .on("POST", "http://example.com/negotiate?negotiateVersion=1", (req) -> Single.just(new HttpResponse(200, "", TestUtils.stringToByteBuffer("{\"url\":\"http://testexample.com/\"}")))) + .on("POST", "http://testexample.com/negotiate?negotiateVersion=1", (req) -> Single.just(new HttpResponse(200, "", + TestUtils.stringToByteBuffer("{\"connectionId\":\"bVOiRPG8-6YiJ6d7ZcTOVQ\",\"availableTransports\":[{\"transport\":\"WebSockets\",\"transferFormats\":[\"Text\",\"Binary\"]}]}")))); HubConnection hubConnection = HubConnectionBuilder .create("http://example.com") @@ -2537,7 +3539,7 @@ public void hubConnectionCanBeStartedAfterBeingStoppedAndRedirected() { public void non200FromNegotiateThrowsError() { TestHttpClient client = new TestHttpClient() .on("POST", "http://example.com/negotiate?negotiateVersion=1", - (req) -> Single.just(new HttpResponse(500, "Internal server error", ""))); + (req) -> Single.just(new HttpResponse(500, "Internal server error", TestUtils.emptyByteBuffer))); MockTransport transport = new MockTransport(); HubConnection hubConnection = HubConnectionBuilder @@ -2555,9 +3557,9 @@ public void non200FromNegotiateThrowsError() { public void hubConnectionCloseCallsStop() throws Exception { MockTransport mockTransport = new MockTransport(); TestHttpClient client = new TestHttpClient() - .on("POST", "http://example.com/negotiate?negotiateVersion=1", (req) -> Single.just(new HttpResponse(200, "", "{\"url\":\"http://testexample.com/\"}"))) - .on("POST", "http://testexample.com/negotiate?negotiateVersion=1", (req) -> Single.just(new HttpResponse(200, "", "{\"connectionId\":\"bVOiRPG8-6YiJ6d7ZcTOVQ\",\"" - + "availableTransports\":[{\"transport\":\"WebSockets\",\"transferFormats\":[\"Text\",\"Binary\"]}]}"))); + .on("POST", "http://example.com/negotiate?negotiateVersion=1", (req) -> Single.just(new HttpResponse(200, "", TestUtils.stringToByteBuffer("{\"url\":\"http://testexample.com/\"}")))) + .on("POST", "http://testexample.com/negotiate?negotiateVersion=1", (req) -> Single.just(new HttpResponse(200, "", + TestUtils.stringToByteBuffer("{\"connectionId\":\"bVOiRPG8-6YiJ6d7ZcTOVQ\",\"availableTransports\":[{\"transport\":\"WebSockets\",\"transferFormats\":[\"Text\",\"Binary\"]}]}")))); CompletableSubject close = CompletableSubject.create(); diff --git a/src/SignalR/clients/java/signalr/src/test/java/com/microsoft/signalr/JsonHubProtocolTest.java b/src/SignalR/clients/java/signalr/src/test/java/com/microsoft/signalr/JsonHubProtocolTest.java index f4df7a24b673..92febe6ad0bd 100644 --- a/src/SignalR/clients/java/signalr/src/test/java/com/microsoft/signalr/JsonHubProtocolTest.java +++ b/src/SignalR/clients/java/signalr/src/test/java/com/microsoft/signalr/JsonHubProtocolTest.java @@ -5,12 +5,15 @@ import static org.junit.jupiter.api.Assertions.*; +import java.lang.reflect.Type; +import java.nio.ByteBuffer; import java.util.ArrayList; -import java.util.Arrays; +import java.util.HashMap; import java.util.List; import org.junit.jupiter.api.Test; +import com.fasterxml.jackson.core.type.TypeReference; class JsonHubProtocolTest { private JsonHubProtocol jsonHubProtocol = new JsonHubProtocol(); @@ -32,8 +35,8 @@ public void checkTransferFormat() { @Test public void verifyWriteMessage() { - InvocationMessage invocationMessage = new InvocationMessage(null, "test", new Object[] {"42"}); - String result = jsonHubProtocol.writeMessage(invocationMessage); + InvocationMessage invocationMessage = new InvocationMessage(null, null, "test", new Object[] {"42"}, null); + String result = TestUtils.byteBufferToString(jsonHubProtocol.writeMessage(invocationMessage)); String expectedResult = "{\"type\":1,\"target\":\"test\",\"arguments\":[\"42\"]}\u001E"; assertEquals(expectedResult, result); } @@ -41,29 +44,33 @@ public void verifyWriteMessage() { @Test public void parsePingMessage() { String stringifiedMessage = "{\"type\":6}\u001E"; - TestBinder binder = new TestBinder(PingMessage.getInstance()); + ByteBuffer message = TestUtils.stringToByteBuffer(stringifiedMessage); + TestBinder binder = new TestBinder(null, null); - HubMessage[] messages = jsonHubProtocol.parseMessages(stringifiedMessage, binder); + List messages = jsonHubProtocol.parseMessages(message, binder); //We know it's only one message - assertEquals(1, messages.length); - assertEquals(HubMessageType.PING, messages[0].getMessageType()); + assertNotNull(messages); + assertEquals(1, messages.size()); + assertEquals(HubMessageType.PING, messages.get(0).getMessageType()); } @Test public void parseCloseMessage() { String stringifiedMessage = "{\"type\":7}\u001E"; - TestBinder binder = new TestBinder(new CloseMessage()); + ByteBuffer message = TestUtils.stringToByteBuffer(stringifiedMessage); + TestBinder binder = new TestBinder(null, null); - HubMessage[] messages = jsonHubProtocol.parseMessages(stringifiedMessage, binder); + List messages = jsonHubProtocol.parseMessages(message, binder); //We know it's only one message - assertEquals(1, messages.length); + assertNotNull(messages); + assertEquals(1, messages.size()); - assertEquals(HubMessageType.CLOSE, messages[0].getMessageType()); + assertEquals(HubMessageType.CLOSE, messages.get(0).getMessageType()); //We can safely cast here because we know that it's a close message. - CloseMessage closeMessage = (CloseMessage) messages[0]; + CloseMessage closeMessage = (CloseMessage) messages.get(0); assertEquals(null, closeMessage.getError()); } @@ -71,17 +78,19 @@ public void parseCloseMessage() { @Test public void parseCloseMessageWithError() { String stringifiedMessage = "{\"type\":7,\"error\": \"There was an error\"}\u001E"; - TestBinder binder = new TestBinder(new CloseMessage("There was an error")); + ByteBuffer message = TestUtils.stringToByteBuffer(stringifiedMessage); + TestBinder binder = new TestBinder(new Type[] { int.class }, null); - HubMessage[] messages = jsonHubProtocol.parseMessages(stringifiedMessage, binder); + List messages = jsonHubProtocol.parseMessages(message, binder); //We know it's only one message - assertEquals(1, messages.length); + assertNotNull(messages); + assertEquals(1, messages.size()); - assertEquals(HubMessageType.CLOSE, messages[0].getMessageType()); + assertEquals(HubMessageType.CLOSE, messages.get(0).getMessageType()); //We can safely cast here because we know that it's a close message. - CloseMessage closeMessage = (CloseMessage) messages[0]; + CloseMessage closeMessage = (CloseMessage) messages.get(0); assertEquals("There was an error", closeMessage.getError()); } @@ -89,17 +98,19 @@ public void parseCloseMessageWithError() { @Test public void parseSingleMessage() { String stringifiedMessage = "{\"type\":1,\"target\":\"test\",\"arguments\":[42]}\u001E"; - TestBinder binder = new TestBinder(new InvocationMessage("1", "test", new Object[] { 42 })); + ByteBuffer message = TestUtils.stringToByteBuffer(stringifiedMessage); + TestBinder binder = new TestBinder(new Type[] { int.class }, null); - HubMessage[] messages = jsonHubProtocol.parseMessages(stringifiedMessage, binder); + List messages = jsonHubProtocol.parseMessages(message, binder); //We know it's only one message - assertEquals(1, messages.length); + assertNotNull(messages); + assertEquals(1, messages.size()); - assertEquals(HubMessageType.INVOCATION, messages[0].getMessageType()); + assertEquals(HubMessageType.INVOCATION, messages.get(0).getMessageType()); //We can safely cast here because we know that it's an invocation message. - InvocationMessage invocationMessage = (InvocationMessage) messages[0]; + InvocationMessage invocationMessage = (InvocationMessage) messages.get(0); assertEquals("test", invocationMessage.getTarget()); assertEquals(null, invocationMessage.getInvocationId()); @@ -111,34 +122,39 @@ public void parseSingleMessage() { @Test public void parseSingleUnsupportedStreamInvocationMessage() { String stringifiedMessage = "{\"type\":4,\"Id\":1,\"target\":\"test\",\"arguments\":[42]}\u001E"; - TestBinder binder = new TestBinder(new StreamInvocationMessage("1", "test", new Object[] { 42 })); + ByteBuffer message = TestUtils.stringToByteBuffer(stringifiedMessage); + TestBinder binder = new TestBinder(new Type[] { int.class }, null); - Throwable exception = assertThrows(UnsupportedOperationException.class, () -> jsonHubProtocol.parseMessages(stringifiedMessage, binder)); + Throwable exception = assertThrows(UnsupportedOperationException.class, () -> jsonHubProtocol.parseMessages(message, binder)); assertEquals("The message type STREAM_INVOCATION is not supported yet.", exception.getMessage()); } @Test public void parseSingleUnsupportedCancelInvocationMessage() { String stringifiedMessage = "{\"type\":5,\"invocationId\":123}\u001E"; - TestBinder binder = new TestBinder(null); + ByteBuffer message = TestUtils.stringToByteBuffer(stringifiedMessage); + TestBinder binder = new TestBinder(null, null); - Throwable exception = assertThrows(UnsupportedOperationException.class, () -> jsonHubProtocol.parseMessages(stringifiedMessage, binder)); + Throwable exception = assertThrows(UnsupportedOperationException.class, () -> jsonHubProtocol.parseMessages(message, binder)); assertEquals("The message type CANCEL_INVOCATION is not supported yet.", exception.getMessage()); } @Test public void parseTwoMessages() { - String twoMessages = "{\"type\":1,\"target\":\"one\",\"arguments\":[42]}\u001E{\"type\":1,\"target\":\"two\",\"arguments\":[43]}\u001E"; - TestBinder binder = new TestBinder(new InvocationMessage("1", "one", new Object[] { 42 })); + String stringifiedMessage = "{\"type\":1,\"target\":\"one\",\"arguments\":[42]}\u001E{\"type\":1,\"target\":\"two\",\"arguments\":[43]}\u001E"; + ByteBuffer message = TestUtils.stringToByteBuffer(stringifiedMessage); + TestBinder binder = new TestBinder(new Type[] { int.class }, null); - HubMessage[] messages = jsonHubProtocol.parseMessages(twoMessages, binder); - assertEquals(2, messages.length); + List messages = jsonHubProtocol.parseMessages(message, binder); + + assertNotNull(messages); + assertEquals(2, messages.size()); // Check the first message - assertEquals(HubMessageType.INVOCATION, messages[0].getMessageType()); + assertEquals(HubMessageType.INVOCATION, messages.get(0).getMessageType()); //Now that we know we have an invocation message we can cast the hubMessage. - InvocationMessage invocationMessage = (InvocationMessage) messages[0]; + InvocationMessage invocationMessage = (InvocationMessage) messages.get(0); assertEquals("one", invocationMessage.getTarget()); assertEquals(null, invocationMessage.getInvocationId()); @@ -146,10 +162,10 @@ public void parseTwoMessages() { assertEquals(42, messageResult); // Check the second message - assertEquals(HubMessageType.INVOCATION, messages[1].getMessageType()); + assertEquals(HubMessageType.INVOCATION, messages.get(1).getMessageType()); //Now that we know we have an invocation message we can cast the hubMessage. - InvocationMessage invocationMessage2 = (InvocationMessage) messages[1]; + InvocationMessage invocationMessage2 = (InvocationMessage) messages.get(1); assertEquals("two", invocationMessage2.getTarget()); assertEquals(null, invocationMessage2.getInvocationId()); @@ -160,37 +176,135 @@ public void parseTwoMessages() { @Test public void parseSingleMessageMutipleArgs() { String stringifiedMessage = "{\"type\":1,\"target\":\"test\",\"arguments\":[42, 24]}\u001E"; - TestBinder binder = new TestBinder(new InvocationMessage("1", "test", new Object[] { 42, 24 })); + ByteBuffer message = TestUtils.stringToByteBuffer(stringifiedMessage); + TestBinder binder = new TestBinder(new Type[] { int.class, int.class }, null); - HubMessage[] messages = jsonHubProtocol.parseMessages(stringifiedMessage, binder); + List messages = jsonHubProtocol.parseMessages(message, binder); + + assertNotNull(messages); + assertEquals(1, messages.size()); //We know it's only one message - assertEquals(HubMessageType.INVOCATION, messages[0].getMessageType()); + assertEquals(HubMessageType.INVOCATION, messages.get(0).getMessageType()); - InvocationMessage message = (InvocationMessage)messages[0]; - assertEquals("test", message.getTarget()); - assertEquals(null, message.getInvocationId()); - int messageResult = (int) message.getArguments()[0]; - int messageResult2 = (int) message.getArguments()[1]; + InvocationMessage invocationMessage = (InvocationMessage)messages.get(0); + assertEquals("test", invocationMessage.getTarget()); + assertEquals(null, invocationMessage.getInvocationId()); + int messageResult = (int) invocationMessage.getArguments()[0]; + int messageResult2 = (int) invocationMessage.getArguments()[1]; assertEquals(42, messageResult); assertEquals(24, messageResult2); } + + @Test + public void parseSingleMessageNestedCollection() { + String stringifiedMessage = "{\"type\":1,\"target\":\"test\",\"arguments\":[[{\"one\":[\"a\",\"b\"],\"two\":[\"\uBEEF\",\"\uABCD\"]},{\"four\":[\"^\",\"*\"],\"three\":[\"5\",\"9\"]}]]}\u001E"; + ByteBuffer message = TestUtils.stringToByteBuffer(stringifiedMessage); + TestBinder binder = new TestBinder(new Type[] { (new TypeReference>>>() { }).getType() }, null); + + List 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>> result = (ArrayList>>)invocationMessage.getArguments()[0]; + assertEquals(2, result.size()); + + HashMap> firstMap = result.get(0); + HashMap> secondMap = result.get(1); + + assertEquals(2, firstMap.keySet().size()); + assertEquals(2, secondMap.keySet().size()); + + ArrayList firstList = firstMap.get("one"); + ArrayList secondList = firstMap.get("two"); + + ArrayList thirdList = secondMap.get("three"); + ArrayList 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>>() { }).getType() }, null); + + List 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> result = (PersonPojo>)invocationMessage.getArguments()[0]; + assertEquals("John", result.getFirstName()); + assertEquals("Doe", result.getLastName()); + assertEquals(30, result.getAge()); + + ArrayList generic = result.getT(); + assertEquals(2, generic.size()); + assertEquals((short)5, (short)generic.get(0)); + assertEquals((short)8, (short)generic.get(1)); + } @Test public void parseMessageWithOutOfOrderProperties() { String stringifiedMessage = "{\"arguments\":[42, 24],\"type\":1,\"target\":\"test\"}\u001E"; - TestBinder binder = new TestBinder(new InvocationMessage("1", "test", new Object[] { 42, 24 })); + ByteBuffer message = TestUtils.stringToByteBuffer(stringifiedMessage); + TestBinder binder = new TestBinder(new Type[] { int.class, int.class }, null); - HubMessage[] messages = jsonHubProtocol.parseMessages(stringifiedMessage, binder); + List messages = jsonHubProtocol.parseMessages(message, binder); + + assertNotNull(messages); + assertEquals(1, messages.size()); // We know it's only one message - assertEquals(HubMessageType.INVOCATION, messages[0].getMessageType()); + assertEquals(HubMessageType.INVOCATION, messages.get(0).getMessageType()); - InvocationMessage message = (InvocationMessage) messages[0]; - assertEquals("test", message.getTarget()); - assertEquals(null, message.getInvocationId()); - int messageResult = (int) message.getArguments()[0]; - int messageResult2 = (int) message.getArguments()[1]; + InvocationMessage invocationMessage = (InvocationMessage) messages.get(0); + assertEquals("test", invocationMessage.getTarget()); + assertEquals(null, invocationMessage.getInvocationId()); + int messageResult = (int) invocationMessage.getArguments()[0]; + int messageResult2 = (int) invocationMessage.getArguments()[1]; assertEquals(42, messageResult); assertEquals(24, messageResult2); } @@ -198,134 +312,111 @@ public void parseMessageWithOutOfOrderProperties() { @Test public void parseCompletionMessageWithOutOfOrderProperties() { String stringifiedMessage = "{\"type\":3,\"result\":42,\"invocationId\":\"1\"}\u001E"; - TestBinder binder = new TestBinder(new CompletionMessage("1", 42, null)); + ByteBuffer message = TestUtils.stringToByteBuffer(stringifiedMessage); + TestBinder binder = new TestBinder(null, int.class); - HubMessage[] messages = jsonHubProtocol.parseMessages(stringifiedMessage, binder); + List messages = jsonHubProtocol.parseMessages(message, binder); + + assertNotNull(messages); + assertEquals(1, messages.size()); // We know it's only one message - assertEquals(HubMessageType.COMPLETION, messages[0].getMessageType()); + assertEquals(HubMessageType.COMPLETION, messages.get(0).getMessageType()); - CompletionMessage message = (CompletionMessage) messages[0]; - assertEquals(null, message.getError()); - assertEquals(42 , message.getResult()); + CompletionMessage completionMessage = (CompletionMessage) messages.get(0); + assertEquals(null, completionMessage.getError()); + assertEquals(42 , completionMessage.getResult()); } @Test public void invocationBindingFailureWhileParsingTooManyArgumentsWithOutOfOrderProperties() { String stringifiedMessage = "{\"arguments\":[42, 24],\"type\":1,\"target\":\"test\"}\u001E"; - TestBinder binder = new TestBinder(new InvocationMessage(null, "test", new Object[] { 42 })); - - HubMessage[] messages = jsonHubProtocol.parseMessages(stringifiedMessage, binder); - assertEquals(1, messages.length); - assertEquals(InvocationBindingFailureMessage.class, messages[0].getClass()); - InvocationBindingFailureMessage message = (InvocationBindingFailureMessage)messages[0]; - assertEquals("Invocation provides 2 argument(s) but target expects 1.", message.getException().getMessage()); + ByteBuffer message = TestUtils.stringToByteBuffer(stringifiedMessage); + TestBinder binder = new TestBinder(new Type[] { int.class }, null); + + List messages = jsonHubProtocol.parseMessages(message, binder); + + assertNotNull(messages); + assertEquals(1, messages.size()); + + assertEquals(InvocationBindingFailureMessage.class, messages.get(0).getClass()); + InvocationBindingFailureMessage invocationBindingFailureMessage = (InvocationBindingFailureMessage)messages.get(0); + assertEquals("Invocation provides 2 argument(s) but target expects 1.", invocationBindingFailureMessage.getException().getMessage()); } @Test public void invocationBindingFailureWhileParsingTooManyArguments() { String stringifiedMessage = "{\"type\":1,\"target\":\"test\",\"arguments\":[42, 24]}\u001E"; - TestBinder binder = new TestBinder(new InvocationMessage(null, "test", new Object[] { 42 })); - - HubMessage[] messages = jsonHubProtocol.parseMessages(stringifiedMessage, binder); - assertEquals(1, messages.length); - assertEquals(InvocationBindingFailureMessage.class, messages[0].getClass()); - InvocationBindingFailureMessage message = (InvocationBindingFailureMessage) messages[0]; - assertEquals("Invocation provides 2 argument(s) but target expects 1.", message.getException().getMessage()); + ByteBuffer message = TestUtils.stringToByteBuffer(stringifiedMessage); + TestBinder binder = new TestBinder(new Type[] { int.class }, null); + + List messages = jsonHubProtocol.parseMessages(message, binder); + + assertNotNull(messages); + assertEquals(1, messages.size()); + + assertEquals(InvocationBindingFailureMessage.class, messages.get(0).getClass()); + InvocationBindingFailureMessage invocationBindingFailureMessage = (InvocationBindingFailureMessage) messages.get(0); + assertEquals("Invocation provides 2 argument(s) but target expects 1.", invocationBindingFailureMessage.getException().getMessage()); } @Test public void invocationBindingFailureWhileParsingTooFewArguments() { String stringifiedMessage = "{\"type\":1,\"target\":\"test\",\"arguments\":[42]}\u001E"; - TestBinder binder = new TestBinder(new InvocationMessage(null, "test", new Object[] { 42, 24 })); - - HubMessage[] messages = jsonHubProtocol.parseMessages(stringifiedMessage, binder); - assertEquals(1, messages.length); - assertEquals(InvocationBindingFailureMessage.class, messages[0].getClass()); - InvocationBindingFailureMessage message = (InvocationBindingFailureMessage) messages[0]; - assertEquals("Invocation provides 1 argument(s) but target expects 2.", message.getException().getMessage()); + ByteBuffer message = TestUtils.stringToByteBuffer(stringifiedMessage); + TestBinder binder = new TestBinder(new Type[] { int.class, int.class }, null); + + List messages = jsonHubProtocol.parseMessages(message, binder); + + assertNotNull(messages); + assertEquals(1, messages.size()); + + assertEquals(InvocationBindingFailureMessage.class, messages.get(0).getClass()); + InvocationBindingFailureMessage invocationBindingFailureMessage = (InvocationBindingFailureMessage) messages.get(0); + assertEquals("Invocation provides 1 argument(s) but target expects 2.", invocationBindingFailureMessage.getException().getMessage()); } @Test public void invocationBindingFailureWhenParsingIncorrectType() { String stringifiedMessage = "{\"type\":1,\"target\":\"test\",\"arguments\":[\"true\"]}\u001E"; - TestBinder binder = new TestBinder(new InvocationMessage(null, "test", new Object[] { 42 })); - - HubMessage[] messages = jsonHubProtocol.parseMessages(stringifiedMessage, binder); - assertEquals(1, messages.length); - assertEquals(InvocationBindingFailureMessage.class, messages[0].getClass()); - InvocationBindingFailureMessage message = (InvocationBindingFailureMessage) messages[0]; - assertEquals("java.lang.NumberFormatException: For input string: \"true\"", message.getException().getMessage()); + ByteBuffer message = TestUtils.stringToByteBuffer(stringifiedMessage); + TestBinder binder = new TestBinder(new Type[] { int.class }, null); + + List messages = jsonHubProtocol.parseMessages(message, binder); + + assertNotNull(messages); + assertEquals(1, messages.size()); + + assertEquals(InvocationBindingFailureMessage.class, messages.get(0).getClass()); + InvocationBindingFailureMessage invocationBindingFailureMessage = (InvocationBindingFailureMessage) messages.get(0); + assertEquals("java.lang.NumberFormatException: For input string: \"true\"", invocationBindingFailureMessage.getException().getMessage()); } @Test public void invocationBindingFailureStillReadsJsonPayloadAfterFailure() { String stringifiedMessage = "{\"type\":1,\"target\":\"test\",\"arguments\":[\"true\"],\"invocationId\":\"123\"}\u001E"; - TestBinder binder = new TestBinder(new InvocationMessage(null, "test", new Object[] { 42 })); - - HubMessage[] messages = jsonHubProtocol.parseMessages(stringifiedMessage, binder); - assertEquals(1, messages.length); - assertEquals(InvocationBindingFailureMessage.class, messages[0].getClass()); - InvocationBindingFailureMessage message = (InvocationBindingFailureMessage) messages[0]; - assertEquals("java.lang.NumberFormatException: For input string: \"true\"", message.getException().getMessage()); - assertEquals("123", message.getInvocationId()); + ByteBuffer message = TestUtils.stringToByteBuffer(stringifiedMessage); + TestBinder binder = new TestBinder(new Type[] { int.class }, null); + + List messages = jsonHubProtocol.parseMessages(message, binder); + + assertNotNull(messages); + assertEquals(1, messages.size()); + + assertEquals(InvocationBindingFailureMessage.class, messages.get(0).getClass()); + InvocationBindingFailureMessage invocationBindingFailureMessage = (InvocationBindingFailureMessage) messages.get(0); + assertEquals("java.lang.NumberFormatException: For input string: \"true\"", invocationBindingFailureMessage.getException().getMessage()); + assertEquals("123", invocationBindingFailureMessage.getInvocationId()); } @Test public void errorWhileParsingIncompleteMessage() { String stringifiedMessage = "{\"type\":1,\"target\":\"test\",\"arguments\":"; - TestBinder binder = new TestBinder(new InvocationMessage(null, "test", new Object[] { 42, 24 })); + ByteBuffer message = TestUtils.stringToByteBuffer(stringifiedMessage); + TestBinder binder = new TestBinder(new Type[] { int.class, int.class }, null); RuntimeException exception = assertThrows(RuntimeException.class, - () -> jsonHubProtocol.parseMessages(stringifiedMessage, binder)); + () -> jsonHubProtocol.parseMessages(message, binder)); assertEquals("Message is incomplete.", exception.getMessage()); } - - private class TestBinder implements InvocationBinder { - private Class[] paramTypes = null; - private Class returnType = null; - - public TestBinder(HubMessage expectedMessage) { - if (expectedMessage == null) { - return; - } - - switch (expectedMessage.getMessageType()) { - case STREAM_INVOCATION: - ArrayList> streamTypes = new ArrayList<>(); - for (Object obj : ((StreamInvocationMessage) expectedMessage).getArguments()) { - streamTypes.add(obj.getClass()); - } - paramTypes = streamTypes.toArray(new Class[streamTypes.size()]); - break; - case INVOCATION: - ArrayList> 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> getParameterTypes(String methodName) { - if (paramTypes == null) { - return new ArrayList<>(); - } - return new ArrayList>(Arrays.asList(paramTypes)); - } - } } \ No newline at end of file diff --git a/src/SignalR/clients/java/signalr/src/test/java/com/microsoft/signalr/LongPollingTransportTest.java b/src/SignalR/clients/java/signalr/src/test/java/com/microsoft/signalr/LongPollingTransportTest.java index 5a9408387e81..ae9cb177597e 100644 --- a/src/SignalR/clients/java/signalr/src/test/java/com/microsoft/signalr/LongPollingTransportTest.java +++ b/src/SignalR/clients/java/signalr/src/test/java/com/microsoft/signalr/LongPollingTransportTest.java @@ -5,6 +5,7 @@ import static org.junit.jupiter.api.Assertions.*; +import java.nio.ByteBuffer; import java.util.HashMap; import java.util.Map; import java.util.concurrent.TimeUnit; @@ -22,7 +23,7 @@ public class LongPollingTransportTest { @Test public void LongPollingFailsToConnectWith404Response() { TestHttpClient client = new TestHttpClient() - .on("GET", (req) -> Single.just(new HttpResponse(404, "", ""))); + .on("GET", (req) -> Single.just(new HttpResponse(404, "", TestUtils.emptyByteBuffer))); Map headers = new HashMap<>(); LongPollingTransport transport = new LongPollingTransport(headers, client, Single.just("")); @@ -35,11 +36,12 @@ public void LongPollingFailsToConnectWith404Response() { @Test public void LongPollingTransportCantSendBeforeStart() { TestHttpClient client = new TestHttpClient() - .on("GET", (req) -> Single.just(new HttpResponse(404, "", ""))); + .on("GET", (req) -> Single.just(new HttpResponse(404, "", TestUtils.emptyByteBuffer))); Map headers = new HashMap<>(); LongPollingTransport transport = new LongPollingTransport(headers, client, Single.just("")); - Throwable exception = assertThrows(RuntimeException.class, () -> transport.send("First").timeout(1, TimeUnit.SECONDS).blockingAwait()); + ByteBuffer sendBuffer = TestUtils.stringToByteBuffer("First"); + Throwable exception = assertThrows(RuntimeException.class, () -> transport.send(sendBuffer).timeout(1, TimeUnit.SECONDS).blockingAwait()); assertEquals(Exception.class, exception.getCause().getClass()); assertEquals("Cannot send unless the transport is active.", exception.getCause().getMessage()); assertFalse(transport.isActive()); @@ -53,9 +55,9 @@ public void StatusCode204StopsLongPollingTriggersOnClosed() { .on("GET", (req) -> { if (firstPoll.get()) { firstPoll.set(false); - return Single.just(new HttpResponse(200, "", "")); + return Single.just(new HttpResponse(200, "", TestUtils.emptyByteBuffer)); } - return Single.just(new HttpResponse(204, "", "")); + return Single.just(new HttpResponse(204, "", TestUtils.emptyByteBuffer)); }); Map headers = new HashMap<>(); @@ -81,9 +83,9 @@ public void LongPollingFailsWhenReceivingUnexpectedErrorCode() { .on("GET", (req) -> { if (firstPoll.get()) { firstPoll.set(false); - return Single.just(new HttpResponse(200, "", "")); + return Single.just(new HttpResponse(200, "", TestUtils.emptyByteBuffer)); } - return Single.just(new HttpResponse(999, "", "")); + return Single.just(new HttpResponse(999, "", TestUtils.emptyByteBuffer)); }); Map headers = new HashMap<>(); @@ -104,7 +106,7 @@ public void LongPollingFailsWhenReceivingUnexpectedErrorCode() { @Test public void CanSetAndTriggerOnReceive() { TestHttpClient client = new TestHttpClient() - .on("GET", (req) -> Single.just(new HttpResponse(200, "", ""))); + .on("GET", (req) -> Single.just(new HttpResponse(200, "", TestUtils.emptyByteBuffer))); Map headers = new HashMap<>(); LongPollingTransport transport = new LongPollingTransport(headers, client, Single.just("")); @@ -112,12 +114,13 @@ public void CanSetAndTriggerOnReceive() { AtomicBoolean onReceivedRan = new AtomicBoolean(false); transport.setOnReceive((message) -> { onReceivedRan.set(true); - assertEquals("TEST", message); + assertEquals("TEST", TestUtils.byteBufferToString(message)); }); // The transport doesn't need to be active to trigger onReceive for the case // when we are handling the last outstanding poll. - transport.onReceive("TEST"); + ByteBuffer onReceiveBuffer = TestUtils.stringToByteBuffer("TEST"); + transport.onReceive(onReceiveBuffer); assertTrue(onReceivedRan.get()); } @@ -129,13 +132,13 @@ public void LongPollingTransportOnReceiveGetsCalled() { .on("GET", (req) -> { if (requestCount.get() == 0) { requestCount.incrementAndGet(); - return Single.just(new HttpResponse(200, "", "")); + return Single.just(new HttpResponse(200, "", TestUtils.emptyByteBuffer)); } else if (requestCount.get() == 1) { requestCount.incrementAndGet(); - return Single.just(new HttpResponse(200, "", "TEST")); + return Single.just(new HttpResponse(200, "", TestUtils.stringToByteBuffer("TEST"))); } - return Single.just(new HttpResponse(204, "", "")); + return Single.just(new HttpResponse(204, "", TestUtils.emptyByteBuffer)); }); Map headers = new HashMap<>(); @@ -145,7 +148,7 @@ public void LongPollingTransportOnReceiveGetsCalled() { AtomicReference message = new AtomicReference<>(); transport.setOnReceive((msg -> { onReceiveCalled.set(true); - message.set(msg); + message.set(TestUtils.byteBufferToString(msg)); block.onComplete(); }) ); @@ -165,19 +168,19 @@ public void LongPollingTransportOnReceiveGetsCalledMultipleTimes() { .on("GET", (req) -> { if (requestCount.get() == 0) { requestCount.incrementAndGet(); - return Single.just(new HttpResponse(200, "", "")); + return Single.just(new HttpResponse(200, "", TestUtils.emptyByteBuffer)); } else if (requestCount.get() == 1) { requestCount.incrementAndGet(); - return Single.just(new HttpResponse(200, "", "FIRST")); + return Single.just(new HttpResponse(200, "", TestUtils.stringToByteBuffer("FIRST"))); } else if (requestCount.get() == 2) { requestCount.incrementAndGet(); - return Single.just(new HttpResponse(200, "", "SECOND")); + return Single.just(new HttpResponse(200, "", TestUtils.stringToByteBuffer("SECOND"))); } else if (requestCount.get() == 3) { requestCount.incrementAndGet(); - return Single.just(new HttpResponse(200, "", "THIRD")); + return Single.just(new HttpResponse(200, "", TestUtils.stringToByteBuffer("THIRD"))); } - return Single.just(new HttpResponse(204, "", "")); + return Single.just(new HttpResponse(204, "", TestUtils.emptyByteBuffer)); }); Map headers = new HashMap<>(); @@ -188,7 +191,7 @@ public void LongPollingTransportOnReceiveGetsCalledMultipleTimes() { AtomicInteger messageCount = new AtomicInteger(); transport.setOnReceive((msg) -> { onReceiveCalled.set(true); - message.set(message.get() + msg); + message.set(message.get() + TestUtils.byteBufferToString(msg)); if (messageCount.incrementAndGet() == 3) { blocker.onComplete(); } @@ -211,14 +214,14 @@ public void LongPollingTransportSendsHeaders() { .on("GET", (req) -> { if (requestCount.get() == 0) { requestCount.incrementAndGet(); - return Single.just(new HttpResponse(200, "", "")); + return Single.just(new HttpResponse(200, "", TestUtils.emptyByteBuffer)); } assertTrue(close.blockingAwait(1, TimeUnit.SECONDS)); - return Single.just(new HttpResponse(204, "", "")); + return Single.just(new HttpResponse(204, "", TestUtils.emptyByteBuffer)); }).on("POST", (req) -> { assertFalse(req.getHeaders().isEmpty()); headerValue.set(req.getHeaders().get("KEY")); - return Single.just(new HttpResponse(200, "", "")); + return Single.just(new HttpResponse(200, "", TestUtils.emptyByteBuffer)); }); Map headers = new HashMap<>(); @@ -227,7 +230,8 @@ public void LongPollingTransportSendsHeaders() { transport.setOnClose((error) -> {}); transport.start("http://example.com").timeout(1, TimeUnit.SECONDS).blockingAwait(); - assertTrue(transport.send("TEST").blockingAwait(1, TimeUnit.SECONDS)); + ByteBuffer sendBuffer = TestUtils.stringToByteBuffer("TEST"); + assertTrue(transport.send(sendBuffer).blockingAwait(1, TimeUnit.SECONDS)); close.onComplete(); assertEquals(headerValue.get(), "VALUE"); } @@ -241,15 +245,15 @@ public void LongPollingTransportSetsAuthorizationHeader() { .on("GET", (req) -> { if (requestCount.get() == 0) { requestCount.incrementAndGet(); - return Single.just(new HttpResponse(200, "", "")); + return Single.just(new HttpResponse(200, "", TestUtils.emptyByteBuffer)); } assertTrue(close.blockingAwait(1, TimeUnit.SECONDS)); - return Single.just(new HttpResponse(204, "", "")); + return Single.just(new HttpResponse(204, "", TestUtils.emptyByteBuffer)); }) .on("POST", (req) -> { assertFalse(req.getHeaders().isEmpty()); headerValue.set(req.getHeaders().get("Authorization")); - return Single.just(new HttpResponse(200, "", "")); + return Single.just(new HttpResponse(200, "", TestUtils.emptyByteBuffer)); }); Map headers = new HashMap<>(); @@ -258,7 +262,8 @@ public void LongPollingTransportSetsAuthorizationHeader() { transport.setOnClose((error) -> {}); transport.start("http://example.com").timeout(1, TimeUnit.SECONDS).blockingAwait(); - assertTrue(transport.send("TEST").blockingAwait(1, TimeUnit.SECONDS)); + ByteBuffer sendBuffer = TestUtils.stringToByteBuffer("TEST"); + assertTrue(transport.send(sendBuffer).blockingAwait(1, TimeUnit.SECONDS)); assertEquals(headerValue.get(), "Bearer TOKEN"); close.onComplete(); } @@ -273,17 +278,17 @@ public void LongPollingTransportRunsAccessTokenProviderEveryRequest() { .on("GET", (req) -> { if (requestCount.get() == 0) { requestCount.incrementAndGet(); - return Single.just(new HttpResponse(200, "", "")); + return Single.just(new HttpResponse(200, "", TestUtils.emptyByteBuffer)); } assertEquals("Bearer TOKEN1", req.getHeaders().get("Authorization")); secondGet.onComplete(); assertTrue(close.blockingAwait(1, TimeUnit.SECONDS)); - return Single.just(new HttpResponse(204, "", "")); + return Single.just(new HttpResponse(204, "", TestUtils.emptyByteBuffer)); }) .on("POST", (req) -> { assertFalse(req.getHeaders().isEmpty()); headerValue.set(req.getHeaders().get("Authorization")); - return Single.just(new HttpResponse(200, "", "")); + return Single.just(new HttpResponse(200, "", TestUtils.emptyByteBuffer)); }); AtomicInteger i = new AtomicInteger(0); @@ -294,7 +299,8 @@ public void LongPollingTransportRunsAccessTokenProviderEveryRequest() { transport.start("http://example.com").timeout(1, TimeUnit.SECONDS).blockingAwait(); secondGet.blockingAwait(1, TimeUnit.SECONDS); - assertTrue(transport.send("TEST").blockingAwait(1, TimeUnit.SECONDS)); + ByteBuffer sendBuffer = TestUtils.stringToByteBuffer("TEST"); + assertTrue(transport.send(sendBuffer).blockingAwait(1, TimeUnit.SECONDS)); assertEquals("Bearer TOKEN2", headerValue.get()); close.onComplete(); } @@ -307,9 +313,9 @@ public void After204StopDoesNotTriggerOnClose() { .on("GET", (req) -> { if (firstPoll.get()) { firstPoll.set(false); - return Single.just(new HttpResponse(200, "", "")); + return Single.just(new HttpResponse(200, "", TestUtils.emptyByteBuffer)); } - return Single.just(new HttpResponse(204, "", "")); + return Single.just(new HttpResponse(204, "", TestUtils.emptyByteBuffer)); }); Map headers = new HashMap<>(); @@ -341,16 +347,16 @@ public void StoppingTransportRunsCloseHandlersOnce() { .on("GET", (req) -> { if (firstPoll.get()) { firstPoll.set(false); - return Single.just(new HttpResponse(200, "", "")); + return Single.just(new HttpResponse(200, "", TestUtils.emptyByteBuffer)); } else { assertTrue(block.blockingAwait(1, TimeUnit.SECONDS)); - return Single.just(new HttpResponse(204, "", "")); + return Single.just(new HttpResponse(204, "", TestUtils.emptyByteBuffer)); } }) .on("DELETE", (req) ->{ //Unblock the last poll when we sent the DELETE request. block.onComplete(); - return Single.just(new HttpResponse(200, "", "")); + return Single.just(new HttpResponse(200, "", TestUtils.emptyByteBuffer)); }); Map headers = new HashMap<>(); diff --git a/src/SignalR/clients/java/signalr/src/test/java/com/microsoft/signalr/MessagePackHubProtocolTest.java b/src/SignalR/clients/java/signalr/src/test/java/com/microsoft/signalr/MessagePackHubProtocolTest.java new file mode 100644 index 000000000000..0b508d6e210f --- /dev/null +++ b/src/SignalR/clients/java/signalr/src/test/java/com/microsoft/signalr/MessagePackHubProtocolTest.java @@ -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 headers = new HashMap(); + 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 streamIds = new ArrayList(); + 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 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 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 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 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 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 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 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 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 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 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 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 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 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 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 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 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 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 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>() { }).getType() }, null); + + List arrayMessages = messagePackHubProtocol.parseMessages(message, arrayBinder); + message.flip(); + List 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 listArg = (ArrayList)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>() { }).getType() }, null); + + List 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 result = (HashMap)invocationMessage.getArguments()[0]; + assertEquals(2, result.size()); + assertEquals("value", result.get("key")); + assertEquals("banana", result.get("apple")); + } + + @Test + public void verifyWriteInvocationMessageWithMapArg() { + SortedMap argument = new TreeMap(); + 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>>>() { }).getType() }, null); + + List 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>> result = (ArrayList>>)invocationMessage.getArguments()[0]; + assertEquals(2, result.size()); + + HashMap> firstMap = result.get(0); + HashMap> secondMap = result.get(1); + + assertEquals(2, firstMap.keySet().size()); + assertEquals(2, secondMap.keySet().size()); + + ArrayList firstList = firstMap.get("one"); + ArrayList secondList = firstMap.get("two"); + + ArrayList thirdList = secondMap.get("three"); + ArrayList 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 clist1 = new ArrayList(); + ArrayList clist2 = new ArrayList(); + ArrayList clist3 = new ArrayList(); + ArrayList clist4 = new ArrayList(); + + clist1.add('a'); + clist1.add('b'); + + clist2.add('\ubeef'); + clist2.add('\uabcd'); + + clist3.add('5'); + clist3.add('9'); + + clist4.add('^'); + clist4.add('*'); + + TreeMap> map1 = new TreeMap>(); + TreeMap> map2 = new TreeMap>(); + + map1.put("one", clist1); + map1.put("two", clist2); + + map2.put("three", clist3); + map2.put("four", clist4); + + ArrayList>> argument = new ArrayList>>(); + 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>>() { }).getType() }, null); + + List 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> result = (PersonPojo>)invocationMessage.getArguments()[0]; + assertEquals("John", result.getFirstName()); + assertEquals("Doe", result.getLastName()); + assertEquals(30, result.getAge()); + + ArrayList 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 shorts = new ArrayList(); + shorts.add((short) 5); + shorts.add((short) 8); + + PersonPojo> person = new PersonPojo>("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)); + } +} diff --git a/src/SignalR/clients/java/signalr/src/test/java/com/microsoft/signalr/MockTransport.java b/src/SignalR/clients/java/signalr/src/test/java/com/microsoft/signalr/MockTransport.java index 6b65067c621b..919ba49e7443 100644 --- a/src/SignalR/clients/java/signalr/src/test/java/com/microsoft/signalr/MockTransport.java +++ b/src/SignalR/clients/java/signalr/src/test/java/com/microsoft/signalr/MockTransport.java @@ -3,6 +3,7 @@ package com.microsoft.signalr; +import java.nio.ByteBuffer; import java.util.ArrayList; import io.reactivex.Completable; @@ -11,14 +12,14 @@ class MockTransport implements Transport { private OnReceiveCallBack onReceiveCallBack; - private ArrayList sentMessages = new ArrayList<>(); + private ArrayList sentMessages = new ArrayList<>(); private String url; private TransportOnClosedCallback onClose; final private boolean ignorePings; final private boolean autoHandshake; final private CompletableSubject startSubject = CompletableSubject.create(); final private CompletableSubject stopSubject = CompletableSubject.create(); - private SingleSubject sendSubject = SingleSubject.create(); + private SingleSubject sendSubject = SingleSubject.create(); private static final String RECORD_SEPARATOR = "\u001e"; @@ -40,7 +41,7 @@ public Completable start(String url) { this.url = url; if (autoHandshake) { try { - onReceiveCallBack.invoke("{}" + RECORD_SEPARATOR); + onReceiveCallBack.invoke(TestUtils.stringToByteBuffer("{}" + RECORD_SEPARATOR)); } catch (Exception e) { throw new RuntimeException(e); } @@ -50,8 +51,8 @@ public Completable start(String url) { } @Override - public Completable send(String message) { - if (!(ignorePings && message.equals("{\"type\":6}" + RECORD_SEPARATOR))) { + public Completable send(ByteBuffer message) { + if (!(ignorePings && isPing(message))) { sentMessages.add(message); sendSubject.onSuccess(message); sendSubject = SingleSubject.create(); @@ -65,7 +66,7 @@ public void setOnReceive(OnReceiveCallBack callback) { } @Override - public void onReceive(String message) { + public void onReceive(ByteBuffer message) { this.onReceiveCallBack.invoke(message); } @@ -86,14 +87,18 @@ public void stopWithError(String errorMessage) { } public void receiveMessage(String message) { + this.onReceive(TestUtils.stringToByteBuffer(message)); + } + + public void receiveMessage(ByteBuffer message) { this.onReceive(message); } - public String[] getSentMessages() { - return sentMessages.toArray(new String[sentMessages.size()]); + public ByteBuffer[] getSentMessages() { + return sentMessages.toArray(new ByteBuffer[sentMessages.size()]); } - public SingleSubject getNextSentMessage() { + public SingleSubject getNextSentMessage() { return sendSubject; } @@ -108,4 +113,9 @@ public Completable getStartTask() { public Completable getStopTask() { return stopSubject; } + + private boolean isPing(ByteBuffer message) { + return (TestUtils.byteBufferToString(message).equals("{\"type\":6}" + RECORD_SEPARATOR) || + (message.array()[0] == 2 && message.array()[1] == -111 && message.array()[2] == 6)); + } } diff --git a/src/SignalR/clients/java/signalr/src/test/java/com/microsoft/signalr/PersonPojo.java b/src/SignalR/clients/java/signalr/src/test/java/com/microsoft/signalr/PersonPojo.java new file mode 100644 index 000000000000..446977351292 --- /dev/null +++ b/src/SignalR/clients/java/signalr/src/test/java/com/microsoft/signalr/PersonPojo.java @@ -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 implements Comparable> { + 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 ep) { + return 0; + } +} diff --git a/src/SignalR/clients/java/signalr/src/test/java/com/microsoft/signalr/TestBinder.java b/src/SignalR/clients/java/signalr/src/test/java/com/microsoft/signalr/TestBinder.java new file mode 100644 index 000000000000..b27ebb4154e4 --- /dev/null +++ b/src/SignalR/clients/java/signalr/src/test/java/com/microsoft/signalr/TestBinder.java @@ -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 getParameterTypes(String methodName) { + if (paramTypes == null) { + return new ArrayList<>(); + } + return new ArrayList(Arrays.asList(paramTypes)); + } +} \ No newline at end of file diff --git a/src/SignalR/clients/java/signalr/src/test/java/com/microsoft/signalr/TestHttpClient.java b/src/SignalR/clients/java/signalr/src/test/java/com/microsoft/signalr/TestHttpClient.java index ef3c27989f5e..b257076e915c 100644 --- a/src/SignalR/clients/java/signalr/src/test/java/com/microsoft/signalr/TestHttpClient.java +++ b/src/SignalR/clients/java/signalr/src/test/java/com/microsoft/signalr/TestHttpClient.java @@ -3,6 +3,7 @@ package com.microsoft.signalr; +import java.nio.ByteBuffer; import java.util.ArrayList; import java.util.List; import java.util.Map; @@ -27,7 +28,7 @@ public Single send(HttpRequest request) { } @Override - public Single send(HttpRequest request, String body) { + public Single send(HttpRequest request, ByteBuffer body) { this.sentRequests.add(request); return this.handler.invoke(request); } diff --git a/src/SignalR/clients/java/signalr/src/test/java/com/microsoft/signalr/TestUtils.java b/src/SignalR/clients/java/signalr/src/test/java/com/microsoft/signalr/TestUtils.java index 795df060e448..29b08709883b 100644 --- a/src/SignalR/clients/java/signalr/src/test/java/com/microsoft/signalr/TestUtils.java +++ b/src/SignalR/clients/java/signalr/src/test/java/com/microsoft/signalr/TestUtils.java @@ -3,21 +3,47 @@ package com.microsoft.signalr; +import java.nio.ByteBuffer; +import java.nio.charset.StandardCharsets; + class TestUtils { + + static ByteBuffer emptyByteBuffer = stringToByteBuffer(""); + static HubConnection createHubConnection(String url) { - return createHubConnection(url, new MockTransport(true), true, new TestHttpClient()); + return createHubConnection(url, new MockTransport(true), true, new TestHttpClient(), false); } static HubConnection createHubConnection(String url, Transport transport) { - return createHubConnection(url, transport, true, new TestHttpClient()); + return createHubConnection(url, transport, true, new TestHttpClient(), false); + } + + static HubConnection createHubConnection(String url, boolean withMessagePack) { + return createHubConnection(url, new MockTransport(true), true, new TestHttpClient(), withMessagePack); + } + + static HubConnection createHubConnection(String url, Transport transport, boolean withMessagePack) { + return createHubConnection(url, transport, true, new TestHttpClient(), withMessagePack); } - static HubConnection createHubConnection(String url, Transport transport, boolean skipNegotiate, HttpClient client) { + static HubConnection createHubConnection(String url, Transport transport, boolean skipNegotiate, HttpClient client, boolean withMessagePack) { HttpHubConnectionBuilder builder = HubConnectionBuilder.create(url) - .withTransportImplementation(transport) - .withHttpClient(client) - .shouldSkipNegotiate(skipNegotiate); + .withTransportImplementation(transport) + .withHttpClient(client) + .shouldSkipNegotiate(skipNegotiate); + + if (withMessagePack) { + builder = builder.withMessagePackHubProtocol(); + } return builder.build(); } + + static String byteBufferToString(ByteBuffer buffer) { + return new String(buffer.array(), StandardCharsets.UTF_8); + } + + static ByteBuffer stringToByteBuffer(String s) { + return ByteBuffer.wrap(s.getBytes(StandardCharsets.UTF_8)); + } } diff --git a/src/SignalR/clients/java/signalr/src/test/java/com/microsoft/signalr/WebSocketTransportTest.java b/src/SignalR/clients/java/signalr/src/test/java/com/microsoft/signalr/WebSocketTransportTest.java index 4aeec16836ee..62ec1e95d7bc 100644 --- a/src/SignalR/clients/java/signalr/src/test/java/com/microsoft/signalr/WebSocketTransportTest.java +++ b/src/SignalR/clients/java/signalr/src/test/java/com/microsoft/signalr/WebSocketTransportTest.java @@ -5,6 +5,7 @@ import static org.junit.jupiter.api.Assertions.*; +import java.nio.ByteBuffer; import java.util.HashMap; import java.util.Map; import java.util.concurrent.TimeUnit; @@ -35,7 +36,7 @@ public Single send(HttpRequest request) { } @Override - public Single send(HttpRequest request, String body) { + public Single send(HttpRequest request, ByteBuffer body) { return null; } @@ -71,7 +72,7 @@ public Completable stop() { } @Override - public Completable send(String message) { + public Completable send(ByteBuffer message) { return null; }