From b0ce55e6c86a18d5e874456f1fd1fe49c70fac1f Mon Sep 17 00:00:00 2001 From: Mikael Mengistu Date: Tue, 26 Jun 2018 14:38:21 -0700 Subject: [PATCH] Adding the HubProtocol Layer to the Java Client (#2524) --- .../DefaultJsonProtocolHandShakeMessage.java | 12 ++- .../signalr/src/main/java/HubConnection.java | 66 +++--------- .../signalr/src/main/java/HubProtocol.java | 11 ++ .../src/main/java/InvocationMessage.java | 4 +- .../src/main/java/JsonHubProtocol.java | 84 +++++++++++++++ .../signalr/src/main/java/TransferFormat.java | 7 ++ .../java/{ITransport.java => Transport.java} | 4 +- .../src/main/java/WebSocketTransport.java | 14 +-- .../src/test/java/JsonHubProtocolTest.java | 102 ++++++++++++++++++ 9 files changed, 234 insertions(+), 70 deletions(-) create mode 100644 clients/java/signalr/src/main/java/HubProtocol.java create mode 100644 clients/java/signalr/src/main/java/JsonHubProtocol.java create mode 100644 clients/java/signalr/src/main/java/TransferFormat.java rename clients/java/signalr/src/main/java/{ITransport.java => Transport.java} (79%) create mode 100644 clients/java/signalr/src/test/java/JsonHubProtocolTest.java diff --git a/clients/java/signalr/src/main/java/DefaultJsonProtocolHandShakeMessage.java b/clients/java/signalr/src/main/java/DefaultJsonProtocolHandShakeMessage.java index 985cf8f21e..7027e0d637 100644 --- a/clients/java/signalr/src/main/java/DefaultJsonProtocolHandShakeMessage.java +++ b/clients/java/signalr/src/main/java/DefaultJsonProtocolHandShakeMessage.java @@ -1,7 +1,15 @@ // 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. +import com.google.gson.Gson; + public class DefaultJsonProtocolHandShakeMessage { - String protocol = "json"; - int version = 1; + private String protocol = "json"; + private int version = 1; + private static final String RECORD_SEPARATOR = "\u001e"; + + public String createHandshakeMessage() { + Gson gson = new Gson(); + return gson.toJson(this) + RECORD_SEPARATOR; + } } diff --git a/clients/java/signalr/src/main/java/HubConnection.java b/clients/java/signalr/src/main/java/HubConnection.java index ab4f0a1c52..5bf6f3d43e 100644 --- a/clients/java/signalr/src/main/java/HubConnection.java +++ b/clients/java/signalr/src/main/java/HubConnection.java @@ -1,34 +1,31 @@ // 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. -import com.google.gson.JsonObject; -import com.google.gson.JsonParser; - import java.net.URISyntaxException; import java.util.HashMap; public class HubConnection { private String _url; - private ITransport _transport; + private Transport _transport; private OnReceiveCallBack callback; private HashMap handlers = new HashMap<>(); - private JsonParser jsonParser = new JsonParser(); - private static final String RECORD_SEPARATOR = "\u001e"; + private HubProtocol protocol; public Boolean connected = false; public HubConnection(String url) { _url = url; + protocol = new JsonHubProtocol(); callback = (payload) -> { - String[] messages = payload.split(RECORD_SEPARATOR); - for (String splitMessage : messages) { + InvocationMessage[] messages = protocol.parseMessages(payload); - // Empty handshake response "{}". We can ignore it - if (splitMessage.length() == 2) { - continue; + // message will be null if we receive any message other than an invocation. + // Adding this to avoid getting error messages on pings for now. + for (InvocationMessage message : messages) { + if (message != null && handlers.containsKey(message.target)) { + handlers.get(message.target).invoke(message.arguments[0]); } - processMessage(splitMessage); } }; @@ -39,44 +36,6 @@ public class HubConnection { } } - private void processMessage(String message) { - JsonObject jsonMessage = jsonParser.parse(message).getAsJsonObject(); - String messageType = jsonMessage.get("type").toString(); - switch(messageType) { - case "1": - //Invocation Message - String target = jsonMessage.get("target").getAsString(); - if (handlers.containsKey(target)) { - handlers.get(target).invoke(jsonMessage.get("arguments")); - } - break; - case "2": - //Stream item - //Don't care yet - break; - case "3": - //Completion - //Don't care yet - break; - case "4": - //Stream invocation - //Don't care yet; - break; - case "5": - //Cancel invocation - //Don't care yet - break; - case "6": - //Ping - //Don't care yet - break; - case "7": - // Close message - //Don't care yet; - break; - } - } - public void start() throws InterruptedException { _transport.setOnReceive(this.callback); _transport.start(); @@ -88,12 +47,13 @@ public class HubConnection { connected = false; } - public void send(String method, Object arg1) { - InvocationMessage message = new InvocationMessage(method, new Object[]{ arg1 }); + public void send(String method, Object... args) { + InvocationMessage invocationMessage = new InvocationMessage(method, args); + String message = protocol.writeMessage(invocationMessage); _transport.send(message); } public void On(String target, Action callback) { handlers.put(target, callback); } -} +} \ No newline at end of file diff --git a/clients/java/signalr/src/main/java/HubProtocol.java b/clients/java/signalr/src/main/java/HubProtocol.java new file mode 100644 index 0000000000..06413f5111 --- /dev/null +++ b/clients/java/signalr/src/main/java/HubProtocol.java @@ -0,0 +1,11 @@ +// 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. + +public interface HubProtocol { + String getName(); + int getVersion(); + TransferFormat getTransferFormat(); + InvocationMessage[] parseMessages(String message); + String writeMessage(InvocationMessage message); +} + diff --git a/clients/java/signalr/src/main/java/InvocationMessage.java b/clients/java/signalr/src/main/java/InvocationMessage.java index 7abb277aaa..258d57a058 100644 --- a/clients/java/signalr/src/main/java/InvocationMessage.java +++ b/clients/java/signalr/src/main/java/InvocationMessage.java @@ -2,7 +2,7 @@ // Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. public class InvocationMessage { - private int type = 1; + int type = 1; String invocationId; String target; Object[] arguments; @@ -28,7 +28,7 @@ public class InvocationMessage { this.target = target; } - public Object getArguments() { + public Object[] getArguments() { return arguments; } diff --git a/clients/java/signalr/src/main/java/JsonHubProtocol.java b/clients/java/signalr/src/main/java/JsonHubProtocol.java new file mode 100644 index 0000000000..8e5ddd71ee --- /dev/null +++ b/clients/java/signalr/src/main/java/JsonHubProtocol.java @@ -0,0 +1,84 @@ +// 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. + +import com.google.gson.Gson; +import com.google.gson.JsonElement; +import com.google.gson.JsonObject; +import com.google.gson.JsonParser; + +import java.util.ArrayList; +import java.util.List; + +public class JsonHubProtocol implements HubProtocol { + private final JsonParser jsonParser = new JsonParser(); + private final Gson gson = new Gson(); + private static final String RECORD_SEPARATOR = "\u001e"; + + @Override + public String getName() { + return "json"; + } + + @Override + public int getVersion() { + return 1; + } + + @Override + public TransferFormat getTransferFormat() { + return TransferFormat.Text; + } + + @Override + public InvocationMessage[] parseMessages(String payload) { + String[] messages = payload.split(RECORD_SEPARATOR); + List invocationMessages = new ArrayList<>(); + for (String splitMessage : messages) { + // Empty handshake response "{}". We can ignore it + if (splitMessage.equals("{}")) { + continue; + } + + JsonObject jsonMessage = jsonParser.parse(splitMessage).getAsJsonObject(); + String messageType = jsonMessage.get("type").toString(); + switch (messageType) { + case "1": + //Invocation Message + String target = jsonMessage.get("target").getAsString(); + JsonElement args = jsonMessage.get("arguments"); + invocationMessages.add(new InvocationMessage(target, new Object[] {args})); + break; + case "2": + //Stream item + //Don't care yet + break; + case "3": + //Completion + //Don't care yet + break; + case "4": + //Stream invocation + //Don't care yet; + break; + case "5": + //Cancel invocation + //Don't care yet + break; + case "6": + //Ping + //Don't care yet + break; + case "7": + // Close message + //Don't care yet; + break; + } + } + return invocationMessages.toArray(new InvocationMessage[invocationMessages.size()]); + } + + @Override + public String writeMessage(InvocationMessage invocationMessage) { + return gson.toJson(invocationMessage) + RECORD_SEPARATOR; + } +} diff --git a/clients/java/signalr/src/main/java/TransferFormat.java b/clients/java/signalr/src/main/java/TransferFormat.java new file mode 100644 index 0000000000..8813920de6 --- /dev/null +++ b/clients/java/signalr/src/main/java/TransferFormat.java @@ -0,0 +1,7 @@ +// 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. + +public enum TransferFormat { + Text, + Binary +} diff --git a/clients/java/signalr/src/main/java/ITransport.java b/clients/java/signalr/src/main/java/Transport.java similarity index 79% rename from clients/java/signalr/src/main/java/ITransport.java rename to clients/java/signalr/src/main/java/Transport.java index bb582ed17a..819460b9d1 100644 --- a/clients/java/signalr/src/main/java/ITransport.java +++ b/clients/java/signalr/src/main/java/Transport.java @@ -1,9 +1,9 @@ // 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. -public interface ITransport { +public interface Transport { void start() throws InterruptedException; - void send(InvocationMessage invocationMessage); + void send(String message); void setOnReceive(OnReceiveCallBack callback); void onReceive(String message); void stop(); diff --git a/clients/java/signalr/src/main/java/WebSocketTransport.java b/clients/java/signalr/src/main/java/WebSocketTransport.java index 650032a5b7..1fa6a68b70 100644 --- a/clients/java/signalr/src/main/java/WebSocketTransport.java +++ b/clients/java/signalr/src/main/java/WebSocketTransport.java @@ -8,8 +8,7 @@ import org.java_websocket.handshake.ServerHandshake; import java.net.URI; import java.net.URISyntaxException; -public class WebSocketTransport implements ITransport { - private static final String RECORD_SEPARATOR = "\u001e"; +public class WebSocketTransport implements Transport { private WebSocketClient _webSocket; private OnReceiveCallBack onReceiveCallBack; private URI _url; @@ -23,13 +22,11 @@ public class WebSocketTransport implements ITransport { @Override public void start() throws InterruptedException { _webSocket.connectBlocking(); - _webSocket.send(createHandshakeMessage() + RECORD_SEPARATOR); + _webSocket.send((new DefaultJsonProtocolHandShakeMessage()).createHandshakeMessage()); } @Override - public void send(InvocationMessage invocationMessage) { - Gson gson = new Gson(); - String message = gson.toJson(invocationMessage) + RECORD_SEPARATOR; + public void send(String message) { _webSocket.send(message); } @@ -43,11 +40,6 @@ public class WebSocketTransport implements ITransport { this.onReceiveCallBack.invoke(message); } - private String createHandshakeMessage() { - Gson gson = new Gson(); - return gson.toJson(new DefaultJsonProtocolHandShakeMessage()); - } - @Override public void stop() { _webSocket.closeConnection(0, "HubConnection Stopped"); diff --git a/clients/java/signalr/src/test/java/JsonHubProtocolTest.java b/clients/java/signalr/src/test/java/JsonHubProtocolTest.java new file mode 100644 index 0000000000..dbdbbe2e05 --- /dev/null +++ b/clients/java/signalr/src/test/java/JsonHubProtocolTest.java @@ -0,0 +1,102 @@ +// 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. + +import com.google.gson.JsonArray; +import org.junit.Test; + +import static org.junit.Assert.*; + +public class JsonHubProtocolTest { + private JsonHubProtocol jsonHubProtocol = new JsonHubProtocol(); + private static final String RECORD_SEPARATOR = "\u001e"; + + @Test + public void checkProtocolName() { + assertEquals("json", jsonHubProtocol.getName()); + } + + @Test + public void checkVersionNumber() { + assertEquals(1, jsonHubProtocol.getVersion()); + } + + @Test + public void checkTransferFormat() { + assertEquals(TransferFormat.Text, jsonHubProtocol.getTransferFormat()); + } + + @Test + public void VerifyWriteMessage() { + InvocationMessage invocationMessage = new InvocationMessage("test", new Object[] {"42"}); + String result = jsonHubProtocol.writeMessage(invocationMessage); + String expectedResult = "{\"type\":1,\"target\":\"test\",\"arguments\":[\"42\"]}\u001E"; + assertEquals(expectedResult, result); + } + + @Test + public void ParseSingleMessage() { + String stringifiedMessage = "{\"type\":1,\"target\":\"test\",\"arguments\":[42]}\u001E"; + InvocationMessage[] messages = jsonHubProtocol.parseMessages(stringifiedMessage); + + //We know it's only one message + assertEquals(1, messages.length); + InvocationMessage message = messages[0]; + assertEquals("test", message.target); + assertEquals(null, message.invocationId); + assertEquals(1, message.type); + JsonArray messageResult = (JsonArray) message.arguments[0]; + assertEquals(42, messageResult.getAsInt()); + } + + @Test + public void ParseHandshakeResponsePlusMessage() { + String twoMessages = "{}\u001E{\"type\":1,\"target\":\"test\",\"arguments\":[42]}\u001E"; + InvocationMessage[] messages = jsonHubProtocol.parseMessages(twoMessages); + + //We ignore the Handshake response for now + InvocationMessage message = messages[0]; + assertEquals("test", message.target); + assertEquals(null, message.invocationId); + assertEquals(1, message.type); + JsonArray messageResult = (JsonArray) message.arguments[0]; + assertEquals(42, messageResult.getAsInt()); + } + + @Test + public void ParseTwoMessages() { + String twoMessages = "{\"type\":1,\"target\":\"one\",\"arguments\":[42]}\u001E{\"type\":1,\"target\":\"two\",\"arguments\":[43]}\u001E"; + InvocationMessage[] messages = jsonHubProtocol.parseMessages(twoMessages); + assertEquals(2, messages.length); + + // Check the first message + InvocationMessage message = messages[0]; + assertEquals("one", message.target); + assertEquals(null, message.invocationId); + assertEquals(1, message.type); + JsonArray messageResult = (JsonArray) message.arguments[0]; + assertEquals(42, messageResult.getAsInt()); + + // Check the second message + InvocationMessage secondMessage = messages[1]; + assertEquals("two", secondMessage.target); + assertEquals(null, secondMessage.invocationId); + assertEquals(1, secondMessage.type); + JsonArray secondMessageResult = (JsonArray) secondMessage.arguments[0]; + assertEquals(43, secondMessageResult.getAsInt()); + } + + @Test + public void ParseSingleMessageMutipleArgs() { + String stringifiedMessage = "{\"type\":1,\"target\":\"test\",\"arguments\":[42, 24]}\u001E"; + InvocationMessage[] messages = jsonHubProtocol.parseMessages(stringifiedMessage); + + //We know it's only one message + InvocationMessage message = messages[0]; + assertEquals("test", message.target); + assertEquals(null, message.invocationId); + assertEquals(1, message.type); + JsonArray messageResult = ((JsonArray) message.arguments[0]); + assertEquals(42, messageResult.get(0).getAsInt()); + assertEquals(24, messageResult.get(1).getAsInt()); + } +} \ No newline at end of file