Add MessagePack support for Java SignalR Client (#23532)
* Implement ParseMessages for java messagePack client * Fix some spacing & syntax * Implement write * Tab -> Spaces * MessagePacker -> MessageBufferPacker * Tabs -> Spaces * Tabs -> Spaces * InvocationMessage may not include streamIDs * Only 1 ctor per message type * Fixup HubConnection.java * Change return type of parseMessages to List * Fix HubConnection * Check for primitive value before returning * Implement length header prefix * Minor fixes * Use ByteBuffer to read length header * Add case for Char * Close unpacker * Typo * Override onMessage w/ ByteString * Change OKHttpWebSocketWrapper * Account for nil InvocationId * Change interface & MessagePack impl * Update JsonHubProtocol * Use ByteBuffer * Fixup HubConnection * Fixup more stuff * Convert more stuff to ByteBuffer * Account for ReadOnly * Spacing * No need to reset ByteBuffer when setting position * Add Protocol to HubConnection ctor * Set default, make stuff public * Fixup tests * More test cleanup * Spacing * only grab remaining buffer bytes in json * Last test fixes * Get rid of some unused imports * First round of msgpack tests * Flip condition * Respond to feedback * Spacing * More tests * Add test for primitives * Add more tests, start using msgpack-jackson * Fix build.gradle * Remove debug prints * Start using Type instead of Class * Add overloads for Type, make messagePack readValue() more efficient * Apply feedback, add some tests * Add some tests, fix some tests * Fix tests for real * Add a whole buncha tests * Add TestUtils change that I didn't commit yesterday * Respond to some feedback * Add a couple Json tests * Apply more feedback * Move readonly fix to msgpack * Minor optimization * Fixup some javadocs * Respond to feedback * Remove TypeReference, make Protocols private again * Feedback
This commit is contained in:
parent
901ae06bb8
commit
8522ba8e55
|
|
@ -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 {
|
||||
|
|
|
|||
|
|
@ -3,6 +3,7 @@
|
|||
|
||||
package com.microsoft.signalr;
|
||||
|
||||
import java.lang.reflect.Type;
|
||||
import java.util.ArrayList;
|
||||
import java.util.HashMap;
|
||||
import java.util.List;
|
||||
|
|
@ -13,10 +14,10 @@ class CallbackMap {
|
|||
private final Map<String, List<InvocationHandler>> handlers = new HashMap<>();
|
||||
private final ReentrantLock lock = new ReentrantLock();
|
||||
|
||||
public InvocationHandler put(String target, ActionBase action, Class<?>... classes) {
|
||||
public InvocationHandler put(String target, ActionBase action, Type... types) {
|
||||
try {
|
||||
lock.lock();
|
||||
InvocationHandler handler = new InvocationHandler(action, classes);
|
||||
InvocationHandler handler = new InvocationHandler(action, types);
|
||||
if (!handlers.containsKey(target)) {
|
||||
handlers.put(target, new ArrayList<>());
|
||||
}
|
||||
|
|
|
|||
|
|
@ -3,14 +3,28 @@
|
|||
|
||||
package com.microsoft.signalr;
|
||||
|
||||
import java.util.Map;
|
||||
|
||||
final class CancelInvocationMessage extends HubMessage {
|
||||
private final int type = HubMessageType.CANCEL_INVOCATION.value;
|
||||
private Map<String, String> headers;
|
||||
private final String invocationId;
|
||||
|
||||
public CancelInvocationMessage(String invocationId) {
|
||||
|
||||
public CancelInvocationMessage(Map<String, String> headers, String invocationId) {
|
||||
if (headers != null && !headers.isEmpty()) {
|
||||
this.headers = headers;
|
||||
}
|
||||
this.invocationId = invocationId;
|
||||
}
|
||||
|
||||
public Map<String, String> getHeaders() {
|
||||
return headers;
|
||||
}
|
||||
|
||||
public String getInvocationId() {
|
||||
return invocationId;
|
||||
}
|
||||
|
||||
@Override
|
||||
public HubMessageType getMessageType() {
|
||||
return HubMessageType.CANCEL_INVOCATION;
|
||||
|
|
|
|||
|
|
@ -5,6 +5,7 @@ package com.microsoft.signalr;
|
|||
|
||||
final class CloseMessage extends HubMessage {
|
||||
private final String error;
|
||||
private final boolean allowReconnect;
|
||||
|
||||
@Override
|
||||
public HubMessageType getMessageType() {
|
||||
|
|
@ -12,14 +13,27 @@ final class CloseMessage extends HubMessage {
|
|||
}
|
||||
|
||||
public CloseMessage() {
|
||||
this(null);
|
||||
this(null, false);
|
||||
}
|
||||
|
||||
public CloseMessage(String error) {
|
||||
this(error, false);
|
||||
}
|
||||
|
||||
public CloseMessage(boolean allowReconnect) {
|
||||
this(null, allowReconnect);
|
||||
}
|
||||
|
||||
public CloseMessage(String error, boolean allowReconnect) {
|
||||
this.error = error;
|
||||
this.allowReconnect = allowReconnect;
|
||||
}
|
||||
|
||||
public String getError() {
|
||||
return this.error;
|
||||
}
|
||||
|
||||
public boolean getAllowReconnect() {
|
||||
return this.allowReconnect;
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -3,13 +3,19 @@
|
|||
|
||||
package com.microsoft.signalr;
|
||||
|
||||
import java.util.Map;
|
||||
|
||||
final class CompletionMessage extends HubMessage {
|
||||
private final int type = HubMessageType.COMPLETION.value;
|
||||
private Map<String, String> headers;
|
||||
private final String invocationId;
|
||||
private final Object result;
|
||||
private final String error;
|
||||
|
||||
public CompletionMessage(String invocationId, Object result, String error) {
|
||||
|
||||
public CompletionMessage(Map<String, String> headers, String invocationId, Object result, String error) {
|
||||
if (headers != null && !headers.isEmpty()) {
|
||||
this.headers = headers;
|
||||
}
|
||||
if (error != null && result != null) {
|
||||
throw new IllegalArgumentException("Expected either 'error' or 'result' to be provided, but not both.");
|
||||
}
|
||||
|
|
@ -17,6 +23,10 @@ final class CompletionMessage extends HubMessage {
|
|||
this.result = result;
|
||||
this.error = error;
|
||||
}
|
||||
|
||||
public Map<String, String> getHeaders() {
|
||||
return headers;
|
||||
}
|
||||
|
||||
public Object getResult() {
|
||||
return result;
|
||||
|
|
|
|||
|
|
@ -4,6 +4,7 @@
|
|||
package com.microsoft.signalr;
|
||||
|
||||
import java.io.IOException;
|
||||
import java.nio.ByteBuffer;
|
||||
import java.util.ArrayList;
|
||||
import java.util.Collection;
|
||||
import java.util.List;
|
||||
|
|
@ -15,6 +16,7 @@ import java.util.concurrent.locks.ReentrantLock;
|
|||
import io.reactivex.Single;
|
||||
import io.reactivex.subjects.SingleSubject;
|
||||
import okhttp3.*;
|
||||
import okio.ByteString;
|
||||
|
||||
final class DefaultHttpClient extends HttpClient {
|
||||
private OkHttpClient client = null;
|
||||
|
|
@ -104,7 +106,7 @@ final class DefaultHttpClient extends HttpClient {
|
|||
}
|
||||
|
||||
@Override
|
||||
public Single<HttpResponse> send(HttpRequest httpRequest, String bodyContent) {
|
||||
public Single<HttpResponse> send(HttpRequest httpRequest, ByteBuffer bodyContent) {
|
||||
Request.Builder requestBuilder = new Request.Builder().url(httpRequest.getUrl());
|
||||
|
||||
switch (httpRequest.getMethod()) {
|
||||
|
|
@ -114,7 +116,7 @@ final class DefaultHttpClient extends HttpClient {
|
|||
case "POST":
|
||||
RequestBody body;
|
||||
if (bodyContent != null) {
|
||||
body = RequestBody.create(MediaType.parse("text/plain"), bodyContent);
|
||||
body = RequestBody.create(MediaType.parse("text/plain"), ByteString.of(bodyContent));
|
||||
} else {
|
||||
body = RequestBody.create(null, new byte[]{});
|
||||
}
|
||||
|
|
@ -150,7 +152,7 @@ final class DefaultHttpClient extends HttpClient {
|
|||
@Override
|
||||
public void onResponse(Call call, Response response) throws IOException {
|
||||
try (ResponseBody body = response.body()) {
|
||||
HttpResponse httpResponse = new HttpResponse(response.code(), response.message(), body.string());
|
||||
HttpResponse httpResponse = new HttpResponse(response.code(), response.message(), ByteBuffer.wrap(body.bytes()));
|
||||
responseSubject.onSuccess(httpResponse);
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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) {
|
||||
|
|
|
|||
|
|
@ -3,6 +3,7 @@
|
|||
|
||||
package com.microsoft.signalr;
|
||||
|
||||
import java.nio.ByteBuffer;
|
||||
import java.util.HashMap;
|
||||
import java.util.Map;
|
||||
|
||||
|
|
@ -45,23 +46,23 @@ class HttpRequest {
|
|||
class HttpResponse {
|
||||
private final int statusCode;
|
||||
private final String statusText;
|
||||
private final String content;
|
||||
private final ByteBuffer content;
|
||||
|
||||
public HttpResponse(int statusCode) {
|
||||
this(statusCode, "");
|
||||
}
|
||||
|
||||
public HttpResponse(int statusCode, String statusText) {
|
||||
this(statusCode, statusText, "");
|
||||
this(statusCode, statusText, ByteBuffer.wrap(new byte[] {}));
|
||||
}
|
||||
|
||||
public HttpResponse(int statusCode, String statusText, String content) {
|
||||
public HttpResponse(int statusCode, String statusText, ByteBuffer content) {
|
||||
this.statusCode = statusCode;
|
||||
this.statusText = statusText;
|
||||
this.content = content;
|
||||
}
|
||||
|
||||
public String getContent() {
|
||||
public ByteBuffer getContent() {
|
||||
return content;
|
||||
}
|
||||
|
||||
|
|
@ -95,7 +96,7 @@ abstract class HttpClient implements AutoCloseable {
|
|||
return this.send(request);
|
||||
}
|
||||
|
||||
public Single<HttpResponse> post(String url, String body, HttpRequest options) {
|
||||
public Single<HttpResponse> post(String url, ByteBuffer body, HttpRequest options) {
|
||||
options.setUrl(url);
|
||||
options.setMethod("POST");
|
||||
return this.send(options, body);
|
||||
|
|
@ -122,7 +123,7 @@ abstract class HttpClient implements AutoCloseable {
|
|||
|
||||
public abstract Single<HttpResponse> send(HttpRequest request);
|
||||
|
||||
public abstract Single<HttpResponse> send(HttpRequest request, String body);
|
||||
public abstract Single<HttpResponse> send(HttpRequest request, ByteBuffer body);
|
||||
|
||||
public abstract WebSocketWrapper createWebSocket(String url, Map<String, String> headers);
|
||||
|
||||
|
|
|
|||
|
|
@ -16,6 +16,7 @@ public class HttpHubConnectionBuilder {
|
|||
private final String url;
|
||||
private Transport transport;
|
||||
private HttpClient httpClient;
|
||||
private HubProtocol protocol = new JsonHubProtocol();
|
||||
private boolean skipNegotiate;
|
||||
private Single<String> accessTokenProvider;
|
||||
private long handshakeResponseTimeout = 0;
|
||||
|
|
@ -54,6 +55,16 @@ public class HttpHubConnectionBuilder {
|
|||
this.httpClient = httpClient;
|
||||
return this;
|
||||
}
|
||||
|
||||
/**
|
||||
* Sets MessagePack as the {@link HubProtocol} to be used by the {@link HubConnection}.
|
||||
*
|
||||
* @return This instance of the HttpHubConnectionBuilder.
|
||||
*/
|
||||
public HttpHubConnectionBuilder withMessagePackHubProtocol() {
|
||||
this.protocol = new MessagePackHubProtocol();
|
||||
return this;
|
||||
}
|
||||
|
||||
/**
|
||||
* Indicates to the {@link HubConnection} that it should skip the negotiate process.
|
||||
|
|
@ -133,7 +144,7 @@ public class HttpHubConnectionBuilder {
|
|||
* @return A new instance of {@link HubConnection}.
|
||||
*/
|
||||
public HubConnection build() {
|
||||
return new HubConnection(url, transport, skipNegotiate, httpClient, accessTokenProvider,
|
||||
return new HubConnection(url, transport, skipNegotiate, httpClient, protocol, accessTokenProvider,
|
||||
handshakeResponseTimeout, headers, transportEnum, configureBuilder);
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -4,6 +4,10 @@
|
|||
package com.microsoft.signalr;
|
||||
|
||||
import java.io.StringReader;
|
||||
import java.lang.reflect.Type;
|
||||
import java.lang.reflect.TypeVariable;
|
||||
import java.nio.ByteBuffer;
|
||||
import java.nio.charset.StandardCharsets;
|
||||
import java.util.*;
|
||||
import java.util.concurrent.*;
|
||||
import java.util.concurrent.atomic.AtomicInteger;
|
||||
|
|
@ -26,8 +30,8 @@ import okhttp3.OkHttpClient;
|
|||
* A connection used to invoke hub methods on a SignalR Server.
|
||||
*/
|
||||
public class HubConnection implements AutoCloseable {
|
||||
private static final String RECORD_SEPARATOR = "\u001e";
|
||||
private static final List<Class<?>> emptyArray = new ArrayList<>();
|
||||
private static final byte RECORD_SEPARATOR = 0x1e;
|
||||
private static final List<Type> emptyArray = new ArrayList<>();
|
||||
private static final int MAX_NEGOTIATE_ATTEMPTS = 100;
|
||||
|
||||
private String baseUrl;
|
||||
|
|
@ -126,7 +130,7 @@ public class HubConnection implements AutoCloseable {
|
|||
return transport;
|
||||
}
|
||||
|
||||
HubConnection(String url, Transport transport, boolean skipNegotiate, HttpClient httpClient,
|
||||
HubConnection(String url, Transport transport, boolean skipNegotiate, HttpClient httpClient, HubProtocol protocol,
|
||||
Single<String> accessTokenProvider, long handshakeResponseTimeout, Map<String, String> headers, TransportEnum transportEnum,
|
||||
Action1<OkHttpClient.Builder> configureBuilder) {
|
||||
if (url == null || url.isEmpty()) {
|
||||
|
|
@ -134,7 +138,7 @@ public class HubConnection implements AutoCloseable {
|
|||
}
|
||||
|
||||
this.baseUrl = url;
|
||||
this.protocol = new JsonHubProtocol();
|
||||
this.protocol = protocol;
|
||||
|
||||
if (accessTokenProvider != null) {
|
||||
this.accessTokenProvider = accessTokenProvider;
|
||||
|
|
@ -165,8 +169,20 @@ public class HubConnection implements AutoCloseable {
|
|||
this.callback = (payload) -> {
|
||||
resetServerTimeout();
|
||||
if (!handshakeReceived) {
|
||||
int handshakeLength = payload.indexOf(RECORD_SEPARATOR) + 1;
|
||||
String handshakeResponseString = payload.substring(0, handshakeLength - 1);
|
||||
List<Byte> handshakeByteList = new ArrayList<Byte>();
|
||||
byte curr = payload.get();
|
||||
// Add the handshake to handshakeBytes, but not the record separator
|
||||
while (curr != RECORD_SEPARATOR) {
|
||||
handshakeByteList.add(curr);
|
||||
curr = payload.get();
|
||||
}
|
||||
int handshakeLength = handshakeByteList.size() + 1;
|
||||
byte[] handshakeBytes = new byte[handshakeLength - 1];
|
||||
for (int i = 0; i < handshakeLength - 1; i++) {
|
||||
handshakeBytes[i] = handshakeByteList.get(i);
|
||||
}
|
||||
// The handshake will always be a UTF8 Json string
|
||||
String handshakeResponseString = new String(handshakeBytes, StandardCharsets.UTF_8);
|
||||
HandshakeResponseMessage handshakeResponse;
|
||||
try {
|
||||
handshakeResponse = HandshakeProtocol.parseHandshakeResponse(handshakeResponseString);
|
||||
|
|
@ -185,14 +201,13 @@ public class HubConnection implements AutoCloseable {
|
|||
handshakeReceived = true;
|
||||
handshakeResponseSubject.onComplete();
|
||||
|
||||
payload = payload.substring(handshakeLength);
|
||||
// The payload only contained the handshake response so we can return.
|
||||
if (payload.length() == 0) {
|
||||
if (!payload.hasRemaining()) {
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
HubMessage[] messages = protocol.parseMessages(payload, connectionState);
|
||||
|
||||
List<HubMessage> messages = protocol.parseMessages(payload, connectionState);
|
||||
|
||||
for (HubMessage message : messages) {
|
||||
logger.debug("Received message of type {}.", message.getMessageType());
|
||||
|
|
@ -271,7 +286,7 @@ public class HubConnection implements AutoCloseable {
|
|||
throw new RuntimeException(String.format("Unexpected status code returned from negotiate: %d %s.",
|
||||
response.getStatusCode(), response.getStatusText()));
|
||||
}
|
||||
JsonReader reader = new JsonReader(new StringReader(response.getContent()));
|
||||
JsonReader reader = new JsonReader(new StringReader(new String(response.getContent().array(), StandardCharsets.UTF_8)));
|
||||
NegotiateResponse negotiateResponse = new NegotiateResponse(reader);
|
||||
|
||||
if (negotiateResponse.getError() != null) {
|
||||
|
|
@ -372,7 +387,7 @@ public class HubConnection implements AutoCloseable {
|
|||
transport.setOnClose((message) -> stopConnection(message));
|
||||
|
||||
return transport.start(negotiateResponse.getFinalUrl()).andThen(Completable.defer(() -> {
|
||||
String handshake = HandshakeProtocol.createHandshakeRequestMessage(
|
||||
ByteBuffer handshake = HandshakeProtocol.createHandshakeRequestMessage(
|
||||
new HandshakeRequestMessage(protocol.getName(), protocol.getVersion()));
|
||||
|
||||
connectionState = new ConnectionState(this);
|
||||
|
|
@ -585,9 +600,9 @@ public class HubConnection implements AutoCloseable {
|
|||
args = checkUploadStream(args, streamIds);
|
||||
InvocationMessage invocationMessage;
|
||||
if (isStreamInvocation) {
|
||||
invocationMessage = new StreamInvocationMessage(id, method, args, streamIds);
|
||||
invocationMessage = new StreamInvocationMessage(null, id, method, args, streamIds);
|
||||
} else {
|
||||
invocationMessage = new InvocationMessage(id, method, args, streamIds);
|
||||
invocationMessage = new InvocationMessage(null, id, method, args, streamIds);
|
||||
}
|
||||
|
||||
sendHubMessage(invocationMessage);
|
||||
|
|
@ -602,13 +617,13 @@ public class HubConnection implements AutoCloseable {
|
|||
for (String streamId: streamIds) {
|
||||
Observable observable = this.streamMap.get(streamId);
|
||||
observable.subscribe(
|
||||
(item) -> sendHubMessage(new StreamItem(streamId, item)),
|
||||
(item) -> sendHubMessage(new StreamItem(null, streamId, item)),
|
||||
(error) -> {
|
||||
sendHubMessage(new CompletionMessage(streamId, null, error.toString()));
|
||||
sendHubMessage(new CompletionMessage(null, streamId, null, error.toString()));
|
||||
this.streamMap.remove(streamId);
|
||||
},
|
||||
() -> {
|
||||
sendHubMessage(new CompletionMessage(streamId, null, null));
|
||||
sendHubMessage(new CompletionMessage(null, streamId, null, null));
|
||||
this.streamMap.remove(streamId);
|
||||
});
|
||||
}
|
||||
|
|
@ -678,8 +693,26 @@ public class HubConnection implements AutoCloseable {
|
|||
* @param <T> The expected return type.
|
||||
* @return A Single that yields the return value when the invocation has completed.
|
||||
*/
|
||||
@SuppressWarnings("unchecked")
|
||||
public <T> Single<T> invoke(Class<T> returnType, String method, Object... args) {
|
||||
return this.<T>invoke(returnType, returnType, method, args);
|
||||
}
|
||||
|
||||
/**
|
||||
* Invokes a hub method on the server using the specified method name and arguments.
|
||||
*
|
||||
* @param returnType The expected return type.
|
||||
* @param method The name of the server method to invoke.
|
||||
* @param args The arguments used to invoke the server method.
|
||||
* @param <T> The expected return type.
|
||||
* @return A Single that yields the return value when the invocation has completed.
|
||||
*/
|
||||
public <T> Single<T> invoke(Type returnType, String method, Object... args) {
|
||||
Class<?> returnClass = Utils.typeToClass(returnType);
|
||||
return this.<T>invoke(returnType, returnClass, method, args);
|
||||
}
|
||||
|
||||
@SuppressWarnings("unchecked")
|
||||
private <T> Single<T> invoke(Type returnType, Class<?> returnClass, String method, Object... args) {
|
||||
hubConnectionStateLock.lock();
|
||||
try {
|
||||
if (hubConnectionState != HubConnectionState.CONNECTED) {
|
||||
|
|
@ -697,10 +730,10 @@ public class HubConnection implements AutoCloseable {
|
|||
Subject<Object> pendingCall = irq.getPendingCall();
|
||||
pendingCall.subscribe(result -> {
|
||||
// Primitive types can't be cast with the Class cast function
|
||||
if (returnType.isPrimitive()) {
|
||||
if (returnClass.isPrimitive()) {
|
||||
subject.onSuccess((T)result);
|
||||
} else {
|
||||
subject.onSuccess(returnType.cast(result));
|
||||
subject.onSuccess((T)returnClass.cast(result));
|
||||
}
|
||||
}, error -> subject.onError(error));
|
||||
|
||||
|
|
@ -722,8 +755,26 @@ public class HubConnection implements AutoCloseable {
|
|||
* @param <T> The expected return type.
|
||||
* @return An observable that yields the streaming results from the server.
|
||||
*/
|
||||
@SuppressWarnings("unchecked")
|
||||
public <T> Observable<T> stream(Class<T> returnType, String method, Object ... args) {
|
||||
return this.<T>stream(returnType, returnType, method, args);
|
||||
}
|
||||
|
||||
/**
|
||||
* Invokes a streaming hub method on the server using the specified name and arguments.
|
||||
*
|
||||
* @param returnType The expected return type of the stream items.
|
||||
* @param method The name of the server method to invoke.
|
||||
* @param args The arguments used to invoke the server method.
|
||||
* @param <T> The expected return type.
|
||||
* @return An observable that yields the streaming results from the server.
|
||||
*/
|
||||
public <T> Observable<T> stream(Type returnType, String method, Object ... args) {
|
||||
Class<?> returnClass = Utils.typeToClass(returnType);
|
||||
return this.<T>stream(returnType, returnClass, method, args);
|
||||
}
|
||||
|
||||
@SuppressWarnings("unchecked")
|
||||
private <T> Observable<T> stream(Type returnType, Class<?> returnClass, String method, Object ... args) {
|
||||
String invocationId;
|
||||
InvocationRequest irq;
|
||||
hubConnectionStateLock.lock();
|
||||
|
|
@ -741,10 +792,10 @@ public class HubConnection implements AutoCloseable {
|
|||
Subject<Object> pendingCall = irq.getPendingCall();
|
||||
pendingCall.subscribe(result -> {
|
||||
// Primitive types can't be cast with the Class cast function
|
||||
if (returnType.isPrimitive()) {
|
||||
if (returnClass.isPrimitive()) {
|
||||
subject.onNext((T)result);
|
||||
} else {
|
||||
subject.onNext(returnType.cast(result));
|
||||
subject.onNext((T)returnClass.cast(result));
|
||||
}
|
||||
}, error -> subject.onError(error),
|
||||
() -> subject.onComplete());
|
||||
|
|
@ -753,7 +804,7 @@ public class HubConnection implements AutoCloseable {
|
|||
sendInvocationMessage(method, args, invocationId, true);
|
||||
return observable.doOnDispose(() -> {
|
||||
if (subscriptionCount.decrementAndGet() == 0) {
|
||||
CancelInvocationMessage cancelInvocationMessage = new CancelInvocationMessage(invocationId);
|
||||
CancelInvocationMessage cancelInvocationMessage = new CancelInvocationMessage(null, invocationId);
|
||||
sendHubMessage(cancelInvocationMessage);
|
||||
if (connectionState != null) {
|
||||
connectionState.tryRemoveInvocation(invocationId);
|
||||
|
|
@ -767,7 +818,7 @@ public class HubConnection implements AutoCloseable {
|
|||
}
|
||||
|
||||
private void sendHubMessage(HubMessage message) {
|
||||
String serializedMessage = protocol.writeMessage(message);
|
||||
ByteBuffer serializedMessage = protocol.writeMessage(message);
|
||||
if (message.getMessageType() == HubMessageType.INVOCATION ) {
|
||||
logger.debug("Sending {} message '{}'.", message.getMessageType().name(), ((InvocationMessage)message).getInvocationId());
|
||||
} else if (message.getMessageType() == HubMessageType.STREAM_INVOCATION) {
|
||||
|
|
@ -825,6 +876,7 @@ public class HubConnection implements AutoCloseable {
|
|||
|
||||
/**
|
||||
* Registers a handler that will be invoked when the hub method with the specified method name is invoked.
|
||||
* Should be used for primitives and non-generic classes.
|
||||
*
|
||||
* @param target The name of the hub method to define.
|
||||
* @param callback The handler that will be raised when the hub method is invoked.
|
||||
|
|
@ -840,6 +892,7 @@ public class HubConnection implements AutoCloseable {
|
|||
|
||||
/**
|
||||
* Registers a handler that will be invoked when the hub method with the specified method name is invoked.
|
||||
* Should be used for primitives and non-generic classes.
|
||||
*
|
||||
* @param target The name of the hub method to define.
|
||||
* @param callback The handler that will be raised when the hub method is invoked.
|
||||
|
|
@ -858,6 +911,7 @@ public class HubConnection implements AutoCloseable {
|
|||
|
||||
/**
|
||||
* Registers a handler that will be invoked when the hub method with the specified method name is invoked.
|
||||
* Should be used for primitives and non-generic classes.
|
||||
*
|
||||
* @param target The name of the hub method to define.
|
||||
* @param callback The handler that will be raised when the hub method is invoked.
|
||||
|
|
@ -879,6 +933,7 @@ public class HubConnection implements AutoCloseable {
|
|||
|
||||
/**
|
||||
* Registers a handler that will be invoked when the hub method with the specified method name is invoked.
|
||||
* Should be used for primitives and non-generic classes.
|
||||
*
|
||||
* @param target The name of the hub method to define.
|
||||
* @param callback The handler that will be raised when the hub method is invoked.
|
||||
|
|
@ -902,6 +957,7 @@ public class HubConnection implements AutoCloseable {
|
|||
|
||||
/**
|
||||
* Registers a handler that will be invoked when the hub method with the specified method name is invoked.
|
||||
* Should be used for primitives and non-generic classes.
|
||||
*
|
||||
* @param target The name of the hub method to define.
|
||||
* @param callback The handler that will be raised when the hub method is invoked.
|
||||
|
|
@ -928,6 +984,7 @@ public class HubConnection implements AutoCloseable {
|
|||
|
||||
/**
|
||||
* Registers a handler that will be invoked when the hub method with the specified method name is invoked.
|
||||
* Should be used for primitives and non-generic classes.
|
||||
*
|
||||
* @param target The name of the hub method to define.
|
||||
* @param callback The handler that will be raised when the hub method is invoked.
|
||||
|
|
@ -956,6 +1013,7 @@ public class HubConnection implements AutoCloseable {
|
|||
|
||||
/**
|
||||
* Registers a handler that will be invoked when the hub method with the specified method name is invoked.
|
||||
* Should be used for primitives and non-generic classes.
|
||||
*
|
||||
* @param target The name of the hub method to define.
|
||||
* @param callback The handler that will be raised when the hub method is invoked.
|
||||
|
|
@ -986,6 +1044,7 @@ public class HubConnection implements AutoCloseable {
|
|||
|
||||
/**
|
||||
* Registers a handler that will be invoked when the hub method with the specified method name is invoked.
|
||||
* Should be used for primitives and non-generic classes.
|
||||
*
|
||||
* @param target The name of the hub method to define.
|
||||
* @param callback The handler that will be raised when the hub method is invoked.
|
||||
|
|
@ -1016,7 +1075,224 @@ public class HubConnection implements AutoCloseable {
|
|||
return registerHandler(target, action, param1, param2, param3, param4, param5, param6, param7, param8);
|
||||
}
|
||||
|
||||
private Subscription registerHandler(String target, ActionBase action, Class<?>... types) {
|
||||
/**
|
||||
* Registers a handler that will be invoked when the hub method with the specified method name is invoked.
|
||||
* Should be used for generic classes and Parameterized Collections, like List or Map.
|
||||
*
|
||||
* @param target The name of the hub method to define.
|
||||
* @param callback The handler that will be raised when the hub method is invoked.
|
||||
* @param param1 The first parameter.
|
||||
* @param <T1> The first argument type.
|
||||
* @return A {@link Subscription} that can be disposed to unsubscribe from the hub method.
|
||||
*/
|
||||
@SuppressWarnings("unchecked")
|
||||
public <T1> Subscription on(String target, Action1<T1> callback, Type param1) {
|
||||
ActionBase action = params -> callback.invoke((T1)Utils.typeToClass(param1).cast(params[0]));
|
||||
return registerHandler(target, action, param1);
|
||||
}
|
||||
|
||||
/**
|
||||
* Registers a handler that will be invoked when the hub method with the specified method name is invoked.
|
||||
* Should be used for generic classes and Parameterized Collections, like List or Map.
|
||||
*
|
||||
* @param target The name of the hub method to define.
|
||||
* @param callback The handler that will be raised when the hub method is invoked.
|
||||
* @param param1 The first parameter.
|
||||
* @param param2 The second parameter.
|
||||
* @param <T1> The first parameter type.
|
||||
* @param <T2> The second parameter type.
|
||||
* @return A {@link Subscription} that can be disposed to unsubscribe from the hub method.
|
||||
*/
|
||||
@SuppressWarnings("unchecked")
|
||||
public <T1, T2> Subscription on(String target, Action2<T1, T2> callback, Type param1, Type param2) {
|
||||
ActionBase action = params -> {
|
||||
callback.invoke((T1)Utils.typeToClass(param1).cast(params[0]), (T2)Utils.typeToClass(param2).cast(params[1]));
|
||||
};
|
||||
return registerHandler(target, action, param1, param2);
|
||||
}
|
||||
|
||||
/**
|
||||
* Registers a handler that will be invoked when the hub method with the specified method name is invoked.
|
||||
* Should be used for generic classes and Parameterized Collections, like List or Map.
|
||||
*
|
||||
* @param target The name of the hub method to define.
|
||||
* @param callback The handler that will be raised when the hub method is invoked.
|
||||
* @param param1 The first parameter.
|
||||
* @param param2 The second parameter.
|
||||
* @param param3 The third parameter.
|
||||
* @param <T1> The first parameter type.
|
||||
* @param <T2> The second parameter type.
|
||||
* @param <T3> The third parameter type.
|
||||
* @return A {@link Subscription} that can be disposed to unsubscribe from the hub method.
|
||||
*/
|
||||
@SuppressWarnings("unchecked")
|
||||
public <T1, T2, T3> Subscription on(String target, Action3<T1, T2, T3> callback,
|
||||
Type param1, Type param2, Type param3) {
|
||||
ActionBase action = params -> {
|
||||
callback.invoke((T1)Utils.typeToClass(param1).cast(params[0]), (T2)Utils.typeToClass(param2).cast(params[1]),
|
||||
(T3)Utils.typeToClass(param3).cast(params[2]));
|
||||
};
|
||||
return registerHandler(target, action, param1, param2, param3);
|
||||
}
|
||||
|
||||
/**
|
||||
* Registers a handler that will be invoked when the hub method with the specified method name is invoked.
|
||||
* Should be used for generic classes and Parameterized Collections, like List or Map.
|
||||
*
|
||||
* @param target The name of the hub method to define.
|
||||
* @param callback The handler that will be raised when the hub method is invoked.
|
||||
* @param param1 The first parameter.
|
||||
* @param param2 The second parameter.
|
||||
* @param param3 The third parameter.
|
||||
* @param param4 The fourth parameter.
|
||||
* @param <T1> The first parameter type.
|
||||
* @param <T2> The second parameter type.
|
||||
* @param <T3> The third parameter type.
|
||||
* @param <T4> The fourth parameter type.
|
||||
* @return A {@link Subscription} that can be disposed to unsubscribe from the hub method.
|
||||
*/
|
||||
@SuppressWarnings("unchecked")
|
||||
public <T1, T2, T3, T4> Subscription on(String target, Action4<T1, T2, T3, T4> callback,
|
||||
Type param1, Type param2, Type param3, Type param4) {
|
||||
ActionBase action = params -> {
|
||||
callback.invoke((T1)Utils.typeToClass(param1).cast(params[0]), (T2)Utils.typeToClass(param2).cast(params[1]),
|
||||
(T3)Utils.typeToClass(param3).cast(params[2]), (T4)Utils.typeToClass(param4).cast(params[3]));
|
||||
};
|
||||
return registerHandler(target, action, param1, param2, param3, param4);
|
||||
}
|
||||
|
||||
/**
|
||||
* Registers a handler that will be invoked when the hub method with the specified method name is invoked.
|
||||
* Should be used for generic classes and Parameterized Collections, like List or Map.
|
||||
*
|
||||
* @param target The name of the hub method to define.
|
||||
* @param callback The handler that will be raised when the hub method is invoked.
|
||||
* @param param1 The first parameter.
|
||||
* @param param2 The second parameter.
|
||||
* @param param3 The third parameter.
|
||||
* @param param4 The fourth parameter.
|
||||
* @param param5 The fifth parameter.
|
||||
* @param <T1> The first parameter type.
|
||||
* @param <T2> The second parameter type.
|
||||
* @param <T3> The third parameter type.
|
||||
* @param <T4> The fourth parameter type.
|
||||
* @param <T5> The fifth parameter type.
|
||||
* @return A {@link Subscription} that can be disposed to unsubscribe from the hub method.
|
||||
*/
|
||||
@SuppressWarnings("unchecked")
|
||||
public <T1, T2, T3, T4, T5> Subscription on(String target, Action5<T1, T2, T3, T4, T5> callback,
|
||||
Type param1, Type param2, Type param3, Type param4, Type param5) {
|
||||
ActionBase action = params -> {
|
||||
callback.invoke((T1)Utils.typeToClass(param1).cast(params[0]), (T2)Utils.typeToClass(param2).cast(params[1]),
|
||||
(T3)Utils.typeToClass(param3).cast(params[2]), (T4)Utils.typeToClass(param4).cast(params[3]),
|
||||
(T5)Utils.typeToClass(param5).cast(params[4]));
|
||||
};
|
||||
return registerHandler(target, action, param1, param2, param3, param4, param5);
|
||||
}
|
||||
|
||||
/**
|
||||
* Registers a handler that will be invoked when the hub method with the specified method name is invoked.
|
||||
* Should be used for generic classes and Parameterized Collections, like List or Map.
|
||||
*
|
||||
* @param target The name of the hub method to define.
|
||||
* @param callback The handler that will be raised when the hub method is invoked.
|
||||
* @param param1 The first parameter.
|
||||
* @param param2 The second parameter.
|
||||
* @param param3 The third parameter.
|
||||
* @param param4 The fourth parameter.
|
||||
* @param param5 The fifth parameter.
|
||||
* @param param6 The sixth parameter.
|
||||
* @param <T1> The first parameter type.
|
||||
* @param <T2> The second parameter type.
|
||||
* @param <T3> The third parameter type.
|
||||
* @param <T4> The fourth parameter type.
|
||||
* @param <T5> The fifth parameter type.
|
||||
* @param <T6> The sixth parameter type.
|
||||
* @return A {@link Subscription} that can be disposed to unsubscribe from the hub method.
|
||||
*/
|
||||
@SuppressWarnings("unchecked")
|
||||
public <T1, T2, T3, T4, T5, T6> Subscription on(String target, Action6<T1, T2, T3, T4, T5, T6> callback,
|
||||
Type param1, Type param2, Type param3, Type param4, Type param5, Type param6) {
|
||||
ActionBase action = params -> {
|
||||
callback.invoke((T1)Utils.typeToClass(param1).cast(params[0]), (T2)Utils.typeToClass(param2).cast(params[1]),
|
||||
(T3)Utils.typeToClass(param3).cast(params[2]), (T4)Utils.typeToClass(param4).cast(params[3]),
|
||||
(T5)Utils.typeToClass(param5).cast(params[4]), (T6)Utils.typeToClass(param6).cast(params[5]));
|
||||
};
|
||||
return registerHandler(target, action, param1, param2, param3, param4, param5, param6);
|
||||
}
|
||||
|
||||
/**
|
||||
* Registers a handler that will be invoked when the hub method with the specified method name is invoked.
|
||||
* Should be used for generic classes and Parameterized Collections, like List or Map.
|
||||
*
|
||||
* @param target The name of the hub method to define.
|
||||
* @param callback The handler that will be raised when the hub method is invoked.
|
||||
* @param param1 The first parameter.
|
||||
* @param param2 The second parameter.
|
||||
* @param param3 The third parameter.
|
||||
* @param param4 The fourth parameter.
|
||||
* @param param5 The fifth parameter.
|
||||
* @param param6 The sixth parameter.
|
||||
* @param param7 The seventh parameter.
|
||||
* @param <T1> The first parameter type.
|
||||
* @param <T2> The second parameter type.
|
||||
* @param <T3> The third parameter type.
|
||||
* @param <T4> The fourth parameter type.
|
||||
* @param <T5> The fifth parameter type.
|
||||
* @param <T6> The sixth parameter type.
|
||||
* @param <T7> The seventh parameter type.
|
||||
* @return A {@link Subscription} that can be disposed to unsubscribe from the hub method.
|
||||
*/
|
||||
@SuppressWarnings("unchecked")
|
||||
public <T1, T2, T3, T4, T5, T6, T7> Subscription on(String target, Action7<T1, T2, T3, T4, T5, T6, T7> callback,
|
||||
Type param1, Type param2, Type param3, Type param4, Type param5, Type param6, Type param7) {
|
||||
ActionBase action = params -> {
|
||||
callback.invoke((T1)Utils.typeToClass(param1).cast(params[0]), (T2)Utils.typeToClass(param2).cast(params[1]),
|
||||
(T3)Utils.typeToClass(param3).cast(params[2]), (T4)Utils.typeToClass(param4).cast(params[3]),
|
||||
(T5)Utils.typeToClass(param5).cast(params[4]), (T6)Utils.typeToClass(param6).cast(params[5]),
|
||||
(T7)Utils.typeToClass(param7).cast(params[6]));
|
||||
};
|
||||
return registerHandler(target, action, param1, param2, param3, param4, param5, param6, param7);
|
||||
}
|
||||
|
||||
/**
|
||||
* Registers a handler that will be invoked when the hub method with the specified method name is invoked.
|
||||
* Should be used for generic classes and Parameterized Collections, like List or Map.
|
||||
*
|
||||
* @param target The name of the hub method to define.
|
||||
* @param callback The handler that will be raised when the hub method is invoked.
|
||||
* @param param1 The first parameter.
|
||||
* @param param2 The second parameter.
|
||||
* @param param3 The third parameter.
|
||||
* @param param4 The fourth parameter.
|
||||
* @param param5 The fifth parameter.
|
||||
* @param param6 The sixth parameter.
|
||||
* @param param7 The seventh parameter.
|
||||
* @param param8 The eighth parameter
|
||||
* @param <T1> The first parameter type.
|
||||
* @param <T2> The second parameter type.
|
||||
* @param <T3> The third parameter type.
|
||||
* @param <T4> The fourth parameter type.
|
||||
* @param <T5> The fifth parameter type.
|
||||
* @param <T6> The sixth parameter type.
|
||||
* @param <T7> The seventh parameter type.
|
||||
* @param <T8> The eighth parameter type.
|
||||
* @return A {@link Subscription} that can be disposed to unsubscribe from the hub method.
|
||||
*/
|
||||
@SuppressWarnings("unchecked")
|
||||
public <T1, T2, T3, T4, T5, T6, T7, T8> Subscription on(String target, Action8<T1, T2, T3, T4, T5, T6, T7, T8> callback,
|
||||
Type param1, Type param2, Type param3, Type param4, Type param5, Type param6, Type param7,
|
||||
Type param8) {
|
||||
ActionBase action = params -> {
|
||||
callback.invoke((T1)Utils.typeToClass(param1).cast(params[0]), (T2)Utils.typeToClass(param2).cast(params[1]),
|
||||
(T3)Utils.typeToClass(param3).cast(params[2]), (T4)Utils.typeToClass(param4).cast(params[3]),
|
||||
(T5)Utils.typeToClass(param5).cast(params[4]), (T6)Utils.typeToClass(param6).cast(params[5]),
|
||||
(T7)Utils.typeToClass(param7).cast(params[6]), (T8)Utils.typeToClass(param8).cast(params[7]));
|
||||
};
|
||||
return registerHandler(target, action, param1, param2, param3, param4, param5, param6, param7, param8);
|
||||
}
|
||||
|
||||
private Subscription registerHandler(String target, ActionBase action, Type... types) {
|
||||
InvocationHandler handler = handlers.put(target, action, types);
|
||||
logger.debug("Registering handler for client method: '{}'.", target);
|
||||
return new Subscription(handlers, handler, target);
|
||||
|
|
@ -1087,7 +1363,7 @@ public class HubConnection implements AutoCloseable {
|
|||
}
|
||||
|
||||
@Override
|
||||
public Class<?> getReturnType(String invocationId) {
|
||||
public Type getReturnType(String invocationId) {
|
||||
InvocationRequest irq = getInvocation(invocationId);
|
||||
if (irq == null) {
|
||||
return null;
|
||||
|
|
@ -1097,7 +1373,7 @@ public class HubConnection implements AutoCloseable {
|
|||
}
|
||||
|
||||
@Override
|
||||
public List<Class<?>> getParameterTypes(String methodName) {
|
||||
public List<Type> getParameterTypes(String methodName) {
|
||||
List<InvocationHandler> handlers = connection.handlers.get(methodName);
|
||||
if (handlers == null) {
|
||||
logger.warn("Failed to find handler for '{}' method.", methodName);
|
||||
|
|
@ -1108,7 +1384,7 @@ public class HubConnection implements AutoCloseable {
|
|||
throw new RuntimeException(String.format("There are no callbacks registered for the method '%s'.", methodName));
|
||||
}
|
||||
|
||||
return handlers.get(0).getClasses();
|
||||
return handlers.get(0).getTypes();
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -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; }
|
||||
|
|
|
|||
|
|
@ -3,6 +3,9 @@
|
|||
|
||||
package com.microsoft.signalr;
|
||||
|
||||
import java.nio.ByteBuffer;
|
||||
import java.util.List;
|
||||
|
||||
/**
|
||||
* A protocol abstraction for communicating with SignalR hubs.
|
||||
*/
|
||||
|
|
@ -13,15 +16,15 @@ interface HubProtocol {
|
|||
|
||||
/**
|
||||
* Creates a new list of {@link HubMessage}s.
|
||||
* @param message A string representation of one or more {@link HubMessage}s.
|
||||
* @param message A ByteBuffer representation of one or more {@link HubMessage}s.
|
||||
* @return A list of {@link HubMessage}s.
|
||||
*/
|
||||
HubMessage[] parseMessages(String message, InvocationBinder binder);
|
||||
List<HubMessage> parseMessages(ByteBuffer message, InvocationBinder binder);
|
||||
|
||||
/**
|
||||
* Writes the specified {@link HubMessage} to a String.
|
||||
* @param message The message to write.
|
||||
* @return A string representation of the message.
|
||||
* @return A ByteBuffer representation of the message.
|
||||
*/
|
||||
String writeMessage(HubMessage message);
|
||||
ByteBuffer writeMessage(HubMessage message);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -3,9 +3,13 @@
|
|||
|
||||
package com.microsoft.signalr;
|
||||
|
||||
import java.lang.reflect.Type;
|
||||
import java.util.List;
|
||||
|
||||
/**
|
||||
* An abstraction for passing around information about method signatures.
|
||||
*/
|
||||
interface InvocationBinder {
|
||||
Class<?> getReturnType(String invocationId);
|
||||
List<Class<?>> getParameterTypes(String methodName);
|
||||
Type getReturnType(String invocationId);
|
||||
List<Type> getParameterTypes(String methodName);
|
||||
}
|
||||
|
|
@ -3,20 +3,21 @@
|
|||
|
||||
package com.microsoft.signalr;
|
||||
|
||||
import java.lang.reflect.Type;
|
||||
import java.util.Arrays;
|
||||
import java.util.List;
|
||||
|
||||
class InvocationHandler {
|
||||
private final List<Class<?>> classes;
|
||||
private final List<Type> types;
|
||||
private final ActionBase action;
|
||||
|
||||
InvocationHandler(ActionBase action, Class<?>... classes) {
|
||||
InvocationHandler(ActionBase action, Type... types) {
|
||||
this.action = action;
|
||||
this.classes = Arrays.asList(classes);
|
||||
this.types = Arrays.asList(types);
|
||||
}
|
||||
|
||||
public List<Class<?>> getClasses() {
|
||||
return classes;
|
||||
public List<Type> getTypes() {
|
||||
return types;
|
||||
}
|
||||
|
||||
public ActionBase getAction() {
|
||||
|
|
|
|||
|
|
@ -4,19 +4,20 @@
|
|||
package com.microsoft.signalr;
|
||||
|
||||
import java.util.Collection;
|
||||
import java.util.Map;
|
||||
|
||||
class InvocationMessage extends HubMessage {
|
||||
int type = HubMessageType.INVOCATION.value;
|
||||
private Map<String, String> headers;
|
||||
private final String invocationId;
|
||||
private final String target;
|
||||
private final Object[] arguments;
|
||||
private Collection<String> streamIds;
|
||||
|
||||
public InvocationMessage(String invocationId, String target, Object[] args) {
|
||||
this(invocationId, target, args, null);
|
||||
}
|
||||
|
||||
public InvocationMessage(String invocationId, String target, Object[] args, Collection<String> streamIds) {
|
||||
|
||||
public InvocationMessage(Map<String, String> headers, String invocationId, String target, Object[] args, Collection<String> streamIds) {
|
||||
if (headers != null && !headers.isEmpty()) {
|
||||
this.headers = headers;
|
||||
}
|
||||
this.invocationId = invocationId;
|
||||
this.target = target;
|
||||
this.arguments = args;
|
||||
|
|
@ -24,6 +25,10 @@ class InvocationMessage extends HubMessage {
|
|||
this.streamIds = streamIds;
|
||||
}
|
||||
}
|
||||
|
||||
public Map<String, String> getHeaders() {
|
||||
return headers;
|
||||
}
|
||||
|
||||
public String getInvocationId() {
|
||||
return invocationId;
|
||||
|
|
@ -36,6 +41,10 @@ class InvocationMessage extends HubMessage {
|
|||
public Object[] getArguments() {
|
||||
return arguments;
|
||||
}
|
||||
|
||||
public Collection<String> getStreamIds() {
|
||||
return streamIds;
|
||||
}
|
||||
|
||||
@Override
|
||||
public HubMessageType getMessageType() {
|
||||
|
|
|
|||
|
|
@ -3,17 +3,18 @@
|
|||
|
||||
package com.microsoft.signalr;
|
||||
|
||||
import java.lang.reflect.Type;
|
||||
import java.util.concurrent.CancellationException;
|
||||
|
||||
import io.reactivex.subjects.ReplaySubject;
|
||||
import io.reactivex.subjects.Subject;
|
||||
|
||||
class InvocationRequest {
|
||||
private final Class<?> returnType;
|
||||
private final Type returnType;
|
||||
private final Subject<Object> pendingCall = ReplaySubject.create();
|
||||
private final String invocationId;
|
||||
|
||||
InvocationRequest(Class<?> returnType, String invocationId) {
|
||||
InvocationRequest(Type returnType, String invocationId) {
|
||||
this.returnType = returnType;
|
||||
this.invocationId = invocationId;
|
||||
}
|
||||
|
|
@ -47,7 +48,7 @@ class InvocationRequest {
|
|||
return pendingCall;
|
||||
}
|
||||
|
||||
public Class<?> getReturnType() {
|
||||
public Type getReturnType() {
|
||||
return returnType;
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -5,7 +5,11 @@ package com.microsoft.signalr;
|
|||
|
||||
import java.io.IOException;
|
||||
import java.io.StringReader;
|
||||
import java.lang.reflect.Type;
|
||||
import java.nio.ByteBuffer;
|
||||
import java.nio.charset.StandardCharsets;
|
||||
import java.util.ArrayList;
|
||||
import java.util.Arrays;
|
||||
import java.util.List;
|
||||
|
||||
import com.google.gson.Gson;
|
||||
|
|
@ -36,15 +40,26 @@ class JsonHubProtocol implements HubProtocol {
|
|||
}
|
||||
|
||||
@Override
|
||||
public HubMessage[] parseMessages(String payload, InvocationBinder binder) {
|
||||
if (payload.length() == 0) {
|
||||
return new HubMessage[]{};
|
||||
public List<HubMessage> parseMessages(ByteBuffer payload, InvocationBinder binder) {
|
||||
String payloadStr;
|
||||
// If the payload is readOnly, we have to copy the bytes from its array to make the payload string
|
||||
if (payload.isReadOnly()) {
|
||||
byte[] payloadBytes = new byte[payload.remaining()];
|
||||
payload.get(payloadBytes, 0, payloadBytes.length);
|
||||
payloadStr = new String(payloadBytes, StandardCharsets.UTF_8);
|
||||
// Otherwise we can allocate directly from its array
|
||||
} else {
|
||||
// The position of the ByteBuffer may have been incremented - make sure we only grab the remaining bytes
|
||||
payloadStr = new String(payload.array(), payload.position(), payload.remaining(), StandardCharsets.UTF_8);
|
||||
}
|
||||
if (!(payload.substring(payload.length() - 1).equals(RECORD_SEPARATOR))) {
|
||||
if (payloadStr.length() == 0) {
|
||||
return null;
|
||||
}
|
||||
if (!(payloadStr.substring(payloadStr.length() - 1).equals(RECORD_SEPARATOR))) {
|
||||
throw new RuntimeException("Message is incomplete.");
|
||||
}
|
||||
|
||||
String[] messages = payload.split(RECORD_SEPARATOR);
|
||||
String[] messages = payloadStr.split(RECORD_SEPARATOR);
|
||||
List<HubMessage> hubMessages = new ArrayList<>();
|
||||
try {
|
||||
for (String str : messages) {
|
||||
|
|
@ -87,7 +102,7 @@ class JsonHubProtocol implements HubProtocol {
|
|||
if (target != null) {
|
||||
boolean startedArray = false;
|
||||
try {
|
||||
List<Class<?>> types = binder.getParameterTypes(target);
|
||||
List<Type> types = binder.getParameterTypes(target);
|
||||
startedArray = true;
|
||||
arguments = bindArguments(reader, types);
|
||||
} catch (Exception ex) {
|
||||
|
|
@ -125,7 +140,7 @@ class JsonHubProtocol implements HubProtocol {
|
|||
case INVOCATION:
|
||||
if (argumentsToken != null) {
|
||||
try {
|
||||
List<Class<?>> types = binder.getParameterTypes(target);
|
||||
List<Type> types = binder.getParameterTypes(target);
|
||||
arguments = bindArguments(argumentsToken, types);
|
||||
} catch (Exception ex) {
|
||||
argumentBindingException = ex;
|
||||
|
|
@ -135,25 +150,25 @@ class JsonHubProtocol implements HubProtocol {
|
|||
hubMessages.add(new InvocationBindingFailureMessage(invocationId, target, argumentBindingException));
|
||||
} else {
|
||||
if (arguments == null) {
|
||||
hubMessages.add(new InvocationMessage(invocationId, target, new Object[0]));
|
||||
hubMessages.add(new InvocationMessage(null, invocationId, target, new Object[0], null));
|
||||
} else {
|
||||
hubMessages.add(new InvocationMessage(invocationId, target, arguments.toArray()));
|
||||
hubMessages.add(new InvocationMessage(null, invocationId, target, arguments.toArray(), null));
|
||||
}
|
||||
}
|
||||
break;
|
||||
case COMPLETION:
|
||||
if (resultToken != null) {
|
||||
Class<?> returnType = binder.getReturnType(invocationId);
|
||||
Type returnType = binder.getReturnType(invocationId);
|
||||
result = gson.fromJson(resultToken, returnType != null ? returnType : Object.class);
|
||||
}
|
||||
hubMessages.add(new CompletionMessage(invocationId, result, error));
|
||||
hubMessages.add(new CompletionMessage(null, invocationId, result, error));
|
||||
break;
|
||||
case STREAM_ITEM:
|
||||
if (resultToken != null) {
|
||||
Class<?> returnType = binder.getReturnType(invocationId);
|
||||
Type returnType = binder.getReturnType(invocationId);
|
||||
result = gson.fromJson(resultToken, returnType != null ? returnType : Object.class);
|
||||
}
|
||||
hubMessages.add(new StreamItem(invocationId, result));
|
||||
hubMessages.add(new StreamItem(null, invocationId, result));
|
||||
break;
|
||||
case STREAM_INVOCATION:
|
||||
case CANCEL_INVOCATION:
|
||||
|
|
@ -176,15 +191,15 @@ class JsonHubProtocol implements HubProtocol {
|
|||
throw new RuntimeException("Error reading JSON.", ex);
|
||||
}
|
||||
|
||||
return hubMessages.toArray(new HubMessage[hubMessages.size()]);
|
||||
return hubMessages;
|
||||
}
|
||||
|
||||
@Override
|
||||
public String writeMessage(HubMessage hubMessage) {
|
||||
return gson.toJson(hubMessage) + RECORD_SEPARATOR;
|
||||
public ByteBuffer writeMessage(HubMessage hubMessage) {
|
||||
return ByteBuffer.wrap((gson.toJson(hubMessage) + RECORD_SEPARATOR).getBytes(StandardCharsets.UTF_8));
|
||||
}
|
||||
|
||||
private ArrayList<Object> bindArguments(JsonArray argumentsToken, List<Class<?>> paramTypes) {
|
||||
private ArrayList<Object> bindArguments(JsonArray argumentsToken, List<Type> paramTypes) {
|
||||
if (argumentsToken.size() != paramTypes.size()) {
|
||||
throw new RuntimeException(String.format("Invocation provides %d argument(s) but target expects %d.", argumentsToken.size(), paramTypes.size()));
|
||||
}
|
||||
|
|
@ -200,7 +215,7 @@ class JsonHubProtocol implements HubProtocol {
|
|||
return arguments;
|
||||
}
|
||||
|
||||
private ArrayList<Object> bindArguments(JsonReader reader, List<Class<?>> paramTypes) throws IOException {
|
||||
private ArrayList<Object> bindArguments(JsonReader reader, List<Type> paramTypes) throws IOException {
|
||||
reader.beginArray();
|
||||
int paramCount = paramTypes.size();
|
||||
int argCount = 0;
|
||||
|
|
|
|||
|
|
@ -3,6 +3,8 @@
|
|||
|
||||
package com.microsoft.signalr;
|
||||
|
||||
import java.nio.ByteBuffer;
|
||||
import java.nio.charset.StandardCharsets;
|
||||
import java.util.Map;
|
||||
import java.util.concurrent.ExecutorService;
|
||||
import java.util.concurrent.Executors;
|
||||
|
|
@ -100,7 +102,7 @@ class LongPollingTransport implements Transport {
|
|||
} else {
|
||||
if (response.getContent() != null) {
|
||||
logger.debug("Message received.");
|
||||
onReceiveThread.submit(() ->this.onReceive(response.getContent()));
|
||||
onReceiveThread.submit(() -> this.onReceive(response.getContent()));
|
||||
} else {
|
||||
logger.debug("Poll timed out, reissuing.");
|
||||
}
|
||||
|
|
@ -121,7 +123,7 @@ class LongPollingTransport implements Transport {
|
|||
}
|
||||
|
||||
@Override
|
||||
public Completable send(String message) {
|
||||
public Completable send(ByteBuffer message) {
|
||||
if (!this.active) {
|
||||
return Completable.error(new Exception("Cannot send unless the transport is active."));
|
||||
}
|
||||
|
|
@ -138,7 +140,7 @@ class LongPollingTransport implements Transport {
|
|||
}
|
||||
|
||||
@Override
|
||||
public void onReceive(String message) {
|
||||
public void onReceive(ByteBuffer message) {
|
||||
this.onReceiveCallBack.invoke(message);
|
||||
logger.debug("OnReceived callback has been invoked.");
|
||||
}
|
||||
|
|
|
|||
|
|
@ -0,0 +1,637 @@
|
|||
// Copyright (c) .NET Foundation. All rights reserved.
|
||||
// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information.
|
||||
|
||||
package com.microsoft.signalr;
|
||||
|
||||
import java.io.IOException;
|
||||
import java.lang.reflect.Type;
|
||||
import java.nio.ByteBuffer;
|
||||
import java.util.ArrayList;
|
||||
import java.util.Arrays;
|
||||
import java.util.Collection;
|
||||
import java.util.HashMap;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
|
||||
import org.msgpack.core.MessageBufferPacker;
|
||||
import org.msgpack.core.MessageFormat;
|
||||
import org.msgpack.core.MessagePack;
|
||||
import org.msgpack.core.MessagePackException;
|
||||
import org.msgpack.core.MessagePacker;
|
||||
import org.msgpack.core.MessageUnpacker;
|
||||
import org.msgpack.jackson.dataformat.MessagePackFactory;
|
||||
import org.msgpack.value.ValueType;
|
||||
|
||||
import com.fasterxml.jackson.databind.ObjectMapper;
|
||||
import com.fasterxml.jackson.databind.type.TypeFactory;
|
||||
|
||||
class MessagePackHubProtocol implements HubProtocol {
|
||||
|
||||
private static final int ERROR_RESULT = 1;
|
||||
private static final int VOID_RESULT = 2;
|
||||
private static final int NON_VOID_RESULT = 3;
|
||||
|
||||
private ObjectMapper objectMapper = new ObjectMapper(new MessagePackFactory());
|
||||
private TypeFactory typeFactory = objectMapper.getTypeFactory();
|
||||
|
||||
@Override
|
||||
public String getName() {
|
||||
return "messagepack";
|
||||
}
|
||||
|
||||
@Override
|
||||
public int getVersion() {
|
||||
return 1;
|
||||
}
|
||||
|
||||
@Override
|
||||
public TransferFormat getTransferFormat() {
|
||||
return TransferFormat.BINARY;
|
||||
}
|
||||
|
||||
@Override
|
||||
public List<HubMessage> parseMessages(ByteBuffer payload, InvocationBinder binder) {
|
||||
if (payload.remaining() == 0) {
|
||||
return null;
|
||||
}
|
||||
|
||||
// MessagePack library can't handle read-only ByteBuffer - copy into an array-backed ByteBuffer if this is the case
|
||||
if (payload.isReadOnly()) {
|
||||
byte[] payloadBytes = new byte[payload.remaining()];
|
||||
payload.get(payloadBytes, 0, payloadBytes.length);
|
||||
payload = ByteBuffer.wrap(payloadBytes);
|
||||
}
|
||||
|
||||
List<HubMessage> hubMessages = new ArrayList<>();
|
||||
|
||||
while (payload.hasRemaining()) {
|
||||
int length;
|
||||
try {
|
||||
length = Utils.readLengthHeader(payload);
|
||||
// Throw if remaining buffer is shorter than length header
|
||||
if (payload.remaining() < length) {
|
||||
throw new RuntimeException(String.format("MessagePack message was length %d but claimed to be length %d.", payload.remaining(), length));
|
||||
}
|
||||
} catch (IOException ex) {
|
||||
throw new RuntimeException("Error reading length header.", ex);
|
||||
}
|
||||
// Instantiate MessageUnpacker
|
||||
try(MessageUnpacker unpacker = MessagePack.newDefaultUnpacker(payload)) {
|
||||
|
||||
int itemCount = unpacker.unpackArrayHeader();
|
||||
HubMessageType messageType = HubMessageType.values()[unpacker.unpackInt() - 1];
|
||||
|
||||
switch (messageType) {
|
||||
case INVOCATION:
|
||||
hubMessages.add(createInvocationMessage(unpacker, binder, itemCount, payload));
|
||||
break;
|
||||
case STREAM_ITEM:
|
||||
hubMessages.add(createStreamItemMessage(unpacker, binder, payload));
|
||||
break;
|
||||
case COMPLETION:
|
||||
hubMessages.add(createCompletionMessage(unpacker, binder, payload));
|
||||
break;
|
||||
case STREAM_INVOCATION:
|
||||
hubMessages.add(createStreamInvocationMessage(unpacker, binder, itemCount, payload));
|
||||
break;
|
||||
case CANCEL_INVOCATION:
|
||||
hubMessages.add(createCancelInvocationMessage(unpacker));
|
||||
break;
|
||||
case PING:
|
||||
hubMessages.add(PingMessage.getInstance());
|
||||
break;
|
||||
case CLOSE:
|
||||
hubMessages.add(createCloseMessage(unpacker, itemCount));
|
||||
break;
|
||||
default:
|
||||
break;
|
||||
}
|
||||
// Make sure that we actually read the right number of bytes
|
||||
int readBytes = (int) unpacker.getTotalReadBytes();
|
||||
if (readBytes != length) {
|
||||
// Check what the last message was
|
||||
// If it was an invocation binding failure, we have to correct the position of the buffer
|
||||
if (hubMessages.get(hubMessages.size() - 1).getMessageType() == HubMessageType.INVOCATION_BINDING_FAILURE) {
|
||||
payload.position(payload.position() + (length - readBytes));
|
||||
} else {
|
||||
throw new RuntimeException(String.format("MessagePack message was length %d but claimed to be length %d.", readBytes, length));
|
||||
}
|
||||
}
|
||||
unpacker.close();
|
||||
payload.position(payload.position() + readBytes);
|
||||
} catch (MessagePackException | IOException ex) {
|
||||
throw new RuntimeException("Error reading MessagePack data.", ex);
|
||||
}
|
||||
}
|
||||
return hubMessages;
|
||||
}
|
||||
|
||||
@Override
|
||||
public ByteBuffer writeMessage(HubMessage hubMessage) {
|
||||
HubMessageType messageType = hubMessage.getMessageType();
|
||||
|
||||
try {
|
||||
byte[] message;
|
||||
switch (messageType) {
|
||||
case INVOCATION:
|
||||
message = writeInvocationMessage((InvocationMessage) hubMessage);
|
||||
break;
|
||||
case STREAM_ITEM:
|
||||
message = writeStreamItemMessage((StreamItem) hubMessage);
|
||||
break;
|
||||
case COMPLETION:
|
||||
message = writeCompletionMessage((CompletionMessage) hubMessage);
|
||||
break;
|
||||
case STREAM_INVOCATION:
|
||||
message = writeStreamInvocationMessage((StreamInvocationMessage) hubMessage);
|
||||
break;
|
||||
case CANCEL_INVOCATION:
|
||||
message = writeCancelInvocationMessage((CancelInvocationMessage) hubMessage);
|
||||
break;
|
||||
case PING:
|
||||
message = writePingMessage((PingMessage) hubMessage);
|
||||
break;
|
||||
case CLOSE:
|
||||
message = writeCloseMessage((CloseMessage) hubMessage);
|
||||
break;
|
||||
default:
|
||||
throw new RuntimeException(String.format("Unexpected message type: %d", messageType.value));
|
||||
}
|
||||
int length = message.length;
|
||||
List<Byte> header = Utils.getLengthHeader(length);
|
||||
byte[] messageWithHeader = new byte[header.size() + length];
|
||||
int headerSize = header.size();
|
||||
|
||||
// Write the length header, then all of the bytes of the original message
|
||||
for (int i = 0; i < headerSize; i++) {
|
||||
messageWithHeader[i] = header.get(i);
|
||||
}
|
||||
for (int i = 0; i < length; i++) {
|
||||
messageWithHeader[i + headerSize] = message[i];
|
||||
}
|
||||
|
||||
return ByteBuffer.wrap(messageWithHeader);
|
||||
} catch (MessagePackException | IOException ex) {
|
||||
throw new RuntimeException("Error writing MessagePack data.", ex);
|
||||
}
|
||||
}
|
||||
|
||||
private HubMessage createInvocationMessage(MessageUnpacker unpacker, InvocationBinder binder, int itemCount, ByteBuffer payload) throws IOException {
|
||||
Map<String, String> headers = readHeaders(unpacker);
|
||||
|
||||
// invocationId may be nil
|
||||
String invocationId = null;
|
||||
if (!unpacker.tryUnpackNil()) {
|
||||
invocationId = unpacker.unpackString();
|
||||
}
|
||||
|
||||
// For MsgPack, we represent an empty invocation ID as an empty string,
|
||||
// so we need to normalize that to "null", which is what indicates a non-blocking invocation.
|
||||
if (invocationId == null || invocationId.isEmpty()) {
|
||||
invocationId = null;
|
||||
}
|
||||
|
||||
String target = unpacker.unpackString();
|
||||
|
||||
Object[] arguments = null;
|
||||
try {
|
||||
List<Type> types = binder.getParameterTypes(target);
|
||||
arguments = bindArguments(unpacker, types, payload);
|
||||
} catch (Exception ex) {
|
||||
return new InvocationBindingFailureMessage(invocationId, target, ex);
|
||||
}
|
||||
|
||||
Collection<String> streams = null;
|
||||
// Older implementations may not send the streamID array
|
||||
if (itemCount > 5) {
|
||||
streams = readStreamIds(unpacker);
|
||||
}
|
||||
|
||||
return new InvocationMessage(headers, invocationId, target, arguments, streams);
|
||||
}
|
||||
|
||||
private HubMessage createStreamItemMessage(MessageUnpacker unpacker, InvocationBinder binder, ByteBuffer payload) throws IOException {
|
||||
Map<String, String> headers = readHeaders(unpacker);
|
||||
String invocationId = unpacker.unpackString();
|
||||
Object value;
|
||||
try {
|
||||
Type itemType = binder.getReturnType(invocationId);
|
||||
value = readValue(unpacker, itemType, payload, true);
|
||||
} catch (Exception ex) {
|
||||
return new StreamBindingFailureMessage(invocationId, ex);
|
||||
}
|
||||
|
||||
return new StreamItem(headers, invocationId, value);
|
||||
}
|
||||
|
||||
private HubMessage createCompletionMessage(MessageUnpacker unpacker, InvocationBinder binder, ByteBuffer payload) throws IOException {
|
||||
Map<String, String> headers = readHeaders(unpacker);
|
||||
String invocationId = unpacker.unpackString();
|
||||
int resultKind = unpacker.unpackInt();
|
||||
|
||||
String error = null;
|
||||
Object result = null;
|
||||
|
||||
switch (resultKind) {
|
||||
case ERROR_RESULT:
|
||||
error = unpacker.unpackString();
|
||||
break;
|
||||
case VOID_RESULT:
|
||||
break;
|
||||
case NON_VOID_RESULT:
|
||||
Type itemType = binder.getReturnType(invocationId);
|
||||
result = readValue(unpacker, itemType, payload, true);
|
||||
break;
|
||||
default:
|
||||
throw new RuntimeException("Invalid invocation result kind.");
|
||||
}
|
||||
|
||||
return new CompletionMessage(headers, invocationId, result, error);
|
||||
}
|
||||
|
||||
private HubMessage createStreamInvocationMessage(MessageUnpacker unpacker, InvocationBinder binder, int itemCount, ByteBuffer payload) throws IOException {
|
||||
Map<String, String> headers = readHeaders(unpacker);
|
||||
String invocationId = unpacker.unpackString();
|
||||
String target = unpacker.unpackString();
|
||||
|
||||
Object[] arguments = null;
|
||||
try {
|
||||
List<Type> types = binder.getParameterTypes(target);
|
||||
arguments = bindArguments(unpacker, types, payload);
|
||||
} catch (Exception ex) {
|
||||
return new InvocationBindingFailureMessage(invocationId, target, ex);
|
||||
}
|
||||
|
||||
Collection<String> streams = readStreamIds(unpacker);
|
||||
|
||||
return new StreamInvocationMessage(headers, invocationId, target, arguments, streams);
|
||||
}
|
||||
|
||||
private HubMessage createCancelInvocationMessage(MessageUnpacker unpacker) throws IOException {
|
||||
Map<String, String> headers = readHeaders(unpacker);
|
||||
String invocationId = unpacker.unpackString();
|
||||
|
||||
return new CancelInvocationMessage(headers, invocationId);
|
||||
}
|
||||
|
||||
private HubMessage createCloseMessage(MessageUnpacker unpacker, int itemCount) throws IOException {
|
||||
// error may be nil
|
||||
String error = null;
|
||||
if (!unpacker.tryUnpackNil()) {
|
||||
error = unpacker.unpackString();
|
||||
}
|
||||
boolean allowReconnect = false;
|
||||
|
||||
if (itemCount > 2) {
|
||||
allowReconnect = unpacker.unpackBoolean();
|
||||
}
|
||||
|
||||
return new CloseMessage(error, allowReconnect);
|
||||
}
|
||||
|
||||
private byte[] writeInvocationMessage(InvocationMessage message) throws IOException {
|
||||
MessageBufferPacker packer = MessagePack.newDefaultBufferPacker();
|
||||
|
||||
packer.packArrayHeader(6);
|
||||
packer.packInt(message.getMessageType().value);
|
||||
|
||||
writeHeaders(message.getHeaders(), packer);
|
||||
|
||||
String invocationId = message.getInvocationId();
|
||||
if (invocationId != null && !invocationId.isEmpty()) {
|
||||
packer.packString(invocationId);
|
||||
} else {
|
||||
packer.packNil();
|
||||
}
|
||||
|
||||
packer.packString(message.getTarget());
|
||||
|
||||
Object[] arguments = message.getArguments();
|
||||
packer.packArrayHeader(arguments.length);
|
||||
|
||||
for (Object o: arguments) {
|
||||
writeValue(o, packer);
|
||||
}
|
||||
|
||||
writeStreamIds(message.getStreamIds(), packer);
|
||||
|
||||
packer.flush();
|
||||
byte[] content = packer.toByteArray();
|
||||
packer.close();
|
||||
return content;
|
||||
}
|
||||
|
||||
private byte[] writeStreamItemMessage(StreamItem message) throws IOException {
|
||||
MessageBufferPacker packer = MessagePack.newDefaultBufferPacker();
|
||||
|
||||
packer.packArrayHeader(4);
|
||||
packer.packInt(message.getMessageType().value);
|
||||
|
||||
writeHeaders(message.getHeaders(), packer);
|
||||
|
||||
packer.packString(message.getInvocationId());
|
||||
|
||||
writeValue(message.getItem(), packer);
|
||||
|
||||
packer.flush();
|
||||
byte[] content = packer.toByteArray();
|
||||
packer.close();
|
||||
return content;
|
||||
}
|
||||
|
||||
private byte[] writeCompletionMessage(CompletionMessage message) throws IOException {
|
||||
MessageBufferPacker packer = MessagePack.newDefaultBufferPacker();
|
||||
int resultKind =
|
||||
message.getError() != null ? ERROR_RESULT :
|
||||
message.getResult() != null ? NON_VOID_RESULT :
|
||||
VOID_RESULT;
|
||||
|
||||
packer.packArrayHeader(4 + (resultKind != VOID_RESULT ? 1: 0));
|
||||
packer.packInt(message.getMessageType().value);
|
||||
|
||||
writeHeaders(message.getHeaders(), packer);
|
||||
|
||||
packer.packString(message.getInvocationId());
|
||||
packer.packInt(resultKind);
|
||||
|
||||
switch (resultKind) {
|
||||
case ERROR_RESULT:
|
||||
packer.packString(message.getError());
|
||||
break;
|
||||
case NON_VOID_RESULT:
|
||||
writeValue(message.getResult(), packer);
|
||||
break;
|
||||
}
|
||||
|
||||
packer.flush();
|
||||
byte[] content = packer.toByteArray();
|
||||
packer.close();
|
||||
return content;
|
||||
}
|
||||
|
||||
private byte[] writeStreamInvocationMessage(StreamInvocationMessage message) throws IOException {
|
||||
MessageBufferPacker packer = MessagePack.newDefaultBufferPacker();
|
||||
|
||||
packer.packArrayHeader(6);
|
||||
packer.packInt(message.getMessageType().value);
|
||||
|
||||
writeHeaders(message.getHeaders(), packer);
|
||||
|
||||
packer.packString(message.getInvocationId());
|
||||
packer.packString(message.getTarget());
|
||||
|
||||
Object[] arguments = message.getArguments();
|
||||
packer.packArrayHeader(arguments.length);
|
||||
|
||||
for (Object o: arguments) {
|
||||
writeValue(o, packer);
|
||||
}
|
||||
|
||||
writeStreamIds(message.getStreamIds(), packer);
|
||||
|
||||
packer.flush();
|
||||
byte[] content = packer.toByteArray();
|
||||
packer.close();
|
||||
return content;
|
||||
}
|
||||
|
||||
private byte[] writeCancelInvocationMessage(CancelInvocationMessage message) throws IOException {
|
||||
MessageBufferPacker packer = MessagePack.newDefaultBufferPacker();
|
||||
|
||||
packer.packArrayHeader(3);
|
||||
packer.packInt(message.getMessageType().value);
|
||||
|
||||
writeHeaders(message.getHeaders(), packer);
|
||||
|
||||
packer.packString(message.getInvocationId());
|
||||
|
||||
packer.flush();
|
||||
byte[] content = packer.toByteArray();
|
||||
packer.close();
|
||||
return content;
|
||||
}
|
||||
|
||||
private byte[] writePingMessage(PingMessage message) throws IOException {
|
||||
MessageBufferPacker packer = MessagePack.newDefaultBufferPacker();
|
||||
|
||||
packer.packArrayHeader(1);
|
||||
packer.packInt(message.getMessageType().value);
|
||||
|
||||
packer.flush();
|
||||
byte[] content = packer.toByteArray();
|
||||
packer.close();
|
||||
return content;
|
||||
}
|
||||
|
||||
private byte[] writeCloseMessage(CloseMessage message) throws IOException {
|
||||
MessageBufferPacker packer = MessagePack.newDefaultBufferPacker();
|
||||
|
||||
packer.packArrayHeader(3);
|
||||
packer.packInt(message.getMessageType().value);
|
||||
|
||||
String error = message.getError();
|
||||
if (error != null && !error.isEmpty()) {
|
||||
packer.packString(error);
|
||||
} else {
|
||||
packer.packNil();
|
||||
}
|
||||
|
||||
packer.packBoolean(message.getAllowReconnect());
|
||||
|
||||
packer.flush();
|
||||
byte[] content = packer.toByteArray();
|
||||
packer.close();
|
||||
return content;
|
||||
}
|
||||
|
||||
private Map<String, String> readHeaders(MessageUnpacker unpacker) throws IOException {
|
||||
int headerCount = unpacker.unpackMapHeader();
|
||||
if (headerCount > 0) {
|
||||
Map<String, String> headers = new HashMap<String, String>();
|
||||
for (int i = 0; i < headerCount; i++) {
|
||||
headers.put(unpacker.unpackString(), unpacker.unpackString());
|
||||
}
|
||||
return headers;
|
||||
} else {
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
private void writeHeaders(Map<String, String> headers, MessagePacker packer) throws IOException {
|
||||
if (headers != null) {
|
||||
packer.packMapHeader(headers.size());
|
||||
for (String k: headers.keySet()) {
|
||||
packer.packString(k);
|
||||
packer.packString(headers.get(k));
|
||||
}
|
||||
} else {
|
||||
packer.packMapHeader(0);
|
||||
}
|
||||
}
|
||||
|
||||
private Collection<String> readStreamIds(MessageUnpacker unpacker) throws IOException {
|
||||
int streamCount = unpacker.unpackArrayHeader();
|
||||
Collection<String> streams = null;
|
||||
|
||||
if (streamCount > 0) {
|
||||
streams = new ArrayList<String>();
|
||||
for (int i = 0; i < streamCount; i++) {
|
||||
streams.add(unpacker.unpackString());
|
||||
}
|
||||
}
|
||||
|
||||
return streams;
|
||||
}
|
||||
|
||||
private void writeStreamIds(Collection<String> streamIds, MessagePacker packer) throws IOException {
|
||||
if (streamIds != null) {
|
||||
packer.packArrayHeader(streamIds.size());
|
||||
for (String s: streamIds) {
|
||||
packer.packString(s);
|
||||
}
|
||||
} else {
|
||||
packer.packArrayHeader(0);
|
||||
}
|
||||
}
|
||||
|
||||
private Object[] bindArguments(MessageUnpacker unpacker, List<Type> paramTypes, ByteBuffer payload) throws IOException {
|
||||
int argumentCount = unpacker.unpackArrayHeader();
|
||||
|
||||
if (paramTypes.size() != argumentCount) {
|
||||
throw new RuntimeException(String.format("Invocation provides %d argument(s) but target expects %d.", argumentCount, paramTypes.size()));
|
||||
}
|
||||
|
||||
Object[] arguments = new Object[argumentCount];
|
||||
|
||||
for (int i = 0; i < argumentCount; i++) {
|
||||
arguments[i] = readValue(unpacker, paramTypes.get(i), payload, true);
|
||||
}
|
||||
|
||||
return arguments;
|
||||
}
|
||||
|
||||
private Object readValue(MessageUnpacker unpacker, Type itemType, ByteBuffer payload, boolean outermostCall) throws IOException {
|
||||
Class<?> itemClass = Utils.typeToClass(itemType);
|
||||
MessageFormat messageFormat = unpacker.getNextFormat();
|
||||
ValueType valueType = messageFormat.getValueType();
|
||||
int length;
|
||||
long readBytesStart;
|
||||
Object item = null;
|
||||
|
||||
switch(valueType) {
|
||||
case NIL:
|
||||
unpacker.unpackNil();
|
||||
return null;
|
||||
case BOOLEAN:
|
||||
item = unpacker.unpackBoolean();
|
||||
break;
|
||||
case INTEGER:
|
||||
switch (messageFormat) {
|
||||
case UINT64:
|
||||
item = unpacker.unpackBigInteger();
|
||||
break;
|
||||
case INT64:
|
||||
case UINT32:
|
||||
item = unpacker.unpackLong();
|
||||
break;
|
||||
default:
|
||||
item = unpacker.unpackInt();
|
||||
// unpackInt could correspond to an int, short, char, or byte - cast those literally here
|
||||
if (itemClass != null) {
|
||||
if (itemClass.equals(Short.class) || itemClass.equals(short.class)) {
|
||||
item = ((Integer) item).shortValue();
|
||||
} else if (itemClass.equals(Character.class) || itemClass.equals(char.class)) {
|
||||
item = (char) ((Integer) item).shortValue();
|
||||
} else if (itemClass.equals(Byte.class) || itemClass.equals(byte.class)) {
|
||||
item = ((Integer) item).byteValue();
|
||||
}
|
||||
}
|
||||
break;
|
||||
}
|
||||
break;
|
||||
case FLOAT:
|
||||
item = unpacker.unpackDouble();
|
||||
break;
|
||||
case STRING:
|
||||
item = unpacker.unpackString();
|
||||
// ObjectMapper packs chars as Strings - correct back to char while unpacking if necessary
|
||||
if (itemClass != null && (itemClass.equals(char.class) || itemClass.equals(Character.class))) {
|
||||
item = ((String) item).charAt(0);
|
||||
}
|
||||
break;
|
||||
case BINARY:
|
||||
length = unpacker.unpackBinaryHeader();
|
||||
byte[] binaryValue = new byte[length];
|
||||
unpacker.readPayload(binaryValue);
|
||||
item = binaryValue;
|
||||
break;
|
||||
case ARRAY:
|
||||
readBytesStart = unpacker.getTotalReadBytes();
|
||||
length = unpacker.unpackArrayHeader();
|
||||
for (int i = 0; i < length; i++) {
|
||||
readValue(unpacker, Object.class, payload, false);
|
||||
}
|
||||
if (outermostCall) {
|
||||
// Check how many bytes we've read, grab that from the payload, and deserialize with objectMapper
|
||||
byte[] payloadBytes = payload.array();
|
||||
// If itemType was null, we were just in this method to advance the buffer. return null.
|
||||
if (itemType == null) {
|
||||
return null;
|
||||
}
|
||||
return objectMapper.readValue(payloadBytes, payload.position() + (int) readBytesStart, (int) (unpacker.getTotalReadBytes() - readBytesStart),
|
||||
typeFactory.constructType(itemType));
|
||||
} else {
|
||||
// This is an inner call to readValue - we just need to read the right number of bytes
|
||||
// We can return null, and the outermost call will know how many bytes to give to objectMapper.
|
||||
return null;
|
||||
}
|
||||
case MAP:
|
||||
readBytesStart = unpacker.getTotalReadBytes();
|
||||
length = unpacker.unpackMapHeader();
|
||||
for (int i = 0; i < length; i++) {
|
||||
readValue(unpacker, Object.class, payload, false);
|
||||
readValue(unpacker, Object.class, payload, false);
|
||||
}
|
||||
if (outermostCall) {
|
||||
// Check how many bytes we've read, grab that from the payload, and deserialize with objectMapper
|
||||
byte[] payloadBytes = payload.array();
|
||||
byte[] mapBytes = Arrays.copyOfRange(payloadBytes, payload.position() + (int) readBytesStart,
|
||||
payload.position() + (int) unpacker.getTotalReadBytes());
|
||||
// If itemType was null, we were just in this method to advance the buffer. return null.
|
||||
if (itemType == null) {
|
||||
return null;
|
||||
}
|
||||
return objectMapper.readValue(payloadBytes, payload.position() + (int) readBytesStart, (int) (unpacker.getTotalReadBytes() - readBytesStart),
|
||||
typeFactory.constructType(itemType));
|
||||
} else {
|
||||
// This is an inner call to readValue - we just need to read the right number of bytes
|
||||
// We can return null, and the outermost call will know how many bytes to give to objectMapper.
|
||||
return null;
|
||||
}
|
||||
case EXTENSION:
|
||||
/*
|
||||
ExtensionTypeHeader extension = unpacker.unpackExtensionTypeHeader();
|
||||
byte[] extensionValue = new byte[extension.getLength()];
|
||||
unpacker.readPayload(extensionValue);
|
||||
//Convert this to an object?
|
||||
item = extensionValue;
|
||||
*/
|
||||
throw new RuntimeException("Extension types are not supported");
|
||||
default:
|
||||
return null;
|
||||
}
|
||||
// If itemType was null, we were just in this method to advance the buffer. return null.
|
||||
if (itemType == null) {
|
||||
return null;
|
||||
}
|
||||
// If we get here, the item isn't a map or a collection/array, so we use the Class to cast it
|
||||
if (itemClass.isPrimitive()) {
|
||||
return Utils.toPrimitive(itemClass, item);
|
||||
}
|
||||
return itemClass.cast(item);
|
||||
}
|
||||
|
||||
private void writeValue(Object o, MessagePacker packer) throws IOException {
|
||||
packer.addPayload(objectMapper.writeValueAsBytes(o));
|
||||
}
|
||||
}
|
||||
|
|
@ -3,6 +3,8 @@
|
|||
|
||||
package com.microsoft.signalr;
|
||||
|
||||
import java.nio.ByteBuffer;
|
||||
import java.nio.charset.StandardCharsets;
|
||||
import java.util.Map;
|
||||
import java.util.concurrent.locks.ReentrantLock;
|
||||
|
||||
|
|
@ -17,6 +19,7 @@ import okhttp3.Request;
|
|||
import okhttp3.Response;
|
||||
import okhttp3.WebSocket;
|
||||
import okhttp3.WebSocketListener;
|
||||
import okio.ByteString;
|
||||
|
||||
class OkHttpWebSocketWrapper extends WebSocketWrapper {
|
||||
private WebSocket websocketClient;
|
||||
|
|
@ -60,8 +63,9 @@ class OkHttpWebSocketWrapper extends WebSocketWrapper {
|
|||
}
|
||||
|
||||
@Override
|
||||
public Completable send(String message) {
|
||||
websocketClient.send(message);
|
||||
public Completable send(ByteBuffer message) {
|
||||
ByteString bs = ByteString.of(message);
|
||||
websocketClient.send(bs);
|
||||
return Completable.complete();
|
||||
}
|
||||
|
||||
|
|
@ -83,7 +87,12 @@ class OkHttpWebSocketWrapper extends WebSocketWrapper {
|
|||
|
||||
@Override
|
||||
public void onMessage(WebSocket webSocket, String message) {
|
||||
onReceive.invoke(message);
|
||||
onReceive.invoke(ByteBuffer.wrap(message.getBytes(StandardCharsets.UTF_8)));
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onMessage(WebSocket webSocket, ByteString bytes) {
|
||||
onReceive.invoke(bytes.asByteBuffer());
|
||||
}
|
||||
|
||||
@Override
|
||||
|
|
|
|||
|
|
@ -3,6 +3,8 @@
|
|||
|
||||
package com.microsoft.signalr;
|
||||
|
||||
import java.nio.ByteBuffer;
|
||||
|
||||
interface OnReceiveCallBack {
|
||||
void invoke(String message);
|
||||
void invoke(ByteBuffer message);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
}
|
||||
}
|
||||
|
|
@ -4,16 +4,12 @@
|
|||
package com.microsoft.signalr;
|
||||
|
||||
import java.util.Collection;
|
||||
import java.util.Map;
|
||||
|
||||
final class StreamInvocationMessage extends InvocationMessage {
|
||||
|
||||
public StreamInvocationMessage(String invocationId, String target, Object[] args) {
|
||||
super(invocationId, target, args);
|
||||
super.type = HubMessageType.STREAM_INVOCATION.value;
|
||||
}
|
||||
|
||||
public StreamInvocationMessage(String invocationId, String target, Object[] args, Collection<String> streamIds) {
|
||||
super(invocationId, target, args, streamIds);
|
||||
|
||||
public StreamInvocationMessage(Map<String, String> headers, String invocationId, String target, Object[] args, Collection<String> streamIds) {
|
||||
super(headers, invocationId, target, args, streamIds);
|
||||
super.type = HubMessageType.STREAM_INVOCATION.value;
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -3,15 +3,25 @@
|
|||
|
||||
package com.microsoft.signalr;
|
||||
|
||||
import java.util.Map;
|
||||
|
||||
final class StreamItem extends HubMessage {
|
||||
private final int type = HubMessageType.STREAM_ITEM.value;
|
||||
private Map<String, String> headers;
|
||||
private final String invocationId;
|
||||
private final Object item;
|
||||
|
||||
public StreamItem(String invocationId, Object item) {
|
||||
|
||||
public StreamItem(Map<String, String> headers, String invocationId, Object item) {
|
||||
if (headers != null && !headers.isEmpty()) {
|
||||
this.headers = headers;
|
||||
}
|
||||
this.invocationId = invocationId;
|
||||
this.item = item;
|
||||
}
|
||||
|
||||
public Map<String, String> getHeaders() {
|
||||
return headers;
|
||||
}
|
||||
|
||||
public String getInvocationId() {
|
||||
return invocationId;
|
||||
|
|
|
|||
|
|
@ -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();
|
||||
}
|
||||
|
|
|
|||
|
|
@ -3,6 +3,16 @@
|
|||
|
||||
package com.microsoft.signalr;
|
||||
|
||||
import java.io.IOException;
|
||||
import java.lang.reflect.Array;
|
||||
import java.lang.reflect.GenericArrayType;
|
||||
import java.lang.reflect.ParameterizedType;
|
||||
import java.lang.reflect.Type;
|
||||
import java.lang.reflect.TypeVariable;
|
||||
import java.lang.reflect.WildcardType;
|
||||
import java.nio.ByteBuffer;
|
||||
import java.util.ArrayList;
|
||||
|
||||
class Utils {
|
||||
public static String appendQueryString(String original, String queryStringValue) {
|
||||
if (original.contains("?")) {
|
||||
|
|
@ -11,4 +21,92 @@ class Utils {
|
|||
return original + "?" + queryStringValue;
|
||||
}
|
||||
}
|
||||
|
||||
public static int readLengthHeader(ByteBuffer buffer) throws IOException {
|
||||
// The payload starts with a length prefix encoded as a VarInt. VarInts use the most significant bit
|
||||
// as a marker whether the byte is the last byte of the VarInt or if it spans to the next byte. Bytes
|
||||
// appear in the reverse order - i.e. the first byte contains the least significant bits of the value
|
||||
// Examples:
|
||||
// VarInt: 0x35 - %00110101 - the most significant bit is 0 so the value is %x0110101 i.e. 0x35 (53)
|
||||
// VarInt: 0x80 0x25 - %10000000 %00101001 - the most significant bit of the first byte is 1 so the
|
||||
// remaining bits (%x0000000) are the lowest bits of the value. The most significant bit of the second
|
||||
// byte is 0 meaning this is last byte of the VarInt. The actual value bits (%x0101001) need to be
|
||||
// prepended to the bits we already read so the values is %01010010000000 i.e. 0x1480 (5248)
|
||||
// We support payloads up to 2GB so the biggest number we support is 7fffffff which when encoded as
|
||||
// VarInt is 0xFF 0xFF 0xFF 0xFF 0x07 - hence the maximum length prefix is 5 bytes.
|
||||
|
||||
int length = 0;
|
||||
int numBytes = 0;
|
||||
int maxLength = 5;
|
||||
byte curr;
|
||||
|
||||
do {
|
||||
// If we run out of bytes before we finish reading the length header, the message is malformed
|
||||
if (buffer.hasRemaining()) {
|
||||
curr = buffer.get();
|
||||
} else {
|
||||
throw new RuntimeException("The length header was incomplete");
|
||||
}
|
||||
length = length | (curr & (byte) 0x7f) << (numBytes * 7);
|
||||
numBytes++;
|
||||
} while (numBytes < maxLength && (curr & (byte) 0x80) != 0);
|
||||
|
||||
// Max header length is 5, and the maximum value of the 5th byte is 0x07
|
||||
if ((curr & (byte) 0x80) != 0 || (numBytes == maxLength && curr > (byte) 0x07)) {
|
||||
throw new RuntimeException("Messages over 2GB in size are not supported");
|
||||
}
|
||||
|
||||
return length;
|
||||
}
|
||||
|
||||
public static ArrayList<Byte> getLengthHeader(int length) {
|
||||
// This code writes length prefix of the message as a VarInt. Read the comment in
|
||||
// the readLengthHeader for details.
|
||||
|
||||
ArrayList<Byte> header = new ArrayList<Byte>();
|
||||
do {
|
||||
byte curr = (byte) (length & 0x7f);
|
||||
length >>= 7;
|
||||
if (length > 0) {
|
||||
curr |= 0x80;
|
||||
}
|
||||
header.add(curr);
|
||||
} while (length > 0);
|
||||
|
||||
return header;
|
||||
}
|
||||
|
||||
public static Object toPrimitive(Class<?> c, Object value) {
|
||||
if (boolean.class == c) return ((Boolean) value).booleanValue();
|
||||
if (byte.class == c) return ((Byte) value).byteValue();
|
||||
if (short.class == c) return ((Short) value).shortValue();
|
||||
if (int.class == c) return ((Integer) value).intValue();
|
||||
if (long.class == c) return ((Long) value).longValue();
|
||||
if (float.class == c) return ((Float) value).floatValue();
|
||||
if (double.class == c) return ((Double) value).doubleValue();
|
||||
if (char.class == c) return ((Character) value).charValue();
|
||||
return value;
|
||||
}
|
||||
|
||||
public static Class<?> typeToClass(Type type) {
|
||||
if (type == null) {
|
||||
return null;
|
||||
}
|
||||
if (type instanceof Class) {
|
||||
return (Class<?>) type;
|
||||
} else if (type instanceof GenericArrayType) {
|
||||
// Instantiate an array of the same type as this type, then return its class
|
||||
return Array.newInstance(typeToClass(((GenericArrayType)type).getGenericComponentType()), 0).getClass();
|
||||
} else if (type instanceof ParameterizedType) {
|
||||
return typeToClass(((ParameterizedType) type).getRawType());
|
||||
} else if (type instanceof TypeVariable) {
|
||||
Type[] bounds = ((TypeVariable<?>) type).getBounds();
|
||||
return bounds.length == 0 ? Object.class : typeToClass(bounds[0]);
|
||||
} else if (type instanceof WildcardType) {
|
||||
Type[] bounds = ((WildcardType) type).getUpperBounds();
|
||||
return bounds.length == 0 ? Object.class : typeToClass(bounds[0]);
|
||||
} else {
|
||||
throw new UnsupportedOperationException("Cannot handle type class: " + type.getClass());
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -3,6 +3,7 @@
|
|||
|
||||
package com.microsoft.signalr;
|
||||
|
||||
import java.nio.ByteBuffer;
|
||||
import java.util.Map;
|
||||
|
||||
import org.slf4j.Logger;
|
||||
|
|
@ -59,7 +60,7 @@ class WebSocketTransport implements Transport {
|
|||
}
|
||||
|
||||
@Override
|
||||
public Completable send(String message) {
|
||||
public Completable send(ByteBuffer message) {
|
||||
return webSocketClient.send(message);
|
||||
}
|
||||
|
||||
|
|
@ -70,7 +71,7 @@ class WebSocketTransport implements Transport {
|
|||
}
|
||||
|
||||
@Override
|
||||
public void onReceive(String message) {
|
||||
public void onReceive(ByteBuffer message) {
|
||||
this.onReceiveCallBack.invoke(message);
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -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);
|
||||
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
}
|
||||
}
|
||||
|
|
@ -5,15 +5,16 @@ package com.microsoft.signalr;
|
|||
|
||||
import static org.junit.jupiter.api.Assertions.*;
|
||||
|
||||
import java.nio.ByteBuffer;
|
||||
import org.junit.jupiter.api.Test;
|
||||
|
||||
class HandshakeProtocolTest {
|
||||
@Test
|
||||
public void VerifyCreateHandshakerequestMessage() {
|
||||
HandshakeRequestMessage handshakeRequest = new HandshakeRequestMessage("json", 1);
|
||||
String result = HandshakeProtocol.createHandshakeRequestMessage(handshakeRequest);
|
||||
ByteBuffer result = HandshakeProtocol.createHandshakeRequestMessage(handshakeRequest);
|
||||
String expectedResult = "{\"protocol\":\"json\",\"version\":1}\u001E";
|
||||
assertEquals(expectedResult, result);
|
||||
assertEquals(expectedResult, TestUtils.byteBufferToString(result));
|
||||
}
|
||||
|
||||
@Test
|
||||
|
|
|
|||
File diff suppressed because it is too large
Load Diff
|
|
@ -5,12 +5,15 @@ package com.microsoft.signalr;
|
|||
|
||||
import static org.junit.jupiter.api.Assertions.*;
|
||||
|
||||
import java.lang.reflect.Type;
|
||||
import java.nio.ByteBuffer;
|
||||
import java.util.ArrayList;
|
||||
import java.util.Arrays;
|
||||
import java.util.HashMap;
|
||||
import java.util.List;
|
||||
|
||||
import org.junit.jupiter.api.Test;
|
||||
|
||||
import com.fasterxml.jackson.core.type.TypeReference;
|
||||
|
||||
class JsonHubProtocolTest {
|
||||
private JsonHubProtocol jsonHubProtocol = new JsonHubProtocol();
|
||||
|
|
@ -32,8 +35,8 @@ class JsonHubProtocolTest {
|
|||
|
||||
@Test
|
||||
public void verifyWriteMessage() {
|
||||
InvocationMessage invocationMessage = new InvocationMessage(null, "test", new Object[] {"42"});
|
||||
String result = jsonHubProtocol.writeMessage(invocationMessage);
|
||||
InvocationMessage invocationMessage = new InvocationMessage(null, null, "test", new Object[] {"42"}, null);
|
||||
String result = TestUtils.byteBufferToString(jsonHubProtocol.writeMessage(invocationMessage));
|
||||
String expectedResult = "{\"type\":1,\"target\":\"test\",\"arguments\":[\"42\"]}\u001E";
|
||||
assertEquals(expectedResult, result);
|
||||
}
|
||||
|
|
@ -41,29 +44,33 @@ class JsonHubProtocolTest {
|
|||
@Test
|
||||
public void parsePingMessage() {
|
||||
String stringifiedMessage = "{\"type\":6}\u001E";
|
||||
TestBinder binder = new TestBinder(PingMessage.getInstance());
|
||||
ByteBuffer message = TestUtils.stringToByteBuffer(stringifiedMessage);
|
||||
TestBinder binder = new TestBinder(null, null);
|
||||
|
||||
HubMessage[] messages = jsonHubProtocol.parseMessages(stringifiedMessage, binder);
|
||||
List<HubMessage> messages = jsonHubProtocol.parseMessages(message, binder);
|
||||
|
||||
//We know it's only one message
|
||||
assertEquals(1, messages.length);
|
||||
assertEquals(HubMessageType.PING, messages[0].getMessageType());
|
||||
assertNotNull(messages);
|
||||
assertEquals(1, messages.size());
|
||||
assertEquals(HubMessageType.PING, messages.get(0).getMessageType());
|
||||
}
|
||||
|
||||
@Test
|
||||
public void parseCloseMessage() {
|
||||
String stringifiedMessage = "{\"type\":7}\u001E";
|
||||
TestBinder binder = new TestBinder(new CloseMessage());
|
||||
ByteBuffer message = TestUtils.stringToByteBuffer(stringifiedMessage);
|
||||
TestBinder binder = new TestBinder(null, null);
|
||||
|
||||
HubMessage[] messages = jsonHubProtocol.parseMessages(stringifiedMessage, binder);
|
||||
List<HubMessage> messages = jsonHubProtocol.parseMessages(message, binder);
|
||||
|
||||
//We know it's only one message
|
||||
assertEquals(1, messages.length);
|
||||
assertNotNull(messages);
|
||||
assertEquals(1, messages.size());
|
||||
|
||||
assertEquals(HubMessageType.CLOSE, messages[0].getMessageType());
|
||||
assertEquals(HubMessageType.CLOSE, messages.get(0).getMessageType());
|
||||
|
||||
//We can safely cast here because we know that it's a close message.
|
||||
CloseMessage closeMessage = (CloseMessage) messages[0];
|
||||
CloseMessage closeMessage = (CloseMessage) messages.get(0);
|
||||
|
||||
assertEquals(null, closeMessage.getError());
|
||||
}
|
||||
|
|
@ -71,17 +78,19 @@ class JsonHubProtocolTest {
|
|||
@Test
|
||||
public void parseCloseMessageWithError() {
|
||||
String stringifiedMessage = "{\"type\":7,\"error\": \"There was an error\"}\u001E";
|
||||
TestBinder binder = new TestBinder(new CloseMessage("There was an error"));
|
||||
ByteBuffer message = TestUtils.stringToByteBuffer(stringifiedMessage);
|
||||
TestBinder binder = new TestBinder(new Type[] { int.class }, null);
|
||||
|
||||
HubMessage[] messages = jsonHubProtocol.parseMessages(stringifiedMessage, binder);
|
||||
List<HubMessage> messages = jsonHubProtocol.parseMessages(message, binder);
|
||||
|
||||
//We know it's only one message
|
||||
assertEquals(1, messages.length);
|
||||
assertNotNull(messages);
|
||||
assertEquals(1, messages.size());
|
||||
|
||||
assertEquals(HubMessageType.CLOSE, messages[0].getMessageType());
|
||||
assertEquals(HubMessageType.CLOSE, messages.get(0).getMessageType());
|
||||
|
||||
//We can safely cast here because we know that it's a close message.
|
||||
CloseMessage closeMessage = (CloseMessage) messages[0];
|
||||
CloseMessage closeMessage = (CloseMessage) messages.get(0);
|
||||
|
||||
assertEquals("There was an error", closeMessage.getError());
|
||||
}
|
||||
|
|
@ -89,17 +98,19 @@ class JsonHubProtocolTest {
|
|||
@Test
|
||||
public void parseSingleMessage() {
|
||||
String stringifiedMessage = "{\"type\":1,\"target\":\"test\",\"arguments\":[42]}\u001E";
|
||||
TestBinder binder = new TestBinder(new InvocationMessage("1", "test", new Object[] { 42 }));
|
||||
ByteBuffer message = TestUtils.stringToByteBuffer(stringifiedMessage);
|
||||
TestBinder binder = new TestBinder(new Type[] { int.class }, null);
|
||||
|
||||
HubMessage[] messages = jsonHubProtocol.parseMessages(stringifiedMessage, binder);
|
||||
List<HubMessage> messages = jsonHubProtocol.parseMessages(message, binder);
|
||||
|
||||
//We know it's only one message
|
||||
assertEquals(1, messages.length);
|
||||
assertNotNull(messages);
|
||||
assertEquals(1, messages.size());
|
||||
|
||||
assertEquals(HubMessageType.INVOCATION, messages[0].getMessageType());
|
||||
assertEquals(HubMessageType.INVOCATION, messages.get(0).getMessageType());
|
||||
|
||||
//We can safely cast here because we know that it's an invocation message.
|
||||
InvocationMessage invocationMessage = (InvocationMessage) messages[0];
|
||||
InvocationMessage invocationMessage = (InvocationMessage) messages.get(0);
|
||||
|
||||
assertEquals("test", invocationMessage.getTarget());
|
||||
assertEquals(null, invocationMessage.getInvocationId());
|
||||
|
|
@ -111,34 +122,39 @@ class JsonHubProtocolTest {
|
|||
@Test
|
||||
public void parseSingleUnsupportedStreamInvocationMessage() {
|
||||
String stringifiedMessage = "{\"type\":4,\"Id\":1,\"target\":\"test\",\"arguments\":[42]}\u001E";
|
||||
TestBinder binder = new TestBinder(new StreamInvocationMessage("1", "test", new Object[] { 42 }));
|
||||
ByteBuffer message = TestUtils.stringToByteBuffer(stringifiedMessage);
|
||||
TestBinder binder = new TestBinder(new Type[] { int.class }, null);
|
||||
|
||||
Throwable exception = assertThrows(UnsupportedOperationException.class, () -> jsonHubProtocol.parseMessages(stringifiedMessage, binder));
|
||||
Throwable exception = assertThrows(UnsupportedOperationException.class, () -> jsonHubProtocol.parseMessages(message, binder));
|
||||
assertEquals("The message type STREAM_INVOCATION is not supported yet.", exception.getMessage());
|
||||
}
|
||||
|
||||
@Test
|
||||
public void parseSingleUnsupportedCancelInvocationMessage() {
|
||||
String stringifiedMessage = "{\"type\":5,\"invocationId\":123}\u001E";
|
||||
TestBinder binder = new TestBinder(null);
|
||||
ByteBuffer message = TestUtils.stringToByteBuffer(stringifiedMessage);
|
||||
TestBinder binder = new TestBinder(null, null);
|
||||
|
||||
Throwable exception = assertThrows(UnsupportedOperationException.class, () -> jsonHubProtocol.parseMessages(stringifiedMessage, binder));
|
||||
Throwable exception = assertThrows(UnsupportedOperationException.class, () -> jsonHubProtocol.parseMessages(message, binder));
|
||||
assertEquals("The message type CANCEL_INVOCATION is not supported yet.", exception.getMessage());
|
||||
}
|
||||
|
||||
@Test
|
||||
public void parseTwoMessages() {
|
||||
String twoMessages = "{\"type\":1,\"target\":\"one\",\"arguments\":[42]}\u001E{\"type\":1,\"target\":\"two\",\"arguments\":[43]}\u001E";
|
||||
TestBinder binder = new TestBinder(new InvocationMessage("1", "one", new Object[] { 42 }));
|
||||
String stringifiedMessage = "{\"type\":1,\"target\":\"one\",\"arguments\":[42]}\u001E{\"type\":1,\"target\":\"two\",\"arguments\":[43]}\u001E";
|
||||
ByteBuffer message = TestUtils.stringToByteBuffer(stringifiedMessage);
|
||||
TestBinder binder = new TestBinder(new Type[] { int.class }, null);
|
||||
|
||||
HubMessage[] messages = jsonHubProtocol.parseMessages(twoMessages, binder);
|
||||
assertEquals(2, messages.length);
|
||||
List<HubMessage> messages = jsonHubProtocol.parseMessages(message, binder);
|
||||
|
||||
assertNotNull(messages);
|
||||
assertEquals(2, messages.size());
|
||||
|
||||
// Check the first message
|
||||
assertEquals(HubMessageType.INVOCATION, messages[0].getMessageType());
|
||||
assertEquals(HubMessageType.INVOCATION, messages.get(0).getMessageType());
|
||||
|
||||
//Now that we know we have an invocation message we can cast the hubMessage.
|
||||
InvocationMessage invocationMessage = (InvocationMessage) messages[0];
|
||||
InvocationMessage invocationMessage = (InvocationMessage) messages.get(0);
|
||||
|
||||
assertEquals("one", invocationMessage.getTarget());
|
||||
assertEquals(null, invocationMessage.getInvocationId());
|
||||
|
|
@ -146,10 +162,10 @@ class JsonHubProtocolTest {
|
|||
assertEquals(42, messageResult);
|
||||
|
||||
// Check the second message
|
||||
assertEquals(HubMessageType.INVOCATION, messages[1].getMessageType());
|
||||
assertEquals(HubMessageType.INVOCATION, messages.get(1).getMessageType());
|
||||
|
||||
//Now that we know we have an invocation message we can cast the hubMessage.
|
||||
InvocationMessage invocationMessage2 = (InvocationMessage) messages[1];
|
||||
InvocationMessage invocationMessage2 = (InvocationMessage) messages.get(1);
|
||||
|
||||
assertEquals("two", invocationMessage2.getTarget());
|
||||
assertEquals(null, invocationMessage2.getInvocationId());
|
||||
|
|
@ -160,37 +176,135 @@ class JsonHubProtocolTest {
|
|||
@Test
|
||||
public void parseSingleMessageMutipleArgs() {
|
||||
String stringifiedMessage = "{\"type\":1,\"target\":\"test\",\"arguments\":[42, 24]}\u001E";
|
||||
TestBinder binder = new TestBinder(new InvocationMessage("1", "test", new Object[] { 42, 24 }));
|
||||
ByteBuffer message = TestUtils.stringToByteBuffer(stringifiedMessage);
|
||||
TestBinder binder = new TestBinder(new Type[] { int.class, int.class }, null);
|
||||
|
||||
HubMessage[] messages = jsonHubProtocol.parseMessages(stringifiedMessage, binder);
|
||||
List<HubMessage> messages = jsonHubProtocol.parseMessages(message, binder);
|
||||
|
||||
assertNotNull(messages);
|
||||
assertEquals(1, messages.size());
|
||||
|
||||
//We know it's only one message
|
||||
assertEquals(HubMessageType.INVOCATION, messages[0].getMessageType());
|
||||
assertEquals(HubMessageType.INVOCATION, messages.get(0).getMessageType());
|
||||
|
||||
InvocationMessage message = (InvocationMessage)messages[0];
|
||||
assertEquals("test", message.getTarget());
|
||||
assertEquals(null, message.getInvocationId());
|
||||
int messageResult = (int) message.getArguments()[0];
|
||||
int messageResult2 = (int) message.getArguments()[1];
|
||||
InvocationMessage invocationMessage = (InvocationMessage)messages.get(0);
|
||||
assertEquals("test", invocationMessage.getTarget());
|
||||
assertEquals(null, invocationMessage.getInvocationId());
|
||||
int messageResult = (int) invocationMessage.getArguments()[0];
|
||||
int messageResult2 = (int) invocationMessage.getArguments()[1];
|
||||
assertEquals(42, messageResult);
|
||||
assertEquals(24, messageResult2);
|
||||
}
|
||||
|
||||
@Test
|
||||
public void parseSingleMessageNestedCollection() {
|
||||
String stringifiedMessage = "{\"type\":1,\"target\":\"test\",\"arguments\":[[{\"one\":[\"a\",\"b\"],\"two\":[\"\uBEEF\",\"\uABCD\"]},{\"four\":[\"^\",\"*\"],\"three\":[\"5\",\"9\"]}]]}\u001E";
|
||||
ByteBuffer message = TestUtils.stringToByteBuffer(stringifiedMessage);
|
||||
TestBinder binder = new TestBinder(new Type[] { (new TypeReference<ArrayList<HashMap<String, ArrayList<Character>>>>() { }).getType() }, null);
|
||||
|
||||
List<HubMessage> messages = jsonHubProtocol.parseMessages(message, binder);
|
||||
|
||||
assertNotNull(messages);
|
||||
assertEquals(1, messages.size());
|
||||
|
||||
//We know it's only one message
|
||||
assertEquals(HubMessageType.INVOCATION, messages.get(0).getMessageType());
|
||||
|
||||
InvocationMessage invocationMessage = (InvocationMessage)messages.get(0);
|
||||
|
||||
assertEquals("test", invocationMessage.getTarget());
|
||||
assertEquals(null, invocationMessage.getInvocationId());
|
||||
assertEquals(null, invocationMessage.getHeaders());
|
||||
assertEquals(null, invocationMessage.getStreamIds());
|
||||
|
||||
@SuppressWarnings("unchecked")
|
||||
ArrayList<HashMap<String, ArrayList<Character>>> result = (ArrayList<HashMap<String, ArrayList<Character>>>)invocationMessage.getArguments()[0];
|
||||
assertEquals(2, result.size());
|
||||
|
||||
HashMap<String, ArrayList<Character>> firstMap = result.get(0);
|
||||
HashMap<String, ArrayList<Character>> secondMap = result.get(1);
|
||||
|
||||
assertEquals(2, firstMap.keySet().size());
|
||||
assertEquals(2, secondMap.keySet().size());
|
||||
|
||||
ArrayList<Character> firstList = firstMap.get("one");
|
||||
ArrayList<Character> secondList = firstMap.get("two");
|
||||
|
||||
ArrayList<Character> thirdList = secondMap.get("three");
|
||||
ArrayList<Character> fourthList = secondMap.get("four");
|
||||
|
||||
assertEquals(2, firstList.size());
|
||||
assertEquals(2, secondList.size());
|
||||
assertEquals(2, thirdList.size());
|
||||
assertEquals(2, fourthList.size());
|
||||
|
||||
assertEquals('a', (char) firstList.get(0));
|
||||
assertEquals('b', (char) firstList.get(1));
|
||||
|
||||
assertEquals('\ubeef', (char) secondList.get(0));
|
||||
assertEquals('\uabcd', (char) secondList.get(1));
|
||||
|
||||
assertEquals('5', (char) thirdList.get(0));
|
||||
assertEquals('9', (char) thirdList.get(1));
|
||||
|
||||
assertEquals('^', (char) fourthList.get(0));
|
||||
assertEquals('*', (char) fourthList.get(1));
|
||||
}
|
||||
|
||||
@Test
|
||||
public void parseSingleMessageCustomPojoArg() {
|
||||
String stringifiedMessage = "{\"type\":1,\"target\":\"test\",\"arguments\":[{\"firstName\":\"John\",\"lastName\":\"Doe\",\"age\":30,\"t\":[5,8]}]}\u001E";
|
||||
ByteBuffer message = TestUtils.stringToByteBuffer(stringifiedMessage);
|
||||
|
||||
TestBinder binder = new TestBinder(new Type[] { (new TypeReference<PersonPojo<ArrayList<Short>>>() { }).getType() }, null);
|
||||
|
||||
List<HubMessage> messages = jsonHubProtocol.parseMessages(message, binder);
|
||||
|
||||
//We know it's only one message
|
||||
assertNotNull(messages);
|
||||
assertEquals(1, messages.size());
|
||||
|
||||
assertEquals(HubMessageType.INVOCATION, messages.get(0).getMessageType());
|
||||
|
||||
//We can safely cast here because we know that it's an invocation message.
|
||||
InvocationMessage invocationMessage = (InvocationMessage) messages.get(0);
|
||||
|
||||
assertEquals("test", invocationMessage.getTarget());
|
||||
assertEquals(null, invocationMessage.getInvocationId());
|
||||
assertEquals(null, invocationMessage.getHeaders());
|
||||
assertEquals(null, invocationMessage.getStreamIds());
|
||||
|
||||
@SuppressWarnings("unchecked")
|
||||
PersonPojo<ArrayList<Short>> result = (PersonPojo<ArrayList<Short>>)invocationMessage.getArguments()[0];
|
||||
assertEquals("John", result.getFirstName());
|
||||
assertEquals("Doe", result.getLastName());
|
||||
assertEquals(30, result.getAge());
|
||||
|
||||
ArrayList<Short> generic = result.getT();
|
||||
assertEquals(2, generic.size());
|
||||
assertEquals((short)5, (short)generic.get(0));
|
||||
assertEquals((short)8, (short)generic.get(1));
|
||||
}
|
||||
|
||||
@Test
|
||||
public void parseMessageWithOutOfOrderProperties() {
|
||||
String stringifiedMessage = "{\"arguments\":[42, 24],\"type\":1,\"target\":\"test\"}\u001E";
|
||||
TestBinder binder = new TestBinder(new InvocationMessage("1", "test", new Object[] { 42, 24 }));
|
||||
ByteBuffer message = TestUtils.stringToByteBuffer(stringifiedMessage);
|
||||
TestBinder binder = new TestBinder(new Type[] { int.class, int.class }, null);
|
||||
|
||||
HubMessage[] messages = jsonHubProtocol.parseMessages(stringifiedMessage, binder);
|
||||
List<HubMessage> messages = jsonHubProtocol.parseMessages(message, binder);
|
||||
|
||||
assertNotNull(messages);
|
||||
assertEquals(1, messages.size());
|
||||
|
||||
// We know it's only one message
|
||||
assertEquals(HubMessageType.INVOCATION, messages[0].getMessageType());
|
||||
assertEquals(HubMessageType.INVOCATION, messages.get(0).getMessageType());
|
||||
|
||||
InvocationMessage message = (InvocationMessage) messages[0];
|
||||
assertEquals("test", message.getTarget());
|
||||
assertEquals(null, message.getInvocationId());
|
||||
int messageResult = (int) message.getArguments()[0];
|
||||
int messageResult2 = (int) message.getArguments()[1];
|
||||
InvocationMessage invocationMessage = (InvocationMessage) messages.get(0);
|
||||
assertEquals("test", invocationMessage.getTarget());
|
||||
assertEquals(null, invocationMessage.getInvocationId());
|
||||
int messageResult = (int) invocationMessage.getArguments()[0];
|
||||
int messageResult2 = (int) invocationMessage.getArguments()[1];
|
||||
assertEquals(42, messageResult);
|
||||
assertEquals(24, messageResult2);
|
||||
}
|
||||
|
|
@ -198,134 +312,111 @@ class JsonHubProtocolTest {
|
|||
@Test
|
||||
public void parseCompletionMessageWithOutOfOrderProperties() {
|
||||
String stringifiedMessage = "{\"type\":3,\"result\":42,\"invocationId\":\"1\"}\u001E";
|
||||
TestBinder binder = new TestBinder(new CompletionMessage("1", 42, null));
|
||||
ByteBuffer message = TestUtils.stringToByteBuffer(stringifiedMessage);
|
||||
TestBinder binder = new TestBinder(null, int.class);
|
||||
|
||||
HubMessage[] messages = jsonHubProtocol.parseMessages(stringifiedMessage, binder);
|
||||
List<HubMessage> messages = jsonHubProtocol.parseMessages(message, binder);
|
||||
|
||||
assertNotNull(messages);
|
||||
assertEquals(1, messages.size());
|
||||
|
||||
// We know it's only one message
|
||||
assertEquals(HubMessageType.COMPLETION, messages[0].getMessageType());
|
||||
assertEquals(HubMessageType.COMPLETION, messages.get(0).getMessageType());
|
||||
|
||||
CompletionMessage message = (CompletionMessage) messages[0];
|
||||
assertEquals(null, message.getError());
|
||||
assertEquals(42 , message.getResult());
|
||||
CompletionMessage completionMessage = (CompletionMessage) messages.get(0);
|
||||
assertEquals(null, completionMessage.getError());
|
||||
assertEquals(42 , completionMessage.getResult());
|
||||
}
|
||||
|
||||
@Test
|
||||
public void invocationBindingFailureWhileParsingTooManyArgumentsWithOutOfOrderProperties() {
|
||||
String stringifiedMessage = "{\"arguments\":[42, 24],\"type\":1,\"target\":\"test\"}\u001E";
|
||||
TestBinder binder = new TestBinder(new InvocationMessage(null, "test", new Object[] { 42 }));
|
||||
ByteBuffer message = TestUtils.stringToByteBuffer(stringifiedMessage);
|
||||
TestBinder binder = new TestBinder(new Type[] { int.class }, null);
|
||||
|
||||
HubMessage[] messages = jsonHubProtocol.parseMessages(stringifiedMessage, binder);
|
||||
assertEquals(1, messages.length);
|
||||
assertEquals(InvocationBindingFailureMessage.class, messages[0].getClass());
|
||||
InvocationBindingFailureMessage message = (InvocationBindingFailureMessage)messages[0];
|
||||
assertEquals("Invocation provides 2 argument(s) but target expects 1.", message.getException().getMessage());
|
||||
List<HubMessage> messages = jsonHubProtocol.parseMessages(message, binder);
|
||||
|
||||
assertNotNull(messages);
|
||||
assertEquals(1, messages.size());
|
||||
|
||||
assertEquals(InvocationBindingFailureMessage.class, messages.get(0).getClass());
|
||||
InvocationBindingFailureMessage invocationBindingFailureMessage = (InvocationBindingFailureMessage)messages.get(0);
|
||||
assertEquals("Invocation provides 2 argument(s) but target expects 1.", invocationBindingFailureMessage.getException().getMessage());
|
||||
}
|
||||
|
||||
@Test
|
||||
public void invocationBindingFailureWhileParsingTooManyArguments() {
|
||||
String stringifiedMessage = "{\"type\":1,\"target\":\"test\",\"arguments\":[42, 24]}\u001E";
|
||||
TestBinder binder = new TestBinder(new InvocationMessage(null, "test", new Object[] { 42 }));
|
||||
ByteBuffer message = TestUtils.stringToByteBuffer(stringifiedMessage);
|
||||
TestBinder binder = new TestBinder(new Type[] { int.class }, null);
|
||||
|
||||
HubMessage[] messages = jsonHubProtocol.parseMessages(stringifiedMessage, binder);
|
||||
assertEquals(1, messages.length);
|
||||
assertEquals(InvocationBindingFailureMessage.class, messages[0].getClass());
|
||||
InvocationBindingFailureMessage message = (InvocationBindingFailureMessage) messages[0];
|
||||
assertEquals("Invocation provides 2 argument(s) but target expects 1.", message.getException().getMessage());
|
||||
List<HubMessage> messages = jsonHubProtocol.parseMessages(message, binder);
|
||||
|
||||
assertNotNull(messages);
|
||||
assertEquals(1, messages.size());
|
||||
|
||||
assertEquals(InvocationBindingFailureMessage.class, messages.get(0).getClass());
|
||||
InvocationBindingFailureMessage invocationBindingFailureMessage = (InvocationBindingFailureMessage) messages.get(0);
|
||||
assertEquals("Invocation provides 2 argument(s) but target expects 1.", invocationBindingFailureMessage.getException().getMessage());
|
||||
}
|
||||
|
||||
@Test
|
||||
public void invocationBindingFailureWhileParsingTooFewArguments() {
|
||||
String stringifiedMessage = "{\"type\":1,\"target\":\"test\",\"arguments\":[42]}\u001E";
|
||||
TestBinder binder = new TestBinder(new InvocationMessage(null, "test", new Object[] { 42, 24 }));
|
||||
ByteBuffer message = TestUtils.stringToByteBuffer(stringifiedMessage);
|
||||
TestBinder binder = new TestBinder(new Type[] { int.class, int.class }, null);
|
||||
|
||||
HubMessage[] messages = jsonHubProtocol.parseMessages(stringifiedMessage, binder);
|
||||
assertEquals(1, messages.length);
|
||||
assertEquals(InvocationBindingFailureMessage.class, messages[0].getClass());
|
||||
InvocationBindingFailureMessage message = (InvocationBindingFailureMessage) messages[0];
|
||||
assertEquals("Invocation provides 1 argument(s) but target expects 2.", message.getException().getMessage());
|
||||
List<HubMessage> messages = jsonHubProtocol.parseMessages(message, binder);
|
||||
|
||||
assertNotNull(messages);
|
||||
assertEquals(1, messages.size());
|
||||
|
||||
assertEquals(InvocationBindingFailureMessage.class, messages.get(0).getClass());
|
||||
InvocationBindingFailureMessage invocationBindingFailureMessage = (InvocationBindingFailureMessage) messages.get(0);
|
||||
assertEquals("Invocation provides 1 argument(s) but target expects 2.", invocationBindingFailureMessage.getException().getMessage());
|
||||
}
|
||||
|
||||
@Test
|
||||
public void invocationBindingFailureWhenParsingIncorrectType() {
|
||||
String stringifiedMessage = "{\"type\":1,\"target\":\"test\",\"arguments\":[\"true\"]}\u001E";
|
||||
TestBinder binder = new TestBinder(new InvocationMessage(null, "test", new Object[] { 42 }));
|
||||
ByteBuffer message = TestUtils.stringToByteBuffer(stringifiedMessage);
|
||||
TestBinder binder = new TestBinder(new Type[] { int.class }, null);
|
||||
|
||||
HubMessage[] messages = jsonHubProtocol.parseMessages(stringifiedMessage, binder);
|
||||
assertEquals(1, messages.length);
|
||||
assertEquals(InvocationBindingFailureMessage.class, messages[0].getClass());
|
||||
InvocationBindingFailureMessage message = (InvocationBindingFailureMessage) messages[0];
|
||||
assertEquals("java.lang.NumberFormatException: For input string: \"true\"", message.getException().getMessage());
|
||||
List<HubMessage> messages = jsonHubProtocol.parseMessages(message, binder);
|
||||
|
||||
assertNotNull(messages);
|
||||
assertEquals(1, messages.size());
|
||||
|
||||
assertEquals(InvocationBindingFailureMessage.class, messages.get(0).getClass());
|
||||
InvocationBindingFailureMessage invocationBindingFailureMessage = (InvocationBindingFailureMessage) messages.get(0);
|
||||
assertEquals("java.lang.NumberFormatException: For input string: \"true\"", invocationBindingFailureMessage.getException().getMessage());
|
||||
}
|
||||
|
||||
@Test
|
||||
public void invocationBindingFailureStillReadsJsonPayloadAfterFailure() {
|
||||
String stringifiedMessage = "{\"type\":1,\"target\":\"test\",\"arguments\":[\"true\"],\"invocationId\":\"123\"}\u001E";
|
||||
TestBinder binder = new TestBinder(new InvocationMessage(null, "test", new Object[] { 42 }));
|
||||
ByteBuffer message = TestUtils.stringToByteBuffer(stringifiedMessage);
|
||||
TestBinder binder = new TestBinder(new Type[] { int.class }, null);
|
||||
|
||||
HubMessage[] messages = jsonHubProtocol.parseMessages(stringifiedMessage, binder);
|
||||
assertEquals(1, messages.length);
|
||||
assertEquals(InvocationBindingFailureMessage.class, messages[0].getClass());
|
||||
InvocationBindingFailureMessage message = (InvocationBindingFailureMessage) messages[0];
|
||||
assertEquals("java.lang.NumberFormatException: For input string: \"true\"", message.getException().getMessage());
|
||||
assertEquals("123", message.getInvocationId());
|
||||
List<HubMessage> messages = jsonHubProtocol.parseMessages(message, binder);
|
||||
|
||||
assertNotNull(messages);
|
||||
assertEquals(1, messages.size());
|
||||
|
||||
assertEquals(InvocationBindingFailureMessage.class, messages.get(0).getClass());
|
||||
InvocationBindingFailureMessage invocationBindingFailureMessage = (InvocationBindingFailureMessage) messages.get(0);
|
||||
assertEquals("java.lang.NumberFormatException: For input string: \"true\"", invocationBindingFailureMessage.getException().getMessage());
|
||||
assertEquals("123", invocationBindingFailureMessage.getInvocationId());
|
||||
}
|
||||
|
||||
@Test
|
||||
public void errorWhileParsingIncompleteMessage() {
|
||||
String stringifiedMessage = "{\"type\":1,\"target\":\"test\",\"arguments\":";
|
||||
TestBinder binder = new TestBinder(new InvocationMessage(null, "test", new Object[] { 42, 24 }));
|
||||
ByteBuffer message = TestUtils.stringToByteBuffer(stringifiedMessage);
|
||||
TestBinder binder = new TestBinder(new Type[] { int.class, int.class }, null);
|
||||
|
||||
RuntimeException exception = assertThrows(RuntimeException.class,
|
||||
() -> jsonHubProtocol.parseMessages(stringifiedMessage, binder));
|
||||
() -> jsonHubProtocol.parseMessages(message, binder));
|
||||
assertEquals("Message is incomplete.", exception.getMessage());
|
||||
}
|
||||
|
||||
private class TestBinder implements InvocationBinder {
|
||||
private Class<?>[] paramTypes = null;
|
||||
private Class<?> returnType = null;
|
||||
|
||||
public TestBinder(HubMessage expectedMessage) {
|
||||
if (expectedMessage == null) {
|
||||
return;
|
||||
}
|
||||
|
||||
switch (expectedMessage.getMessageType()) {
|
||||
case STREAM_INVOCATION:
|
||||
ArrayList<Class<?>> streamTypes = new ArrayList<>();
|
||||
for (Object obj : ((StreamInvocationMessage) expectedMessage).getArguments()) {
|
||||
streamTypes.add(obj.getClass());
|
||||
}
|
||||
paramTypes = streamTypes.toArray(new Class<?>[streamTypes.size()]);
|
||||
break;
|
||||
case INVOCATION:
|
||||
ArrayList<Class<?>> types = new ArrayList<>();
|
||||
for (Object obj : ((InvocationMessage) expectedMessage).getArguments()) {
|
||||
types.add(obj.getClass());
|
||||
}
|
||||
paramTypes = types.toArray(new Class<?>[types.size()]);
|
||||
break;
|
||||
case STREAM_ITEM:
|
||||
break;
|
||||
case COMPLETION:
|
||||
returnType = ((CompletionMessage)expectedMessage).getResult().getClass();
|
||||
break;
|
||||
default:
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public Class<?> getReturnType(String invocationId) {
|
||||
return returnType;
|
||||
}
|
||||
|
||||
@Override
|
||||
public List<Class<?>> getParameterTypes(String methodName) {
|
||||
if (paramTypes == null) {
|
||||
return new ArrayList<>();
|
||||
}
|
||||
return new ArrayList<Class<?>>(Arrays.asList(paramTypes));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -5,6 +5,7 @@ package com.microsoft.signalr;
|
|||
|
||||
import static org.junit.jupiter.api.Assertions.*;
|
||||
|
||||
import java.nio.ByteBuffer;
|
||||
import java.util.HashMap;
|
||||
import java.util.Map;
|
||||
import java.util.concurrent.TimeUnit;
|
||||
|
|
@ -22,7 +23,7 @@ public class LongPollingTransportTest {
|
|||
@Test
|
||||
public void LongPollingFailsToConnectWith404Response() {
|
||||
TestHttpClient client = new TestHttpClient()
|
||||
.on("GET", (req) -> Single.just(new HttpResponse(404, "", "")));
|
||||
.on("GET", (req) -> Single.just(new HttpResponse(404, "", TestUtils.emptyByteBuffer)));
|
||||
|
||||
Map<String, String> headers = new HashMap<>();
|
||||
LongPollingTransport transport = new LongPollingTransport(headers, client, Single.just(""));
|
||||
|
|
@ -35,11 +36,12 @@ public class LongPollingTransportTest {
|
|||
@Test
|
||||
public void LongPollingTransportCantSendBeforeStart() {
|
||||
TestHttpClient client = new TestHttpClient()
|
||||
.on("GET", (req) -> Single.just(new HttpResponse(404, "", "")));
|
||||
.on("GET", (req) -> Single.just(new HttpResponse(404, "", TestUtils.emptyByteBuffer)));
|
||||
|
||||
Map<String, String> headers = new HashMap<>();
|
||||
LongPollingTransport transport = new LongPollingTransport(headers, client, Single.just(""));
|
||||
Throwable exception = assertThrows(RuntimeException.class, () -> transport.send("First").timeout(1, TimeUnit.SECONDS).blockingAwait());
|
||||
ByteBuffer sendBuffer = TestUtils.stringToByteBuffer("First");
|
||||
Throwable exception = assertThrows(RuntimeException.class, () -> transport.send(sendBuffer).timeout(1, TimeUnit.SECONDS).blockingAwait());
|
||||
assertEquals(Exception.class, exception.getCause().getClass());
|
||||
assertEquals("Cannot send unless the transport is active.", exception.getCause().getMessage());
|
||||
assertFalse(transport.isActive());
|
||||
|
|
@ -53,9 +55,9 @@ public class LongPollingTransportTest {
|
|||
.on("GET", (req) -> {
|
||||
if (firstPoll.get()) {
|
||||
firstPoll.set(false);
|
||||
return Single.just(new HttpResponse(200, "", ""));
|
||||
return Single.just(new HttpResponse(200, "", TestUtils.emptyByteBuffer));
|
||||
}
|
||||
return Single.just(new HttpResponse(204, "", ""));
|
||||
return Single.just(new HttpResponse(204, "", TestUtils.emptyByteBuffer));
|
||||
});
|
||||
|
||||
Map<String, String> headers = new HashMap<>();
|
||||
|
|
@ -81,9 +83,9 @@ public class LongPollingTransportTest {
|
|||
.on("GET", (req) -> {
|
||||
if (firstPoll.get()) {
|
||||
firstPoll.set(false);
|
||||
return Single.just(new HttpResponse(200, "", ""));
|
||||
return Single.just(new HttpResponse(200, "", TestUtils.emptyByteBuffer));
|
||||
}
|
||||
return Single.just(new HttpResponse(999, "", ""));
|
||||
return Single.just(new HttpResponse(999, "", TestUtils.emptyByteBuffer));
|
||||
});
|
||||
|
||||
Map<String, String> headers = new HashMap<>();
|
||||
|
|
@ -104,7 +106,7 @@ public class LongPollingTransportTest {
|
|||
@Test
|
||||
public void CanSetAndTriggerOnReceive() {
|
||||
TestHttpClient client = new TestHttpClient()
|
||||
.on("GET", (req) -> Single.just(new HttpResponse(200, "", "")));
|
||||
.on("GET", (req) -> Single.just(new HttpResponse(200, "", TestUtils.emptyByteBuffer)));
|
||||
|
||||
Map<String, String> headers = new HashMap<>();
|
||||
LongPollingTransport transport = new LongPollingTransport(headers, client, Single.just(""));
|
||||
|
|
@ -112,12 +114,13 @@ public class LongPollingTransportTest {
|
|||
AtomicBoolean onReceivedRan = new AtomicBoolean(false);
|
||||
transport.setOnReceive((message) -> {
|
||||
onReceivedRan.set(true);
|
||||
assertEquals("TEST", message);
|
||||
assertEquals("TEST", TestUtils.byteBufferToString(message));
|
||||
});
|
||||
|
||||
// The transport doesn't need to be active to trigger onReceive for the case
|
||||
// when we are handling the last outstanding poll.
|
||||
transport.onReceive("TEST");
|
||||
ByteBuffer onReceiveBuffer = TestUtils.stringToByteBuffer("TEST");
|
||||
transport.onReceive(onReceiveBuffer);
|
||||
assertTrue(onReceivedRan.get());
|
||||
}
|
||||
|
||||
|
|
@ -129,13 +132,13 @@ public class LongPollingTransportTest {
|
|||
.on("GET", (req) -> {
|
||||
if (requestCount.get() == 0) {
|
||||
requestCount.incrementAndGet();
|
||||
return Single.just(new HttpResponse(200, "", ""));
|
||||
return Single.just(new HttpResponse(200, "", TestUtils.emptyByteBuffer));
|
||||
} else if (requestCount.get() == 1) {
|
||||
requestCount.incrementAndGet();
|
||||
return Single.just(new HttpResponse(200, "", "TEST"));
|
||||
return Single.just(new HttpResponse(200, "", TestUtils.stringToByteBuffer("TEST")));
|
||||
}
|
||||
|
||||
return Single.just(new HttpResponse(204, "", ""));
|
||||
return Single.just(new HttpResponse(204, "", TestUtils.emptyByteBuffer));
|
||||
});
|
||||
|
||||
Map<String, String> headers = new HashMap<>();
|
||||
|
|
@ -145,7 +148,7 @@ public class LongPollingTransportTest {
|
|||
AtomicReference<String> message = new AtomicReference<>();
|
||||
transport.setOnReceive((msg -> {
|
||||
onReceiveCalled.set(true);
|
||||
message.set(msg);
|
||||
message.set(TestUtils.byteBufferToString(msg));
|
||||
block.onComplete();
|
||||
}) );
|
||||
|
||||
|
|
@ -165,19 +168,19 @@ public class LongPollingTransportTest {
|
|||
.on("GET", (req) -> {
|
||||
if (requestCount.get() == 0) {
|
||||
requestCount.incrementAndGet();
|
||||
return Single.just(new HttpResponse(200, "", ""));
|
||||
return Single.just(new HttpResponse(200, "", TestUtils.emptyByteBuffer));
|
||||
} else if (requestCount.get() == 1) {
|
||||
requestCount.incrementAndGet();
|
||||
return Single.just(new HttpResponse(200, "", "FIRST"));
|
||||
return Single.just(new HttpResponse(200, "", TestUtils.stringToByteBuffer("FIRST")));
|
||||
} else if (requestCount.get() == 2) {
|
||||
requestCount.incrementAndGet();
|
||||
return Single.just(new HttpResponse(200, "", "SECOND"));
|
||||
return Single.just(new HttpResponse(200, "", TestUtils.stringToByteBuffer("SECOND")));
|
||||
} else if (requestCount.get() == 3) {
|
||||
requestCount.incrementAndGet();
|
||||
return Single.just(new HttpResponse(200, "", "THIRD"));
|
||||
return Single.just(new HttpResponse(200, "", TestUtils.stringToByteBuffer("THIRD")));
|
||||
}
|
||||
|
||||
return Single.just(new HttpResponse(204, "", ""));
|
||||
return Single.just(new HttpResponse(204, "", TestUtils.emptyByteBuffer));
|
||||
});
|
||||
|
||||
Map<String, String> headers = new HashMap<>();
|
||||
|
|
@ -188,7 +191,7 @@ public class LongPollingTransportTest {
|
|||
AtomicInteger messageCount = new AtomicInteger();
|
||||
transport.setOnReceive((msg) -> {
|
||||
onReceiveCalled.set(true);
|
||||
message.set(message.get() + msg);
|
||||
message.set(message.get() + TestUtils.byteBufferToString(msg));
|
||||
if (messageCount.incrementAndGet() == 3) {
|
||||
blocker.onComplete();
|
||||
}
|
||||
|
|
@ -211,14 +214,14 @@ public class LongPollingTransportTest {
|
|||
.on("GET", (req) -> {
|
||||
if (requestCount.get() == 0) {
|
||||
requestCount.incrementAndGet();
|
||||
return Single.just(new HttpResponse(200, "", ""));
|
||||
return Single.just(new HttpResponse(200, "", TestUtils.emptyByteBuffer));
|
||||
}
|
||||
assertTrue(close.blockingAwait(1, TimeUnit.SECONDS));
|
||||
return Single.just(new HttpResponse(204, "", ""));
|
||||
return Single.just(new HttpResponse(204, "", TestUtils.emptyByteBuffer));
|
||||
}).on("POST", (req) -> {
|
||||
assertFalse(req.getHeaders().isEmpty());
|
||||
headerValue.set(req.getHeaders().get("KEY"));
|
||||
return Single.just(new HttpResponse(200, "", ""));
|
||||
return Single.just(new HttpResponse(200, "", TestUtils.emptyByteBuffer));
|
||||
});
|
||||
|
||||
Map<String, String> headers = new HashMap<>();
|
||||
|
|
@ -227,7 +230,8 @@ public class LongPollingTransportTest {
|
|||
transport.setOnClose((error) -> {});
|
||||
|
||||
transport.start("http://example.com").timeout(1, TimeUnit.SECONDS).blockingAwait();
|
||||
assertTrue(transport.send("TEST").blockingAwait(1, TimeUnit.SECONDS));
|
||||
ByteBuffer sendBuffer = TestUtils.stringToByteBuffer("TEST");
|
||||
assertTrue(transport.send(sendBuffer).blockingAwait(1, TimeUnit.SECONDS));
|
||||
close.onComplete();
|
||||
assertEquals(headerValue.get(), "VALUE");
|
||||
}
|
||||
|
|
@ -241,15 +245,15 @@ public class LongPollingTransportTest {
|
|||
.on("GET", (req) -> {
|
||||
if (requestCount.get() == 0) {
|
||||
requestCount.incrementAndGet();
|
||||
return Single.just(new HttpResponse(200, "", ""));
|
||||
return Single.just(new HttpResponse(200, "", TestUtils.emptyByteBuffer));
|
||||
}
|
||||
assertTrue(close.blockingAwait(1, TimeUnit.SECONDS));
|
||||
return Single.just(new HttpResponse(204, "", ""));
|
||||
return Single.just(new HttpResponse(204, "", TestUtils.emptyByteBuffer));
|
||||
})
|
||||
.on("POST", (req) -> {
|
||||
assertFalse(req.getHeaders().isEmpty());
|
||||
headerValue.set(req.getHeaders().get("Authorization"));
|
||||
return Single.just(new HttpResponse(200, "", ""));
|
||||
return Single.just(new HttpResponse(200, "", TestUtils.emptyByteBuffer));
|
||||
});
|
||||
|
||||
Map<String, String> headers = new HashMap<>();
|
||||
|
|
@ -258,7 +262,8 @@ public class LongPollingTransportTest {
|
|||
transport.setOnClose((error) -> {});
|
||||
|
||||
transport.start("http://example.com").timeout(1, TimeUnit.SECONDS).blockingAwait();
|
||||
assertTrue(transport.send("TEST").blockingAwait(1, TimeUnit.SECONDS));
|
||||
ByteBuffer sendBuffer = TestUtils.stringToByteBuffer("TEST");
|
||||
assertTrue(transport.send(sendBuffer).blockingAwait(1, TimeUnit.SECONDS));
|
||||
assertEquals(headerValue.get(), "Bearer TOKEN");
|
||||
close.onComplete();
|
||||
}
|
||||
|
|
@ -273,17 +278,17 @@ public class LongPollingTransportTest {
|
|||
.on("GET", (req) -> {
|
||||
if (requestCount.get() == 0) {
|
||||
requestCount.incrementAndGet();
|
||||
return Single.just(new HttpResponse(200, "", ""));
|
||||
return Single.just(new HttpResponse(200, "", TestUtils.emptyByteBuffer));
|
||||
}
|
||||
assertEquals("Bearer TOKEN1", req.getHeaders().get("Authorization"));
|
||||
secondGet.onComplete();
|
||||
assertTrue(close.blockingAwait(1, TimeUnit.SECONDS));
|
||||
return Single.just(new HttpResponse(204, "", ""));
|
||||
return Single.just(new HttpResponse(204, "", TestUtils.emptyByteBuffer));
|
||||
})
|
||||
.on("POST", (req) -> {
|
||||
assertFalse(req.getHeaders().isEmpty());
|
||||
headerValue.set(req.getHeaders().get("Authorization"));
|
||||
return Single.just(new HttpResponse(200, "", ""));
|
||||
return Single.just(new HttpResponse(200, "", TestUtils.emptyByteBuffer));
|
||||
});
|
||||
|
||||
AtomicInteger i = new AtomicInteger(0);
|
||||
|
|
@ -294,7 +299,8 @@ public class LongPollingTransportTest {
|
|||
|
||||
transport.start("http://example.com").timeout(1, TimeUnit.SECONDS).blockingAwait();
|
||||
secondGet.blockingAwait(1, TimeUnit.SECONDS);
|
||||
assertTrue(transport.send("TEST").blockingAwait(1, TimeUnit.SECONDS));
|
||||
ByteBuffer sendBuffer = TestUtils.stringToByteBuffer("TEST");
|
||||
assertTrue(transport.send(sendBuffer).blockingAwait(1, TimeUnit.SECONDS));
|
||||
assertEquals("Bearer TOKEN2", headerValue.get());
|
||||
close.onComplete();
|
||||
}
|
||||
|
|
@ -307,9 +313,9 @@ public class LongPollingTransportTest {
|
|||
.on("GET", (req) -> {
|
||||
if (firstPoll.get()) {
|
||||
firstPoll.set(false);
|
||||
return Single.just(new HttpResponse(200, "", ""));
|
||||
return Single.just(new HttpResponse(200, "", TestUtils.emptyByteBuffer));
|
||||
}
|
||||
return Single.just(new HttpResponse(204, "", ""));
|
||||
return Single.just(new HttpResponse(204, "", TestUtils.emptyByteBuffer));
|
||||
});
|
||||
|
||||
Map<String, String> headers = new HashMap<>();
|
||||
|
|
@ -341,16 +347,16 @@ public class LongPollingTransportTest {
|
|||
.on("GET", (req) -> {
|
||||
if (firstPoll.get()) {
|
||||
firstPoll.set(false);
|
||||
return Single.just(new HttpResponse(200, "", ""));
|
||||
return Single.just(new HttpResponse(200, "", TestUtils.emptyByteBuffer));
|
||||
} else {
|
||||
assertTrue(block.blockingAwait(1, TimeUnit.SECONDS));
|
||||
return Single.just(new HttpResponse(204, "", ""));
|
||||
return Single.just(new HttpResponse(204, "", TestUtils.emptyByteBuffer));
|
||||
}
|
||||
})
|
||||
.on("DELETE", (req) ->{
|
||||
//Unblock the last poll when we sent the DELETE request.
|
||||
block.onComplete();
|
||||
return Single.just(new HttpResponse(200, "", ""));
|
||||
return Single.just(new HttpResponse(200, "", TestUtils.emptyByteBuffer));
|
||||
});
|
||||
|
||||
Map<String, String> headers = new HashMap<>();
|
||||
|
|
|
|||
|
|
@ -0,0 +1,919 @@
|
|||
// Copyright (c) .NET Foundation. All rights reserved.
|
||||
// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information.
|
||||
|
||||
package com.microsoft.signalr;
|
||||
|
||||
import static org.junit.jupiter.api.Assertions.assertEquals;
|
||||
import static org.junit.jupiter.api.Assertions.assertNotNull;
|
||||
import static org.junit.jupiter.api.Assertions.assertThrows;
|
||||
import static org.junit.jupiter.api.Assertions.assertTrue;
|
||||
|
||||
import java.lang.reflect.Type;
|
||||
import java.nio.ByteBuffer;
|
||||
import java.util.ArrayList;
|
||||
import java.util.HashMap;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
import java.util.SortedMap;
|
||||
import java.util.TreeMap;
|
||||
|
||||
import org.junit.jupiter.api.Test;
|
||||
|
||||
import com.fasterxml.jackson.core.type.TypeReference;
|
||||
|
||||
class MessagePackHubProtocolTest {
|
||||
private MessagePackHubProtocol messagePackHubProtocol = new MessagePackHubProtocol();
|
||||
|
||||
@Test
|
||||
public void checkProtocolName() {
|
||||
assertEquals("messagepack", messagePackHubProtocol.getName());
|
||||
}
|
||||
|
||||
@Test
|
||||
public void checkVersionNumber() {
|
||||
assertEquals(1, messagePackHubProtocol.getVersion());
|
||||
}
|
||||
|
||||
@Test
|
||||
public void checkTransferFormat() {
|
||||
assertEquals(TransferFormat.BINARY, messagePackHubProtocol.getTransferFormat());
|
||||
}
|
||||
|
||||
@Test
|
||||
public void verifyWriteInvocationMessage() {
|
||||
InvocationMessage invocationMessage = new InvocationMessage(null, null, "test", new Object[] { 42 }, null);
|
||||
ByteBuffer result = messagePackHubProtocol.writeMessage(invocationMessage);
|
||||
byte[] expectedBytes = {0x0C, (byte) 0x96, 0x01, (byte) 0x80, (byte) 0xC0, (byte) 0xA4, 0x74, 0x65, 0x73,
|
||||
0x74, (byte) 0x91, 0x2A, (byte) 0x90};
|
||||
ByteString expectedResult = ByteString.of(expectedBytes);
|
||||
assertEquals(expectedResult, ByteString.of(result));
|
||||
}
|
||||
|
||||
@Test
|
||||
public void verifyWriteInvocationMessageWithHeaders() {
|
||||
Map<String, String> headers = new HashMap<String, String>();
|
||||
headers.put("a", "b");
|
||||
headers.put("c", "d");
|
||||
InvocationMessage invocationMessage = new InvocationMessage(headers, null, "test", new Object[] { 42 }, null);
|
||||
ByteBuffer result = messagePackHubProtocol.writeMessage(invocationMessage);
|
||||
byte[] expectedBytes = {0x14, (byte) 0x96, 0x01, (byte) 0x82, (byte) 0xA1, 0x61, (byte) 0xA1, 0x62, (byte) 0xA1, 0x63,
|
||||
(byte) 0xA1, 0x64, (byte) 0xC0, (byte) 0xA4, 0x74, 0x65, 0x73, 0x74, (byte) 0x91, 0x2A, (byte) 0x90};
|
||||
ByteString expectedResult = ByteString.of(expectedBytes);
|
||||
assertEquals(expectedResult, ByteString.of(result));
|
||||
}
|
||||
|
||||
@Test
|
||||
public void verifyWriteStreamItem() {
|
||||
StreamItem streamItem = new StreamItem(null, "id", 42);
|
||||
ByteBuffer result = messagePackHubProtocol.writeMessage(streamItem);
|
||||
byte[] expectedBytes = {0x07, (byte) 0x94, 0x02, (byte) 0x80, (byte) 0xA2, 0x69, 0x64, 0x2A};
|
||||
ByteString expectedResult = ByteString.of(expectedBytes);
|
||||
assertEquals(expectedResult, ByteString.of(result));
|
||||
}
|
||||
|
||||
@Test
|
||||
public void verifyWriteCompletionMessageNonVoid() {
|
||||
CompletionMessage completionMessage = new CompletionMessage(null, "id", 42, null);
|
||||
ByteBuffer result = messagePackHubProtocol.writeMessage(completionMessage);
|
||||
byte[] expectedBytes = {0x08, (byte) 0x95, 0x03, (byte) 0x80, (byte) 0xA2, 0x69, 0x64, 0x03, 0x2A};
|
||||
ByteString expectedResult = ByteString.of(expectedBytes);
|
||||
assertEquals(expectedResult, ByteString.of(result));
|
||||
}
|
||||
|
||||
@Test
|
||||
public void verifyWriteCompletionMessageVoid() {
|
||||
CompletionMessage completionMessage = new CompletionMessage(null, "id", null, null);
|
||||
ByteBuffer result = messagePackHubProtocol.writeMessage(completionMessage);
|
||||
byte[] expectedBytes = {0x07, (byte) 0x94, 0x03, (byte) 0x80, (byte) 0xA2, 0x69, 0x64, 0x02};
|
||||
ByteString expectedResult = ByteString.of(expectedBytes);
|
||||
assertEquals(expectedResult, ByteString.of(result));
|
||||
}
|
||||
|
||||
@Test
|
||||
public void verifyWriteCompletionMessageError() {
|
||||
CompletionMessage completionMessage = new CompletionMessage(null, "id", null, "error");
|
||||
ByteBuffer result = messagePackHubProtocol.writeMessage(completionMessage);
|
||||
byte[] expectedBytes = {0x0D, (byte) 0x95, 0x03, (byte) 0x80, (byte) 0xA2, 0x69, 0x64, 0x01, (byte) 0xA5, 0x65, 0x72, 0x72, 0x6F, 0x72};
|
||||
ByteString expectedResult = ByteString.of(expectedBytes);
|
||||
assertEquals(expectedResult, ByteString.of(result));
|
||||
}
|
||||
|
||||
@Test
|
||||
public void verifyWriteStreamInvocationMessage() {
|
||||
List<String> streamIds = new ArrayList<String>();
|
||||
streamIds.add("stream");
|
||||
StreamInvocationMessage streamInvocationMessage = new StreamInvocationMessage(null, "id", "test", new Object[] {42}, streamIds);
|
||||
ByteBuffer result = messagePackHubProtocol.writeMessage(streamInvocationMessage);
|
||||
byte[] expectedBytes = {0x15, (byte) 0x96, 0x04, (byte) 0x80, (byte) 0xA2, 0x69, 0x64, (byte) 0xA4, 0x74, 0x65, 0x73, 0x74, (byte) 0x91,
|
||||
0x2A, (byte) 0x91, (byte) 0xA6, 0x73, 0x74, 0x72, 0x65, 0x61, 0x6D};
|
||||
ByteString expectedResult = ByteString.of(expectedBytes);
|
||||
assertEquals(expectedResult, ByteString.of(result));
|
||||
}
|
||||
|
||||
@Test
|
||||
public void verifyWriteCancelInvocationMessage() {
|
||||
CancelInvocationMessage cancelInvocationMessage = new CancelInvocationMessage(null, "id");
|
||||
ByteBuffer result = messagePackHubProtocol.writeMessage(cancelInvocationMessage);
|
||||
byte[] expectedBytes = {0x06, (byte) 0x93, 0x05, (byte) 0x80, (byte) 0xA2, 0x69, 0x64};
|
||||
ByteString expectedResult = ByteString.of(expectedBytes);
|
||||
assertEquals(expectedResult, ByteString.of(result));
|
||||
}
|
||||
|
||||
@Test
|
||||
public void verifyWritePingMessage() {
|
||||
ByteBuffer result = messagePackHubProtocol.writeMessage(PingMessage.getInstance());
|
||||
byte[] expectedBytes = {0x02, (byte) 0x91, 0x06};
|
||||
ByteString expectedResult = ByteString.of(expectedBytes);
|
||||
assertEquals(expectedResult, ByteString.of(result));
|
||||
}
|
||||
|
||||
@Test
|
||||
public void verifyWriteCloseMessage() {
|
||||
CloseMessage closeMessage = new CloseMessage();
|
||||
ByteBuffer result = messagePackHubProtocol.writeMessage(closeMessage);
|
||||
byte[] expectedBytes = {0x04, (byte) 0x93, 0x07, (byte) 0xC0, (byte) 0xC2};
|
||||
ByteString expectedResult = ByteString.of(expectedBytes);
|
||||
assertEquals(expectedResult, ByteString.of(result));
|
||||
}
|
||||
|
||||
@Test
|
||||
public void verifyWriteCloseMessageWithError() {
|
||||
CloseMessage closeMessage = new CloseMessage("Error");
|
||||
ByteBuffer result = messagePackHubProtocol.writeMessage(closeMessage);
|
||||
byte[] expectedBytes = {0x09, (byte) 0x93, 0x07, (byte) 0xA5, 0x45, 0x72, 0x72, 0x6F, 0x72, (byte) 0xC2};
|
||||
ByteString expectedResult = ByteString.of(expectedBytes);
|
||||
assertEquals(expectedResult, ByteString.of(result));
|
||||
}
|
||||
|
||||
@Test
|
||||
public void parsePingMessage() {
|
||||
byte[] messageBytes = {0x02, (byte) 0x91, 0x06};
|
||||
ByteBuffer message = ByteBuffer.wrap(messageBytes);
|
||||
TestBinder binder = new TestBinder(null, null);
|
||||
|
||||
List<HubMessage> messages = messagePackHubProtocol.parseMessages(message, binder);
|
||||
|
||||
//We know it's only one message
|
||||
assertNotNull(messages);
|
||||
assertEquals(1, messages.size());
|
||||
assertEquals(HubMessageType.PING, messages.get(0).getMessageType());
|
||||
}
|
||||
|
||||
@Test
|
||||
public void parseCloseMessage() {
|
||||
byte[] messageBytes = {0x04, (byte) 0x93, 0x07, (byte) 0xC0, (byte) 0xC2};
|
||||
ByteBuffer message = ByteBuffer.wrap(messageBytes);
|
||||
TestBinder binder = new TestBinder(null, null);
|
||||
|
||||
List<HubMessage> messages = messagePackHubProtocol.parseMessages(message, binder);
|
||||
|
||||
//We know it's only one message
|
||||
assertNotNull(messages);
|
||||
assertEquals(1, messages.size());
|
||||
|
||||
assertEquals(HubMessageType.CLOSE, messages.get(0).getMessageType());
|
||||
|
||||
//We can safely cast here because we know that it's a close message.
|
||||
CloseMessage closeMessage = (CloseMessage) messages.get(0);
|
||||
|
||||
assertEquals(null, closeMessage.getError());
|
||||
}
|
||||
|
||||
@Test
|
||||
public void parseCloseMessageWithError() {
|
||||
byte[] messageBytes = {0x09, (byte) 0x93, 0x07, (byte) 0xA5, 0x45, 0x72, 0x72, 0x6F, 0x72, (byte) 0xC2};
|
||||
ByteBuffer message = ByteBuffer.wrap(messageBytes);
|
||||
TestBinder binder = new TestBinder(null, null);
|
||||
|
||||
List<HubMessage> messages = messagePackHubProtocol.parseMessages(message, binder);
|
||||
|
||||
//We know it's only one message
|
||||
assertNotNull(messages);
|
||||
assertEquals(1, messages.size());
|
||||
|
||||
assertEquals(HubMessageType.CLOSE, messages.get(0).getMessageType());
|
||||
|
||||
//We can safely cast here because we know that it's a close message.
|
||||
CloseMessage closeMessage = (CloseMessage) messages.get(0);
|
||||
|
||||
assertEquals("Error", closeMessage.getError());
|
||||
}
|
||||
|
||||
@Test
|
||||
public void parseSingleInvocationMessage() {
|
||||
byte[] messageBytes = {0x0C, (byte) 0x96, 0x01, (byte) 0x80, (byte) 0xC0, (byte) 0xA4, 0x74, 0x65, 0x73,
|
||||
0x74, (byte) 0x91, 0x2A, (byte) 0x90};
|
||||
ByteBuffer message = ByteBuffer.wrap(messageBytes);
|
||||
TestBinder binder = new TestBinder(new Type[] { int.class }, null);
|
||||
|
||||
List<HubMessage> messages = messagePackHubProtocol.parseMessages(message, binder);
|
||||
|
||||
//We know it's only one message
|
||||
assertNotNull(messages);
|
||||
assertEquals(1, messages.size());
|
||||
|
||||
assertEquals(HubMessageType.INVOCATION, messages.get(0).getMessageType());
|
||||
|
||||
//We can safely cast here because we know that it's an invocation message.
|
||||
InvocationMessage invocationMessage = (InvocationMessage) messages.get(0);
|
||||
|
||||
assertEquals("test", invocationMessage.getTarget());
|
||||
assertEquals(null, invocationMessage.getInvocationId());
|
||||
assertEquals(null, invocationMessage.getHeaders());
|
||||
assertEquals(null, invocationMessage.getStreamIds());
|
||||
|
||||
int messageResult = (int)invocationMessage.getArguments()[0];
|
||||
assertEquals(42, messageResult);
|
||||
}
|
||||
|
||||
@Test
|
||||
public void parseSingleInvocationMessageWithHeaders() {
|
||||
byte[] messageBytes = {0x14, (byte) 0x96, 0x01, (byte) 0x82, (byte) 0xA1, 0x61, (byte) 0xA1, 0x62, (byte) 0xA1, 0x63,
|
||||
(byte) 0xA1, 0x64, (byte) 0xC0, (byte) 0xA4, 0x74, 0x65, 0x73, 0x74, (byte) 0x91, 0x2A, (byte) 0x90};
|
||||
ByteBuffer message = ByteBuffer.wrap(messageBytes);
|
||||
TestBinder binder = new TestBinder(new Type[] { int.class }, null);
|
||||
|
||||
List<HubMessage> messages = messagePackHubProtocol.parseMessages(message, binder);
|
||||
|
||||
//We know it's only one message
|
||||
assertNotNull(messages);
|
||||
assertEquals(1, messages.size());
|
||||
|
||||
assertEquals(HubMessageType.INVOCATION, messages.get(0).getMessageType());
|
||||
|
||||
//We can safely cast here because we know that it's an invocation message.
|
||||
InvocationMessage invocationMessage = (InvocationMessage) messages.get(0);
|
||||
|
||||
assertEquals("test", invocationMessage.getTarget());
|
||||
assertEquals(null, invocationMessage.getInvocationId());
|
||||
|
||||
Map<String, String> headers = invocationMessage.getHeaders();
|
||||
assertEquals(2, headers.size());
|
||||
assertEquals("b", headers.get("a"));
|
||||
assertEquals("d", headers.get("c"));
|
||||
|
||||
assertEquals(null, invocationMessage.getStreamIds());
|
||||
|
||||
int messageResult = (int)invocationMessage.getArguments()[0];
|
||||
assertEquals(42, messageResult);
|
||||
}
|
||||
|
||||
@Test
|
||||
public void parseSingleStreamInvocationMessage() {
|
||||
byte[] messageBytes = {0x12, (byte) 0x96, 0x04, (byte) 0x80, (byte) 0xA6, 0x6D, 0x65, 0x74, 0x68, 0x6F, 0x64,
|
||||
(byte) 0xA4, 0x74, 0x65, 0x73, 0x74, (byte) 0x91, 0x2A, (byte) 0x90};
|
||||
ByteBuffer message = ByteBuffer.wrap(messageBytes);
|
||||
TestBinder binder = new TestBinder(new Type[] { int.class }, null);
|
||||
|
||||
List<HubMessage> messages = messagePackHubProtocol.parseMessages(message, binder);
|
||||
|
||||
//We know it's only one message
|
||||
assertNotNull(messages);
|
||||
assertEquals(1, messages.size());
|
||||
|
||||
assertEquals(HubMessageType.STREAM_INVOCATION, messages.get(0).getMessageType());
|
||||
|
||||
//We can safely cast here because we know that it's a streaminvocation message.
|
||||
StreamInvocationMessage streamInvocationMessage = (StreamInvocationMessage) messages.get(0);
|
||||
|
||||
assertEquals("test", streamInvocationMessage.getTarget());
|
||||
assertEquals("method", streamInvocationMessage.getInvocationId());
|
||||
assertEquals(null, streamInvocationMessage.getHeaders());
|
||||
assertEquals(null, streamInvocationMessage.getStreamIds());
|
||||
|
||||
int messageResult = (int)streamInvocationMessage.getArguments()[0];
|
||||
assertEquals(42, messageResult);
|
||||
}
|
||||
|
||||
@Test
|
||||
public void parseSingleCancelInvocationMessage() {
|
||||
byte[] messageBytes = {0x0A, (byte) 0x93, 0x05, (byte) 0x80, (byte) 0xA6, 0x6D, 0x65, 0x74, 0x68, 0x6F, 0x64};
|
||||
ByteBuffer message = ByteBuffer.wrap(messageBytes);
|
||||
TestBinder binder = new TestBinder(null, null);
|
||||
|
||||
List<HubMessage> messages = messagePackHubProtocol.parseMessages(message, binder);
|
||||
|
||||
//We know it's only one message
|
||||
assertNotNull(messages);
|
||||
assertEquals(1, messages.size());
|
||||
|
||||
assertEquals(HubMessageType.CANCEL_INVOCATION, messages.get(0).getMessageType());
|
||||
|
||||
//We can safely cast here because we know that it's a cancelinvocation message.
|
||||
CancelInvocationMessage cancelInvocationMessage = (CancelInvocationMessage) messages.get(0);
|
||||
|
||||
assertEquals("method", cancelInvocationMessage.getInvocationId());
|
||||
assertEquals(null, cancelInvocationMessage.getHeaders());
|
||||
}
|
||||
|
||||
@Test
|
||||
public void parseTwoMessages() {
|
||||
byte[] messageBytes = {0x0B, (byte) 0x96, 0x01, (byte) 0x80, (byte) 0xC0, (byte) 0xA3, 0x6F, 0x6E, 0x65, (byte) 0x91, 0x2A,
|
||||
(byte) 0x90, 0x0B, (byte) 0x96, 0x01, (byte) 0x80, (byte) 0xC0, (byte) 0xA3, 0x74, 0x77, 0x6F, (byte) 0x91, 0x2B, (byte) 0x90};
|
||||
ByteBuffer message = ByteBuffer.wrap(messageBytes);
|
||||
TestBinder binder = new TestBinder(new Type[] { int.class }, null);
|
||||
|
||||
List<HubMessage> messages = messagePackHubProtocol.parseMessages(message, binder);
|
||||
|
||||
assertNotNull(messages);
|
||||
assertEquals(2, messages.size());
|
||||
|
||||
// Check the first message
|
||||
assertEquals(HubMessageType.INVOCATION, messages.get(0).getMessageType());
|
||||
|
||||
//Now that we know we have an invocation message we can cast the hubMessage.
|
||||
InvocationMessage invocationMessage = (InvocationMessage) messages.get(0);
|
||||
|
||||
assertEquals("one", invocationMessage.getTarget());
|
||||
assertEquals(null, invocationMessage.getInvocationId());
|
||||
assertEquals(null, invocationMessage.getHeaders());
|
||||
assertEquals(null, invocationMessage.getStreamIds());
|
||||
|
||||
int messageResult = (int)invocationMessage.getArguments()[0];
|
||||
assertEquals(42, messageResult);
|
||||
|
||||
// Check the second message
|
||||
assertEquals(HubMessageType.INVOCATION, messages.get(1).getMessageType());
|
||||
|
||||
//Now that we know we have an invocation message we can cast the hubMessage.
|
||||
InvocationMessage invocationMessage2 = (InvocationMessage) messages.get(1);
|
||||
|
||||
assertEquals("two", invocationMessage2.getTarget());
|
||||
assertEquals(null, invocationMessage2.getInvocationId());
|
||||
assertEquals(null, invocationMessage2.getHeaders());
|
||||
assertEquals(null, invocationMessage2.getStreamIds());
|
||||
|
||||
int secondMessageResult = (int)invocationMessage2.getArguments()[0];
|
||||
assertEquals(43, secondMessageResult);
|
||||
}
|
||||
|
||||
@Test
|
||||
public void parseSingleMessageMutipleArgs() {
|
||||
byte[] messageBytes = {0x0F, (byte) 0x96, 0x01, (byte) 0x80, (byte) 0xC0, (byte) 0xA4, 0x74, 0x65, 0x73, 0x74, (byte) 0x92,
|
||||
0x2A, (byte) 0xA2, 0x34, 0x32, (byte) 0x90};
|
||||
ByteBuffer message = ByteBuffer.wrap(messageBytes);
|
||||
TestBinder binder = new TestBinder(new Type[] { int.class, String.class }, null);
|
||||
|
||||
List<HubMessage> messages = messagePackHubProtocol.parseMessages(message, binder);
|
||||
|
||||
assertNotNull(messages);
|
||||
assertEquals(1, messages.size());
|
||||
|
||||
//We know it's only one message
|
||||
assertEquals(HubMessageType.INVOCATION, messages.get(0).getMessageType());
|
||||
|
||||
InvocationMessage invocationMessage = (InvocationMessage)messages.get(0);
|
||||
assertEquals("test", invocationMessage.getTarget());
|
||||
assertEquals(null, invocationMessage.getInvocationId());
|
||||
int messageResult = (int) invocationMessage.getArguments()[0];
|
||||
String messageResult2 = (String) invocationMessage.getArguments()[1];
|
||||
assertEquals(42, messageResult);
|
||||
assertEquals("42", messageResult2);
|
||||
}
|
||||
|
||||
@Test
|
||||
public void invocationBindingFailureWhileParsingTooManyArguments() {
|
||||
byte[] messageBytes = {0x0F, (byte) 0x96, 0x01, (byte) 0x80, (byte) 0xC0, (byte) 0xA4, 0x74, 0x65, 0x73, 0x74, (byte) 0x92,
|
||||
0x2A, (byte) 0xA2, 0x34, 0x32, (byte) 0x90};
|
||||
ByteBuffer message = ByteBuffer.wrap(messageBytes);
|
||||
TestBinder binder = new TestBinder(new Type[] { int.class }, null);
|
||||
|
||||
List<HubMessage> messages = messagePackHubProtocol.parseMessages(message, binder);
|
||||
|
||||
assertNotNull(messages);
|
||||
assertEquals(1, messages.size());
|
||||
|
||||
assertEquals(HubMessageType.INVOCATION_BINDING_FAILURE, messages.get(0).getMessageType());
|
||||
InvocationBindingFailureMessage invocationBindingFailureMessage = (InvocationBindingFailureMessage)messages.get(0);
|
||||
assertEquals("Invocation provides 2 argument(s) but target expects 1.", invocationBindingFailureMessage.getException().getMessage());
|
||||
}
|
||||
|
||||
@Test
|
||||
public void invocationBindingFailureWhileParsingTooFewArguments() {
|
||||
byte[] messageBytes = {0x0C, (byte) 0x96, 0x01, (byte) 0x80, (byte) 0xC0, (byte) 0xA4, 0x74, 0x65, 0x73, 0x74, (byte) 0x91, 0x2A,
|
||||
(byte) 0x90};
|
||||
ByteBuffer message = ByteBuffer.wrap(messageBytes);
|
||||
TestBinder binder = new TestBinder(new Type[] { int.class, int.class }, null);
|
||||
|
||||
List<HubMessage> messages = messagePackHubProtocol.parseMessages(message, binder);
|
||||
|
||||
assertNotNull(messages);
|
||||
assertEquals(1, messages.size());
|
||||
|
||||
assertEquals(HubMessageType.INVOCATION_BINDING_FAILURE, messages.get(0).getMessageType());
|
||||
InvocationBindingFailureMessage invocationBindingFailureMessage = (InvocationBindingFailureMessage) messages.get(0);
|
||||
assertEquals("Invocation provides 1 argument(s) but target expects 2.", invocationBindingFailureMessage.getException().getMessage());
|
||||
}
|
||||
|
||||
@Test
|
||||
public void invocationBindingFailureWhenParsingIncorrectType() {
|
||||
byte[] messageBytes = {0x0C, (byte) 0x96, 0x01, (byte) 0x80, (byte) 0xC0, (byte) 0xA4, 0x74, 0x65, 0x73, 0x74, (byte) 0x91,
|
||||
(byte) 0xC3, (byte) 0x90};
|
||||
ByteBuffer message = ByteBuffer.wrap(messageBytes);
|
||||
TestBinder binder = new TestBinder(new Type[] { int.class }, null);
|
||||
|
||||
List<HubMessage> messages = messagePackHubProtocol.parseMessages(message, binder);
|
||||
|
||||
assertNotNull(messages);
|
||||
assertEquals(1, messages.size());
|
||||
|
||||
assertEquals(HubMessageType.INVOCATION_BINDING_FAILURE, messages.get(0).getMessageType());
|
||||
InvocationBindingFailureMessage invocationBindingFailureMessage = (InvocationBindingFailureMessage) messages.get(0);
|
||||
// We get different exception messages on different platforms, so use a regex
|
||||
assertTrue(invocationBindingFailureMessage.getException().getMessage().matches("^.*Boolean.*cannot be cast to.*Integer.*"));
|
||||
}
|
||||
|
||||
@Test
|
||||
public void invocationBindingFailureReadsNextMessageAfterTooManyArguments() {
|
||||
byte[] messageBytes = {0x0F, (byte) 0x96, 0x01, (byte) 0x80, (byte) 0xC0, (byte) 0xA4, 0x74, 0x65, 0x73, 0x74, (byte) 0x92,
|
||||
0x2A, (byte) 0xA2, 0x34, 0x32, (byte) 0x90, 0x0B, (byte) 0x96, 0x01, (byte) 0x80, (byte) 0xC0, (byte) 0xA3, 0x74,
|
||||
0x77, 0x6F, (byte) 0x91, 0x2B, (byte) 0x90};
|
||||
ByteBuffer message = ByteBuffer.wrap(messageBytes);
|
||||
TestBinder binder = new TestBinder(new Type[] { int.class }, null);
|
||||
|
||||
List<HubMessage> messages = messagePackHubProtocol.parseMessages(message, binder);
|
||||
|
||||
assertNotNull(messages);
|
||||
assertEquals(2, messages.size());
|
||||
|
||||
assertEquals(HubMessageType.INVOCATION_BINDING_FAILURE, messages.get(0).getMessageType());
|
||||
InvocationBindingFailureMessage invocationBindingFailureMessage = (InvocationBindingFailureMessage) messages.get(0);
|
||||
assertEquals("Invocation provides 2 argument(s) but target expects 1.", invocationBindingFailureMessage.getException().getMessage());
|
||||
|
||||
// Check the second message
|
||||
assertEquals(HubMessageType.INVOCATION, messages.get(1).getMessageType());
|
||||
|
||||
//Now that we know we have an invocation message we can cast the hubMessage.
|
||||
InvocationMessage invocationMessage2 = (InvocationMessage) messages.get(1);
|
||||
|
||||
assertEquals("two", invocationMessage2.getTarget());
|
||||
assertEquals(null, invocationMessage2.getInvocationId());
|
||||
assertEquals(null, invocationMessage2.getHeaders());
|
||||
assertEquals(null, invocationMessage2.getStreamIds());
|
||||
|
||||
int secondMessageResult = (int)invocationMessage2.getArguments()[0];
|
||||
assertEquals(43, secondMessageResult);
|
||||
}
|
||||
|
||||
@Test
|
||||
public void invocationBindingFailureReadsNextMessageAfterTooFewArguments() {
|
||||
byte[] messageBytes = {0x0C, (byte) 0x96, 0x01, (byte) 0x80, (byte) 0xC0, (byte) 0xA4, 0x74, 0x65, 0x73, 0x74, (byte) 0x91, 0x2A,
|
||||
(byte) 0x90, 0x0C, (byte) 0x96, 0x01, (byte) 0x80, (byte) 0xC0, (byte) 0xA3, 0x74, 0x77, 0x6F, (byte) 0x92, 0x2A, 0x2B, (byte) 0x90};
|
||||
ByteBuffer message = ByteBuffer.wrap(messageBytes);
|
||||
TestBinder binder = new TestBinder(new Type[] { int.class, int.class }, null);
|
||||
|
||||
List<HubMessage> messages = messagePackHubProtocol.parseMessages(message, binder);
|
||||
|
||||
assertNotNull(messages);
|
||||
assertEquals(2, messages.size());
|
||||
|
||||
assertEquals(HubMessageType.INVOCATION_BINDING_FAILURE, messages.get(0).getMessageType());
|
||||
InvocationBindingFailureMessage invocationBindingFailureMessage = (InvocationBindingFailureMessage) messages.get(0);
|
||||
assertEquals("Invocation provides 1 argument(s) but target expects 2.", invocationBindingFailureMessage.getException().getMessage());
|
||||
|
||||
// Check the second message
|
||||
assertEquals(HubMessageType.INVOCATION, messages.get(1).getMessageType());
|
||||
|
||||
//Now that we know we have an invocation message we can cast the hubMessage.
|
||||
InvocationMessage invocationMessage2 = (InvocationMessage) messages.get(1);
|
||||
|
||||
assertEquals("two", invocationMessage2.getTarget());
|
||||
assertEquals(null, invocationMessage2.getInvocationId());
|
||||
assertEquals(null, invocationMessage2.getHeaders());
|
||||
assertEquals(null, invocationMessage2.getStreamIds());
|
||||
|
||||
int secondMessageResult1 = (int)invocationMessage2.getArguments()[0];
|
||||
int secondMessageResult2 = (int)invocationMessage2.getArguments()[1];
|
||||
assertEquals(42, secondMessageResult1);
|
||||
assertEquals(43, secondMessageResult2);
|
||||
}
|
||||
|
||||
@Test
|
||||
public void invocationBindingFailureReadsNextMessageAfterIncorrectArgument() {
|
||||
byte[] messageBytes = {0x0C, (byte) 0x96, 0x01, (byte) 0x80, (byte) 0xC0, (byte) 0xA4, 0x74, 0x65, 0x73, 0x74, (byte) 0x91,
|
||||
(byte) 0xC3, (byte) 0x90, 0x0C, (byte) 0x96, 0x01, (byte) 0x80, (byte) 0xC0, (byte) 0xA4, 0x74, 0x65, 0x73, 0x74,
|
||||
(byte) 0x91, 0x2A, (byte) 0x90};
|
||||
ByteBuffer message = ByteBuffer.wrap(messageBytes);
|
||||
TestBinder binder = new TestBinder(new Type[] { int.class }, null);
|
||||
|
||||
List<HubMessage> messages = messagePackHubProtocol.parseMessages(message, binder);
|
||||
|
||||
assertNotNull(messages);
|
||||
assertEquals(2, messages.size());
|
||||
|
||||
assertEquals(HubMessageType.INVOCATION_BINDING_FAILURE, messages.get(0).getMessageType());
|
||||
InvocationBindingFailureMessage invocationBindingFailureMessage = (InvocationBindingFailureMessage) messages.get(0);
|
||||
// We get different exception messages on different platforms, so use a regex
|
||||
assertTrue(invocationBindingFailureMessage.getException().getMessage().matches("^.*Boolean.*cannot be cast to.*Integer.*"));
|
||||
|
||||
// Check the second message
|
||||
assertEquals(HubMessageType.INVOCATION, messages.get(1).getMessageType());
|
||||
|
||||
//Now that we know we have an invocation message we can cast the hubMessage.
|
||||
InvocationMessage invocationMessage2 = (InvocationMessage) messages.get(1);
|
||||
|
||||
assertEquals("test", invocationMessage2.getTarget());
|
||||
assertEquals(null, invocationMessage2.getInvocationId());
|
||||
assertEquals(null, invocationMessage2.getHeaders());
|
||||
assertEquals(null, invocationMessage2.getStreamIds());
|
||||
|
||||
int secondMessageResult = (int)invocationMessage2.getArguments()[0];
|
||||
assertEquals(42, secondMessageResult);
|
||||
}
|
||||
|
||||
@Test
|
||||
public void errorWhenLengthHeaderTooLong() {
|
||||
byte[] messageBytes = {0x0D, (byte) 0x96, 0x01, (byte) 0x80, (byte) 0xC0, (byte) 0xA4, 0x74, 0x65, 0x73, 0x74, (byte) 0x91,
|
||||
0x2A, (byte) 0x90};
|
||||
ByteBuffer message = ByteBuffer.wrap(messageBytes);
|
||||
TestBinder binder = new TestBinder(new Type[] { int.class }, null);
|
||||
|
||||
RuntimeException exception = assertThrows(RuntimeException.class,
|
||||
() -> messagePackHubProtocol.parseMessages(message, binder));
|
||||
assertEquals("MessagePack message was length 12 but claimed to be length 13.", exception.getMessage());
|
||||
}
|
||||
|
||||
@Test
|
||||
public void errorWhenLengthHeaderTooShort() {
|
||||
byte[] messageBytes = {0x0B, (byte) 0x96, 0x01, (byte) 0x80, (byte) 0xC0, (byte) 0xA4, 0x74, 0x65, 0x73, 0x74, (byte) 0x91,
|
||||
0x2A, (byte) 0x90};
|
||||
ByteBuffer message = ByteBuffer.wrap(messageBytes);
|
||||
TestBinder binder = new TestBinder(new Type[] { int.class }, null);
|
||||
|
||||
RuntimeException exception = assertThrows(RuntimeException.class,
|
||||
() -> messagePackHubProtocol.parseMessages(message, binder));
|
||||
assertEquals("MessagePack message was length 12 but claimed to be length 11.", exception.getMessage());
|
||||
}
|
||||
|
||||
@Test
|
||||
public void parseMessageWithTwoByteLengthHeader() {
|
||||
// Test that a long message w/ a 2-byte length header is still parsed correctly
|
||||
byte[] messageBytes = {(byte) 0x87, 0x01, (byte) 0x96, 0x01, (byte) 0x80, (byte) 0xC0, (byte) 0xA4, 0x74, 0x65, 0x73, 0x74,
|
||||
(byte) 0x91, (byte) 0xD9, 0x7A, 0x54, 0x68, 0x69, 0x73, 0x20, 0x69, 0x73, 0x20, 0x61, 0x20, 0x72, 0x65, 0x61, 0x6C, 0x6C,
|
||||
0x79, 0x20, 0x6C, 0x6F, 0x6E, 0x67, 0x20, 0x61, 0x72, 0x67, 0x75, 0x6D, 0x65, 0x6E, 0x74, 0x20, 0x74, 0x6F, 0x20, 0x6D,
|
||||
0x61, 0x6B, 0x65, 0x20, 0x74, 0x68, 0x65, 0x20, 0x6C, 0x65, 0x6E, 0x67, 0x74, 0x68, 0x20, 0x6F, 0x66, 0x20, 0x74, 0x68,
|
||||
0x69, 0x73, 0x20, 0x6D, 0x65, 0x73, 0x73, 0x61, 0x67, 0x65, 0x20, 0x6D, 0x6F, 0x72, 0x65, 0x20, 0x74, 0x68, 0x61, 0x6E,
|
||||
0x20, 0x31, 0x32, 0x37, 0x20, 0x62, 0x79, 0x74, 0x65, 0x73, 0x2E, 0x20, 0x57, 0x65, 0x20, 0x6A, 0x75, 0x73, 0x74, 0x20,
|
||||
0x6E, 0x65, 0x65, 0x64, 0x20, 0x61, 0x20, 0x66, 0x65, 0x77, 0x20, 0x6D, 0x6F, 0x72, 0x65, 0x20, 0x63, 0x68, 0x61, 0x72,
|
||||
0x61, 0x63, 0x74, 0x65, 0x72, 0x73, 0x2E, (byte) 0x90};
|
||||
ByteBuffer message = ByteBuffer.wrap(messageBytes);
|
||||
TestBinder binder = new TestBinder(new Type[] { String.class }, null);
|
||||
|
||||
List<HubMessage> messages = messagePackHubProtocol.parseMessages(message, binder);
|
||||
|
||||
//We know it's only one message
|
||||
assertNotNull(messages);
|
||||
assertEquals(1, messages.size());
|
||||
|
||||
assertEquals(HubMessageType.INVOCATION, messages.get(0).getMessageType());
|
||||
|
||||
//We can safely cast here because we know that it's an invocation message.
|
||||
InvocationMessage invocationMessage = (InvocationMessage) messages.get(0);
|
||||
|
||||
assertEquals("test", invocationMessage.getTarget());
|
||||
assertEquals(null, invocationMessage.getInvocationId());
|
||||
assertEquals(null, invocationMessage.getHeaders());
|
||||
assertEquals(null, invocationMessage.getStreamIds());
|
||||
|
||||
String messageResult = (String)invocationMessage.getArguments()[0];
|
||||
assertEquals("This is a really long argument to make the length of this message more than "
|
||||
+ "127 bytes. We just need a few more characters.", messageResult);
|
||||
}
|
||||
|
||||
@Test
|
||||
public void verifyWriteInvocationMessageWithTwoByteLengthHeader() {
|
||||
InvocationMessage invocationMessage = new InvocationMessage(null, null, "test", new Object[] { "This is a really long argument to make "
|
||||
+ "the length of this message more than 127 bytes. We just need a few more characters." }, null);
|
||||
ByteBuffer result = messagePackHubProtocol.writeMessage(invocationMessage);
|
||||
byte[] expectedBytes = {(byte) 0x87, 0x01, (byte) 0x96, 0x01, (byte) 0x80, (byte) 0xC0, (byte) 0xA4, 0x74, 0x65, 0x73, 0x74,
|
||||
(byte) 0x91, (byte) 0xD9, 0x7A, 0x54, 0x68, 0x69, 0x73, 0x20, 0x69, 0x73, 0x20, 0x61, 0x20, 0x72, 0x65, 0x61, 0x6C, 0x6C,
|
||||
0x79, 0x20, 0x6C, 0x6F, 0x6E, 0x67, 0x20, 0x61, 0x72, 0x67, 0x75, 0x6D, 0x65, 0x6E, 0x74, 0x20, 0x74, 0x6F, 0x20, 0x6D,
|
||||
0x61, 0x6B, 0x65, 0x20, 0x74, 0x68, 0x65, 0x20, 0x6C, 0x65, 0x6E, 0x67, 0x74, 0x68, 0x20, 0x6F, 0x66, 0x20, 0x74, 0x68,
|
||||
0x69, 0x73, 0x20, 0x6D, 0x65, 0x73, 0x73, 0x61, 0x67, 0x65, 0x20, 0x6D, 0x6F, 0x72, 0x65, 0x20, 0x74, 0x68, 0x61, 0x6E,
|
||||
0x20, 0x31, 0x32, 0x37, 0x20, 0x62, 0x79, 0x74, 0x65, 0x73, 0x2E, 0x20, 0x57, 0x65, 0x20, 0x6A, 0x75, 0x73, 0x74, 0x20,
|
||||
0x6E, 0x65, 0x65, 0x64, 0x20, 0x61, 0x20, 0x66, 0x65, 0x77, 0x20, 0x6D, 0x6F, 0x72, 0x65, 0x20, 0x63, 0x68, 0x61, 0x72,
|
||||
0x61, 0x63, 0x74, 0x65, 0x72, 0x73, 0x2E, (byte) 0x90};
|
||||
ByteString expectedResult = ByteString.of(expectedBytes);
|
||||
assertEquals(expectedResult, ByteString.of(result));
|
||||
}
|
||||
|
||||
@Test
|
||||
public void parseInvocationMessageWithPrimitiveArgs() {
|
||||
byte[] messageBytes = {0x1E, (byte) 0x96, 0x01, (byte) 0x80, (byte) 0xC0, (byte) 0xA4, 0x74, 0x65, 0x73, 0x74, (byte) 0x96, 0x01, (byte) 0xCB,
|
||||
0x40, 0x04, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, (byte) 0xC3, 0x11, (byte) 0xA1, 0x63, (byte) 0xCE, (byte) 0xC6, (byte) 0xAE, (byte) 0xA1,
|
||||
0x55, (byte) 0x90};
|
||||
ByteBuffer message = ByteBuffer.wrap(messageBytes);
|
||||
int i = 1;
|
||||
double d = 2.5d;
|
||||
boolean bool = true;
|
||||
byte bite = 0x11;
|
||||
char c = 'c';
|
||||
long l = 3333333333l;
|
||||
TestBinder binder = new TestBinder(new Type[] { int.class, double.class, boolean.class, byte.class, char.class, long.class }, null);
|
||||
|
||||
List<HubMessage> messages = messagePackHubProtocol.parseMessages(message, binder);
|
||||
|
||||
//We know it's only one message
|
||||
assertNotNull(messages);
|
||||
assertEquals(1, messages.size());
|
||||
|
||||
assertEquals(HubMessageType.INVOCATION, messages.get(0).getMessageType());
|
||||
|
||||
//We can safely cast here because we know that it's an invocation message.
|
||||
InvocationMessage invocationMessage = (InvocationMessage) messages.get(0);
|
||||
|
||||
assertEquals("test", invocationMessage.getTarget());
|
||||
assertEquals(null, invocationMessage.getInvocationId());
|
||||
assertEquals(null, invocationMessage.getHeaders());
|
||||
assertEquals(null, invocationMessage.getStreamIds());
|
||||
|
||||
Object[] args = invocationMessage.getArguments();
|
||||
assertEquals(6, args.length);
|
||||
assertEquals(i, (int)args[0]);
|
||||
assertEquals(d, (double)args[1]);
|
||||
assertEquals(bool, (boolean)args[2]);
|
||||
assertEquals(bite, (byte)args[3]);
|
||||
assertEquals(c, (char)args[4]);
|
||||
assertEquals(l, (long)args[5]);
|
||||
}
|
||||
|
||||
@Test
|
||||
public void verifyWriteInvocationMessageWithPrimitiveArgs() {
|
||||
int i = 1;
|
||||
double d = 2.5d;
|
||||
boolean bool = true;
|
||||
byte bite = 0x11;
|
||||
char c = 'c';
|
||||
long l = 3333333333l;
|
||||
InvocationMessage invocationMessage = new InvocationMessage(null, null, "test", new Object[] { i, d, bool, bite, c, l }, null);
|
||||
ByteBuffer result = messagePackHubProtocol.writeMessage(invocationMessage);
|
||||
byte[] expectedBytes = {0x1E, (byte) 0x96, 0x01, (byte) 0x80, (byte) 0xC0, (byte) 0xA4, 0x74, 0x65, 0x73, 0x74, (byte) 0x96, 0x01,
|
||||
(byte) 0xCB, 0x40, 0x04, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, (byte) 0xC3, 0x11, (byte) 0xA1, 0x63, (byte) 0xCE, (byte) 0xC6, (byte) 0xAE,
|
||||
(byte) 0xA1, 0x55, (byte) 0x90};
|
||||
ByteString expectedResult = ByteString.of(expectedBytes);
|
||||
assertEquals(expectedResult, ByteString.of(result));
|
||||
}
|
||||
|
||||
@Test
|
||||
public void parseInvocationMessageWithArrayArg() {
|
||||
// Make sure that the same bytes can be parsed as both an Array and a List
|
||||
byte[] messageBytes = {0x10, (byte) 0x96, 0x01, (byte) 0x80, (byte) 0xC0, (byte) 0xA4, 0x74, 0x65, 0x73, 0x74, (byte) 0x91, (byte) 0x94, 0x01,
|
||||
0x02, 0x03, 0x04, (byte) 0x90};
|
||||
ByteBuffer message = ByteBuffer.wrap(messageBytes);
|
||||
|
||||
TestBinder arrayBinder = new TestBinder(new Type[] { int[].class }, null);
|
||||
TestBinder listBinder = new TestBinder(new Type[] { (new TypeReference<ArrayList<Integer>>() { }).getType() }, null);
|
||||
|
||||
List<HubMessage> arrayMessages = messagePackHubProtocol.parseMessages(message, arrayBinder);
|
||||
message.flip();
|
||||
List<HubMessage> listMessages = messagePackHubProtocol.parseMessages(message, listBinder);
|
||||
|
||||
//We know it's only one message
|
||||
assertNotNull(arrayMessages);
|
||||
assertEquals(1, arrayMessages.size());
|
||||
|
||||
assertNotNull(listMessages);
|
||||
assertEquals(1, listMessages.size());
|
||||
|
||||
assertEquals(HubMessageType.INVOCATION, arrayMessages.get(0).getMessageType());
|
||||
assertEquals(HubMessageType.INVOCATION, listMessages.get(0).getMessageType());
|
||||
|
||||
//We can safely cast here because we know that it's an invocation message.
|
||||
InvocationMessage arrayInvocationMessage = (InvocationMessage) arrayMessages.get(0);
|
||||
InvocationMessage listInvocationMessage = (InvocationMessage) listMessages.get(0);
|
||||
|
||||
assertEquals("test", arrayInvocationMessage.getTarget());
|
||||
assertEquals(null, arrayInvocationMessage.getInvocationId());
|
||||
assertEquals(null, arrayInvocationMessage.getHeaders());
|
||||
assertEquals(null, arrayInvocationMessage.getStreamIds());
|
||||
|
||||
assertEquals("test", listInvocationMessage.getTarget());
|
||||
assertEquals(null, listInvocationMessage.getInvocationId());
|
||||
assertEquals(null, listInvocationMessage.getHeaders());
|
||||
assertEquals(null, listInvocationMessage.getStreamIds());
|
||||
|
||||
int[] arrayArg = (int[])arrayInvocationMessage.getArguments()[0];
|
||||
@SuppressWarnings("unchecked")
|
||||
List<Integer> listArg = (ArrayList<Integer>)listInvocationMessage.getArguments()[0];
|
||||
|
||||
assertEquals(4, arrayArg.length);
|
||||
assertEquals(4, listArg.size());
|
||||
for (int i = 0; i < arrayArg.length; i++) {
|
||||
assertEquals(i + 1, arrayArg[i]);
|
||||
assertEquals(i + 1, (int) listArg.get(i));
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
public void verifyWriteInvocationMessageWithArrayArg() {
|
||||
InvocationMessage invocationMessage = new InvocationMessage(null, null, "test", new Object[] { new int[] { 1, 2, 3, 4 } }, null);
|
||||
ByteBuffer result = messagePackHubProtocol.writeMessage(invocationMessage);
|
||||
byte[] expectedBytes = {0x10, (byte) 0x96, 0x01, (byte) 0x80, (byte) 0xC0, (byte) 0xA4, 0x74, 0x65, 0x73, 0x74, (byte) 0x91, (byte) 0x94, 0x01,
|
||||
0x02, 0x03, 0x04, (byte) 0x90};
|
||||
ByteString expectedResult = ByteString.of(expectedBytes);
|
||||
assertEquals(expectedResult, ByteString.of(result));
|
||||
}
|
||||
|
||||
@Test
|
||||
public void parseInvocationMessageWithMapArg() {
|
||||
byte[] messageBytes = {0x23, (byte) 0x96, 0x01, (byte) 0x80, (byte) 0xC0, (byte) 0xA4, 0x74, 0x65, 0x73, 0x74, (byte) 0x91, (byte) 0x82, (byte) 0xA5,
|
||||
0x61, 0x70, 0x70, 0x6C, 0x65, (byte) 0xA6, 0x62, 0x61, 0x6E, 0x61, 0x6E, 0x61, (byte) 0xA3, 0x6B, 0x65, 0x79, (byte) 0xA5, 0x76, 0x61, 0x6C, 0x75,
|
||||
0x65, (byte) 0x90};
|
||||
ByteBuffer message = ByteBuffer.wrap(messageBytes);
|
||||
|
||||
TestBinder binder = new TestBinder(new Type[] { (new TypeReference<HashMap<String, String>>() { }).getType() }, null);
|
||||
|
||||
List<HubMessage> messages = messagePackHubProtocol.parseMessages(message, binder);
|
||||
|
||||
//We know it's only one message
|
||||
assertNotNull(messages);
|
||||
assertEquals(1, messages.size());
|
||||
|
||||
assertEquals(HubMessageType.INVOCATION, messages.get(0).getMessageType());
|
||||
|
||||
//We can safely cast here because we know that it's an invocation message.
|
||||
InvocationMessage invocationMessage = (InvocationMessage) messages.get(0);
|
||||
|
||||
assertEquals("test", invocationMessage.getTarget());
|
||||
assertEquals(null, invocationMessage.getInvocationId());
|
||||
assertEquals(null, invocationMessage.getHeaders());
|
||||
assertEquals(null, invocationMessage.getStreamIds());
|
||||
|
||||
@SuppressWarnings("unchecked")
|
||||
Map<String, String> result = (HashMap<String, String>)invocationMessage.getArguments()[0];
|
||||
assertEquals(2, result.size());
|
||||
assertEquals("value", result.get("key"));
|
||||
assertEquals("banana", result.get("apple"));
|
||||
}
|
||||
|
||||
@Test
|
||||
public void verifyWriteInvocationMessageWithMapArg() {
|
||||
SortedMap<String, String> argument = new TreeMap<String, String>();
|
||||
argument.put("apple", "banana");
|
||||
argument.put("key", "value");
|
||||
InvocationMessage invocationMessage = new InvocationMessage(null, null, "test", new Object[] { argument }, null);
|
||||
ByteBuffer result = messagePackHubProtocol.writeMessage(invocationMessage);
|
||||
byte[] expectedBytes = {0x23, (byte) 0x96, 0x01, (byte) 0x80, (byte) 0xC0, (byte) 0xA4, 0x74, 0x65, 0x73, 0x74, (byte) 0x91, (byte) 0x82, (byte) 0xA5,
|
||||
0x61, 0x70, 0x70, 0x6C, 0x65, (byte) 0xA6, 0x62, 0x61, 0x6E, 0x61, 0x6E, 0x61, (byte) 0xA3, 0x6B, 0x65, 0x79, (byte) 0xA5, 0x76, 0x61, 0x6C, 0x75,
|
||||
0x65, (byte) 0x90};
|
||||
ByteString expectedResult = ByteString.of(expectedBytes);
|
||||
assertEquals(expectedResult, ByteString.of(result));
|
||||
}
|
||||
|
||||
@Test
|
||||
public void parseInvocationMessageWithNestedCollection() {
|
||||
byte[] messageBytes = {0x39, (byte) 0x96, 0x01, (byte) 0x80, (byte) 0xC0, (byte) 0xA4, 0x74, 0x65, 0x73, 0x74, (byte) 0x91, (byte) 0x92,
|
||||
(byte) 0x82, (byte) 0xA3, 0x6F, 0x6E, 0x65, (byte) 0x92, (byte) 0xA1, 0x61, (byte) 0xA1, 0x62, (byte) 0xA3, 0x74, 0x77, 0x6F, (byte) 0x92,
|
||||
(byte) 0xA3, (byte) 0xEB, (byte) 0xBB, (byte) 0xAF, (byte) 0xA3, (byte) 0xEA, (byte) 0xAF, (byte) 0x8D, (byte) 0x82, (byte) 0xA4, 0x66,
|
||||
0x6F, 0x75, 0x72, (byte) 0x92, (byte) 0xA1, 0x5E, (byte) 0xA1, 0x2A, (byte) 0xA5, 0x74, 0x68, 0x72, 0x65, 0x65, (byte) 0x92, (byte) 0xA1,
|
||||
0x35, (byte) 0xA1, 0x39, (byte) 0x90};
|
||||
ByteBuffer message = ByteBuffer.wrap(messageBytes);
|
||||
|
||||
TestBinder binder = new TestBinder(new Type[] { (new TypeReference<ArrayList<HashMap<String, ArrayList<Character>>>>() { }).getType() }, null);
|
||||
|
||||
List<HubMessage> messages = messagePackHubProtocol.parseMessages(message, binder);
|
||||
|
||||
//We know it's only one message
|
||||
assertNotNull(messages);
|
||||
assertEquals(1, messages.size());
|
||||
|
||||
assertEquals(HubMessageType.INVOCATION, messages.get(0).getMessageType());
|
||||
|
||||
//We can safely cast here because we know that it's an invocation message.
|
||||
InvocationMessage invocationMessage = (InvocationMessage) messages.get(0);
|
||||
|
||||
assertEquals("test", invocationMessage.getTarget());
|
||||
assertEquals(null, invocationMessage.getInvocationId());
|
||||
assertEquals(null, invocationMessage.getHeaders());
|
||||
assertEquals(null, invocationMessage.getStreamIds());
|
||||
|
||||
@SuppressWarnings("unchecked")
|
||||
ArrayList<HashMap<String, ArrayList<Character>>> result = (ArrayList<HashMap<String, ArrayList<Character>>>)invocationMessage.getArguments()[0];
|
||||
assertEquals(2, result.size());
|
||||
|
||||
HashMap<String, ArrayList<Character>> firstMap = result.get(0);
|
||||
HashMap<String, ArrayList<Character>> secondMap = result.get(1);
|
||||
|
||||
assertEquals(2, firstMap.keySet().size());
|
||||
assertEquals(2, secondMap.keySet().size());
|
||||
|
||||
ArrayList<Character> firstList = firstMap.get("one");
|
||||
ArrayList<Character> secondList = firstMap.get("two");
|
||||
|
||||
ArrayList<Character> thirdList = secondMap.get("three");
|
||||
ArrayList<Character> fourthList = secondMap.get("four");
|
||||
|
||||
assertEquals(2, firstList.size());
|
||||
assertEquals(2, secondList.size());
|
||||
assertEquals(2, thirdList.size());
|
||||
assertEquals(2, fourthList.size());
|
||||
|
||||
assertEquals('a', (char) firstList.get(0));
|
||||
assertEquals('b', (char) firstList.get(1));
|
||||
|
||||
assertEquals('\ubeef', (char) secondList.get(0));
|
||||
assertEquals('\uabcd', (char) secondList.get(1));
|
||||
|
||||
assertEquals('5', (char) thirdList.get(0));
|
||||
assertEquals('9', (char) thirdList.get(1));
|
||||
|
||||
assertEquals('^', (char) fourthList.get(0));
|
||||
assertEquals('*', (char) fourthList.get(1));
|
||||
}
|
||||
|
||||
@Test
|
||||
public void verifyWriteInvocationMessageWithNestedCollection() {
|
||||
ArrayList<Character> clist1 = new ArrayList<Character>();
|
||||
ArrayList<Character> clist2 = new ArrayList<Character>();
|
||||
ArrayList<Character> clist3 = new ArrayList<Character>();
|
||||
ArrayList<Character> clist4 = new ArrayList<Character>();
|
||||
|
||||
clist1.add('a');
|
||||
clist1.add('b');
|
||||
|
||||
clist2.add('\ubeef');
|
||||
clist2.add('\uabcd');
|
||||
|
||||
clist3.add('5');
|
||||
clist3.add('9');
|
||||
|
||||
clist4.add('^');
|
||||
clist4.add('*');
|
||||
|
||||
TreeMap<String, ArrayList<Character>> map1 = new TreeMap<String, ArrayList<Character>>();
|
||||
TreeMap<String, ArrayList<Character>> map2 = new TreeMap<String, ArrayList<Character>>();
|
||||
|
||||
map1.put("one", clist1);
|
||||
map1.put("two", clist2);
|
||||
|
||||
map2.put("three", clist3);
|
||||
map2.put("four", clist4);
|
||||
|
||||
ArrayList<TreeMap<String, ArrayList<Character>>> argument = new ArrayList<TreeMap<String, ArrayList<Character>>>();
|
||||
argument.add(map1);
|
||||
argument.add(map2);
|
||||
InvocationMessage invocationMessage = new InvocationMessage(null, null, "test", new Object[] { argument }, null);
|
||||
ByteBuffer result = messagePackHubProtocol.writeMessage(invocationMessage);
|
||||
byte[] expectedBytes = {0x39, (byte) 0x96, 0x01, (byte) 0x80, (byte) 0xC0, (byte) 0xA4, 0x74, 0x65, 0x73, 0x74, (byte) 0x91, (byte) 0x92,
|
||||
(byte) 0x82, (byte) 0xA3, 0x6F, 0x6E, 0x65, (byte) 0x92, (byte) 0xA1, 0x61, (byte) 0xA1, 0x62, (byte) 0xA3, 0x74, 0x77, 0x6F, (byte) 0x92,
|
||||
(byte) 0xA3, (byte) 0xEB, (byte) 0xBB, (byte) 0xAF, (byte) 0xA3, (byte) 0xEA, (byte) 0xAF, (byte) 0x8D, (byte) 0x82, (byte) 0xA4, 0x66,
|
||||
0x6F, 0x75, 0x72, (byte) 0x92, (byte) 0xA1, 0x5E, (byte) 0xA1, 0x2A, (byte) 0xA5, 0x74, 0x68, 0x72, 0x65, 0x65, (byte) 0x92, (byte) 0xA1,
|
||||
0x35, (byte) 0xA1, 0x39, (byte) 0x90};
|
||||
ByteString expectedResult = ByteString.of(expectedBytes);
|
||||
assertEquals(expectedResult, ByteString.of(result));
|
||||
}
|
||||
|
||||
@Test
|
||||
public void parseInvocationMessageWithCustomPojoArg() {
|
||||
byte[] messageBytes = {0x32, (byte) 0x96, 0x01, (byte) 0x80, (byte) 0xC0, (byte) 0xA4, 0x74, 0x65, 0x73, 0x74, (byte) 0x91, (byte) 0x84, (byte) 0xA9,
|
||||
0x66, 0x69, 0x72, 0x73, 0x74, 0x4E, 0x61, 0x6D, 0x65, (byte) 0xA4, 0x4A, 0x6F, 0x68, 0x6E, (byte) 0xA8, 0x6C, 0x61, 0x73, 0x74, 0x4E, 0x61,
|
||||
0x6D, 0x65, (byte) 0xA3, 0x44, 0x6F, 0x65, (byte) 0xA3, 0x61, 0x67, 0x65, 0x1E, (byte) 0xA1, 0x74, (byte) 0x92, 0x05, 0x08, (byte) 0x90};
|
||||
ByteBuffer message = ByteBuffer.wrap(messageBytes);
|
||||
|
||||
TestBinder binder = new TestBinder(new Type[] { (new TypeReference<PersonPojo<ArrayList<Short>>>() { }).getType() }, null);
|
||||
|
||||
List<HubMessage> messages = messagePackHubProtocol.parseMessages(message, binder);
|
||||
|
||||
//We know it's only one message
|
||||
assertNotNull(messages);
|
||||
assertEquals(1, messages.size());
|
||||
|
||||
assertEquals(HubMessageType.INVOCATION, messages.get(0).getMessageType());
|
||||
|
||||
//We can safely cast here because we know that it's an invocation message.
|
||||
InvocationMessage invocationMessage = (InvocationMessage) messages.get(0);
|
||||
|
||||
assertEquals("test", invocationMessage.getTarget());
|
||||
assertEquals(null, invocationMessage.getInvocationId());
|
||||
assertEquals(null, invocationMessage.getHeaders());
|
||||
assertEquals(null, invocationMessage.getStreamIds());
|
||||
|
||||
@SuppressWarnings("unchecked")
|
||||
PersonPojo<ArrayList<Short>> result = (PersonPojo<ArrayList<Short>>)invocationMessage.getArguments()[0];
|
||||
assertEquals("John", result.getFirstName());
|
||||
assertEquals("Doe", result.getLastName());
|
||||
assertEquals(30, result.getAge());
|
||||
|
||||
ArrayList<Short> generic = result.getT();
|
||||
assertEquals(2, generic.size());
|
||||
assertEquals((short)5, (short)generic.get(0));
|
||||
assertEquals((short)8, (short)generic.get(1));
|
||||
}
|
||||
|
||||
@Test
|
||||
public void verifyWriteInvocationMessageWithCustomPojoArg() {
|
||||
ArrayList<Short> shorts = new ArrayList<Short>();
|
||||
shorts.add((short) 5);
|
||||
shorts.add((short) 8);
|
||||
|
||||
PersonPojo<ArrayList<Short>> person = new PersonPojo<ArrayList<Short>>("John", "Doe", 30, shorts);
|
||||
|
||||
InvocationMessage invocationMessage = new InvocationMessage(null, null, "test", new Object[] { person }, null);
|
||||
ByteBuffer result = messagePackHubProtocol.writeMessage(invocationMessage);
|
||||
byte[] expectedBytes = {0x32, (byte) 0x96, 0x01, (byte) 0x80, (byte) 0xC0, (byte) 0xA4, 0x74, 0x65, 0x73, 0x74, (byte) 0x91, (byte) 0x84, (byte) 0xA9,
|
||||
0x66, 0x69, 0x72, 0x73, 0x74, 0x4E, 0x61, 0x6D, 0x65, (byte) 0xA4, 0x4A, 0x6F, 0x68, 0x6E, (byte) 0xA8, 0x6C, 0x61, 0x73, 0x74, 0x4E, 0x61,
|
||||
0x6D, 0x65, (byte) 0xA3, 0x44, 0x6F, 0x65, (byte) 0xA3, 0x61, 0x67, 0x65, 0x1E, (byte) 0xA1, 0x74, (byte) 0x92, 0x05, 0x08, (byte) 0x90};
|
||||
ByteString expectedResult = ByteString.of(expectedBytes);
|
||||
assertEquals(expectedResult, ByteString.of(result));
|
||||
}
|
||||
}
|
||||
|
|
@ -3,6 +3,7 @@
|
|||
|
||||
package com.microsoft.signalr;
|
||||
|
||||
import java.nio.ByteBuffer;
|
||||
import java.util.ArrayList;
|
||||
|
||||
import io.reactivex.Completable;
|
||||
|
|
@ -11,14 +12,14 @@ import io.reactivex.subjects.SingleSubject;
|
|||
|
||||
class MockTransport implements Transport {
|
||||
private OnReceiveCallBack onReceiveCallBack;
|
||||
private ArrayList<String> sentMessages = new ArrayList<>();
|
||||
private ArrayList<ByteBuffer> sentMessages = new ArrayList<>();
|
||||
private String url;
|
||||
private TransportOnClosedCallback onClose;
|
||||
final private boolean ignorePings;
|
||||
final private boolean autoHandshake;
|
||||
final private CompletableSubject startSubject = CompletableSubject.create();
|
||||
final private CompletableSubject stopSubject = CompletableSubject.create();
|
||||
private SingleSubject<String> sendSubject = SingleSubject.create();
|
||||
private SingleSubject<ByteBuffer> sendSubject = SingleSubject.create();
|
||||
|
||||
private static final String RECORD_SEPARATOR = "\u001e";
|
||||
|
||||
|
|
@ -40,7 +41,7 @@ class MockTransport implements Transport {
|
|||
this.url = url;
|
||||
if (autoHandshake) {
|
||||
try {
|
||||
onReceiveCallBack.invoke("{}" + RECORD_SEPARATOR);
|
||||
onReceiveCallBack.invoke(TestUtils.stringToByteBuffer("{}" + RECORD_SEPARATOR));
|
||||
} catch (Exception e) {
|
||||
throw new RuntimeException(e);
|
||||
}
|
||||
|
|
@ -50,8 +51,8 @@ class MockTransport implements Transport {
|
|||
}
|
||||
|
||||
@Override
|
||||
public Completable send(String message) {
|
||||
if (!(ignorePings && message.equals("{\"type\":6}" + RECORD_SEPARATOR))) {
|
||||
public Completable send(ByteBuffer message) {
|
||||
if (!(ignorePings && isPing(message))) {
|
||||
sentMessages.add(message);
|
||||
sendSubject.onSuccess(message);
|
||||
sendSubject = SingleSubject.create();
|
||||
|
|
@ -65,7 +66,7 @@ class MockTransport implements Transport {
|
|||
}
|
||||
|
||||
@Override
|
||||
public void onReceive(String message) {
|
||||
public void onReceive(ByteBuffer message) {
|
||||
this.onReceiveCallBack.invoke(message);
|
||||
}
|
||||
|
||||
|
|
@ -86,14 +87,18 @@ class MockTransport implements Transport {
|
|||
}
|
||||
|
||||
public void receiveMessage(String message) {
|
||||
this.onReceive(TestUtils.stringToByteBuffer(message));
|
||||
}
|
||||
|
||||
public void receiveMessage(ByteBuffer message) {
|
||||
this.onReceive(message);
|
||||
}
|
||||
|
||||
public String[] getSentMessages() {
|
||||
return sentMessages.toArray(new String[sentMessages.size()]);
|
||||
public ByteBuffer[] getSentMessages() {
|
||||
return sentMessages.toArray(new ByteBuffer[sentMessages.size()]);
|
||||
}
|
||||
|
||||
public SingleSubject<String> getNextSentMessage() {
|
||||
public SingleSubject<ByteBuffer> getNextSentMessage() {
|
||||
return sendSubject;
|
||||
}
|
||||
|
||||
|
|
@ -108,4 +113,9 @@ class MockTransport implements Transport {
|
|||
public Completable getStopTask() {
|
||||
return stopSubject;
|
||||
}
|
||||
|
||||
private boolean isPing(ByteBuffer message) {
|
||||
return (TestUtils.byteBufferToString(message).equals("{\"type\":6}" + RECORD_SEPARATOR) ||
|
||||
(message.array()[0] == 2 && message.array()[1] == -111 && message.array()[2] == 6));
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -0,0 +1,43 @@
|
|||
// Copyright (c) .NET Foundation. All rights reserved.
|
||||
// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information.
|
||||
|
||||
package com.microsoft.signalr;
|
||||
|
||||
class PersonPojo<T> implements Comparable<PersonPojo<T>> {
|
||||
public String firstName;
|
||||
public String lastName;
|
||||
public int age;
|
||||
public T t;
|
||||
|
||||
public PersonPojo() {
|
||||
super();
|
||||
}
|
||||
|
||||
public PersonPojo(String firstName, String lastName, int age, T t) {
|
||||
this.firstName = firstName;
|
||||
this.lastName = lastName;
|
||||
this.age = age;
|
||||
this.t = t;
|
||||
}
|
||||
|
||||
public String getFirstName() {
|
||||
return this.firstName;
|
||||
}
|
||||
|
||||
public String getLastName() {
|
||||
return this.lastName;
|
||||
}
|
||||
|
||||
public int getAge() {
|
||||
return this.age;
|
||||
}
|
||||
|
||||
public T getT() {
|
||||
return t;
|
||||
}
|
||||
|
||||
@Override
|
||||
public int compareTo(PersonPojo<T> ep) {
|
||||
return 0;
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,32 @@
|
|||
// Copyright (c) .NET Foundation. All rights reserved.
|
||||
// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information.
|
||||
|
||||
package com.microsoft.signalr;
|
||||
|
||||
import java.lang.reflect.Type;
|
||||
import java.util.ArrayList;
|
||||
import java.util.Arrays;
|
||||
import java.util.List;
|
||||
|
||||
class TestBinder implements InvocationBinder {
|
||||
private Type[] paramTypes = null;
|
||||
private Type returnType = null;
|
||||
|
||||
public TestBinder(Type[] paramTypes, Type returnType) {
|
||||
this.paramTypes = paramTypes;
|
||||
this.returnType = returnType;
|
||||
}
|
||||
|
||||
@Override
|
||||
public Type getReturnType(String invocationId) {
|
||||
return returnType;
|
||||
}
|
||||
|
||||
@Override
|
||||
public List<Type> getParameterTypes(String methodName) {
|
||||
if (paramTypes == null) {
|
||||
return new ArrayList<>();
|
||||
}
|
||||
return new ArrayList<Type>(Arrays.asList(paramTypes));
|
||||
}
|
||||
}
|
||||
|
|
@ -3,6 +3,7 @@
|
|||
|
||||
package com.microsoft.signalr;
|
||||
|
||||
import java.nio.ByteBuffer;
|
||||
import java.util.ArrayList;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
|
|
@ -27,7 +28,7 @@ class TestHttpClient extends HttpClient {
|
|||
}
|
||||
|
||||
@Override
|
||||
public Single<HttpResponse> send(HttpRequest request, String body) {
|
||||
public Single<HttpResponse> send(HttpRequest request, ByteBuffer body) {
|
||||
this.sentRequests.add(request);
|
||||
return this.handler.invoke(request);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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));
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -5,6 +5,7 @@ package com.microsoft.signalr;
|
|||
|
||||
import static org.junit.jupiter.api.Assertions.*;
|
||||
|
||||
import java.nio.ByteBuffer;
|
||||
import java.util.HashMap;
|
||||
import java.util.Map;
|
||||
import java.util.concurrent.TimeUnit;
|
||||
|
|
@ -35,7 +36,7 @@ class WebSocketTransportTest {
|
|||
}
|
||||
|
||||
@Override
|
||||
public Single<HttpResponse> send(HttpRequest request, String body) {
|
||||
public Single<HttpResponse> send(HttpRequest request, ByteBuffer body) {
|
||||
return null;
|
||||
}
|
||||
|
||||
|
|
@ -71,7 +72,7 @@ class WebSocketTransportTest {
|
|||
}
|
||||
|
||||
@Override
|
||||
public Completable send(String message) {
|
||||
public Completable send(ByteBuffer message) {
|
||||
return null;
|
||||
}
|
||||
|
||||
|
|
|
|||
Loading…
Reference in New Issue